Advisory: D-Link DIR-3060 Authenticated RCE (CVE-2021-28144)

Overview 

The D-Link DIR-3060 (running firmware versions below v1.11b04) is affected by a post-authentication command injection vulnerability. Anybody with authenticated access to a DIR-3060 would be able to run arbitrary system commands on the device as the system “admin” user, with root privileges. D-Link has released a patched firmware version v1.11b04 Hotfix 2 to address this issue. Affected users are advised to apply the patch.

D-Link Dir 3060 © D-Link
D-Link Dir 3060 © D-Link
Affected vendor & product D-Link DIR-3060 (www.dlink.com)
Vulnerable version v1.11b04 & below
Fixed version v1.11b04 Hotfix 2
CVE Number CVE-2021-28144
Impact 8.8 (high) CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Credit T. Shiomitsu, IoT Inspector Research Lab

Vulnerable Component 

Web management functionality on the DIR-3060 is mainly handled by the prog.cgi binary. The lighttpd fastcgi server configuration is such that requests made to /HNAP1/ or files with the .fcgi extension are handled by /etc_ro/lighttpd/www/web/HNAP1/prog.fcgi, which is a symlink to /bin/prog.cgi.

 

// ... 
fastcgi.server = (  
    "/HNAP1/" =>  
    (( 
        "socket" => "/var/prog.fcgi.socket-0", 
        "check-local" => "enable", 
        "bin-path" => "/etc_ro/lighttpd/www/web/HNAP1/prog.fcgi", 
        "idle-timeout" => 10, 
        "min-procs" => 1, 
        "max-procs" => 2 
    )),  
    ".fcgi" =>  
    (( 
        "socket" => "/var/prog.fcgi.socket-0", 
        "check-local" => "enable", 
        "bin-path" => "/etc_ro/lighttpd/www/web/HNAP1/prog.fcgi", 
        "idle-timeout" => 10, 
        "min-procs" => 1, 
        "max-procs" => 2 
    )), 
    "/common/" =>  
    (( 
        "socket" => "/var/myinfo.fcgi.socket-0", 
        "check-local" => "disable", 
        "bin-path" => "/sbin/myinfo.cgi", 
        "idle-timeout" => 10, 
        "min-procs" => 1, 
        "max-procs" => 1 
    )) 
) 
// ...

By default, configuration changes are made by issuing SOAP requests to the web management interface at http://[ROUTER]/HNAP1/ – these are then all then handled by prog.cgi. 

The Vulnerability 

When a SOAP request is made to the SetVirtualServerSettings SOAP endpoint, the function at 00461918  in prog.cgi is invoked. This function traverses the SOAP XML request body, stores expected SOAP field values, and takes different paths depending on the values. 

If a request with a non-null LocalIPAddressEnabled set to “true”, an InternalPort of “9” and a ProtocolType of “UDP” is sent, the function CheckArpTables (named by me, based at 0046163c) is invoked.

 

// ...snip 
      iVar5 = strcmp(Enabled,"true"); 
      if ((((iVar5 == 0) && (LocalIPAddress != (char *)0x0)) && 
          (iVar5 = strcmp(InternalPort,"9"), iVar5 == 0)) && 
         (iVar5 = strcmp(ProtocolType,"UDP"), iVar5 == 0)) { 
        local_4154 = local_4154 + 1; 
        iVar5 = CheckArpTables(LocalIPAddress, InternalPort, ProtocolType, 0xdc, local_4154); 
        if (iVar5 == -1) { 
            local_4160 = 0xb; 
            goto LAB_00462504; 
        } 
      } 
// ...snip

Interestingly, UDP/9 correlates to the canonical Discard Protocolwhich is the TCP/UDP/IP equivalent of /dev/null. 

The CheckArpTables() function attempts to check the device ARP records, by calling the arp system command and greping the output. However, the user-controlled value passed as the LocalIPAddress is written directly into the command line format string with snprint(). This string is then passed directly to a function called FCGI_popen(), which is a library function imported from libfcgi.so.

 

undefined CheckArpTables(char *LocalIPAddress, char *InternalPort, char *ProtocolType, undefined param_4, int param_5) {
    // ...snip...
    memset(buffer, 0, 0x40);
    // ...snip...
    snprintf(buffer, 0x40, "arp | grep %s | awk \'{printf $4}\'", LocalIPAddress);
    iVar1 = FCGI_popen(buffer, "r");
    // ...snip... 
}

We can see in libfcgi.so that FCGI_popen() is essentially only a thin wrapper around the stdio popen() library function. Arguments passed to FCGI_popen() get passed directly to popen().

 

int FCGI_popen(char *param_1, char *param_2) 
{
  FILE *__stream; 
  int iVar1; 
  __stream = popen(param_1,param_2); 
  iVar1 = FCGI_OpenFromFILE(__stream); 
  if ((__stream != (FILE *)0x0) && (iVar1 == 0)) { 
    pclose(__stream); 
  } 
  return iVar1; 
}

Since the LocalIPAddress value is not sanitized or checked in any way, a crafted command injection string can be passed as the LocalIPAddress, which will then be written to the arp command format string, and passed (almost) directly to popen(). 

Key Takeaways 

Abstraction of common library functions with potential security implications is common in embedded device development. Many embedded developers attempt to do this in order to easily “drop-in” common library functions with more secure analogues. 

However, D-Link, in this case, did not do this. They had abstracted (and used) the FCGI_popen() function as a drop-in replacement for popen()  presumably in order to ensure that the implementation could be standardized for code cleanliness (and perhaps security) purposesHowever, there was no extra checking or sanitization in place in the actual FCGI_popen() function. Therefore, there was no particular security benefit to this abstraction.  

From our perspective (we’re in the business of automating security analysis of embedded device firmware, in case you didn’t know) this is an interesting case. Vendors often use such drop-in replacement functions, imported from external libraries. When running our automated security analyses of ELF executables, we have to take into account which imported functions are used within aELF, as well as how exposed potentially dangerous library functions are within these functionsThis can help us drill down more intentionally into these executables to flag potential security issues. This kind of automation can considerably speed up recognition of potential security issues. 

Disclosure Timeline 

2020-11-16: Initial contact made to ipsecure@dlinkcorp.com to request keys for encryption.
2020-11-20: No reply received, so follow-up e-mail sent.
2020-11-27: No reply received, so advisory sent by e-mail without encryption.
2021-02-03No reply received, so follow-up e-mail sent.
2021-02-12No reply received, so inquiry sent using the forms at support.dlink.com and eu.dlink.com/uk/en/contact-d-link.
2021-02-17: Response from the US D-Link support team, pointing us towards the US-specific D-Link security page.
2021-02-17: Sent e-mail to this new US-specific D-Link security e-mail address.
2021-02-19: Response from a member of the D-Link USA SIRT.
2021-02-19: We request a public key from D-Link USA for transmission of the advisory.
2021-02-19: PGP public key is provided by D-Link USA.
2021-02-19: Advisory is sent to D-Link USA with encryption.
2021-02-19: Receipt of advisory is confirmed by D-Link USA SIRT.
2021-02-19: We reply and ask for D-Link USA to keep us updated.
2021-02-20: D-Link “ipsecure” finally answers our e-mail, saying that security@dlink.com should be the official e-mail, and the ipsecure@dlinkcorp.com e-mail (the only one listed on the main D-Link security disclosure page) is only a backup address.
2021-02-22: D-Link USA responds, confirming that the e-mail address listed on the main D-Link security page has been changed.
2021-03-02: We e-mail D-Link USA to ask for a status update.
2021-03-02: D-Link USA responds with status update.
2021-03-08: D-Link USA provides patched firmware for testing.
2021-03-08: We respond asking for assigned CVE number.
2021-03-08: D-Link USA notes that they do not apply for, or manage CVE numbers related to their own products.
2021-03-08: We apply for a CVE number for this issue.
2021-03-08: D-Link USA publishes public advisory.
2021-03-11: CVE is assigned & IoT Inspector Research Lab publishes advisory.