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

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

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

READY TO UPGRADE YOUR RISK MANAGEMENT?

Make cybersecurity and compliance efficient and effective with ONEKEY.

Book a Demo

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. [caption id="attachment_3886" align="aligncenter" width="600"]D-Link Dir 3060 © D-Link D-Link Dir 3060 © D-Link[/caption]
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 
<span data-contrast="auto">prog.cgi</span>
prog.cgi binary. The
<span data-contrast="none">lighttpd</span>
lighttpd 
<span data-contrast="auto">fastcgi</span>
fastcgi server configuration is such that requests made to 
/HNAP1/
/HNAP1/ or files with the
<span data-contrast="auto">.fcgi</span>
.fcgi extension are handled by 
<span data-contrast="none">/etc_ro/lighttpd/www/web/HNAP1/prog.fcgi</span>
/etc_ro/lighttpd/www/web/HNAP1/prog.fcgi, which is a symlink to 
<span data-contrast="none">/bin/</span><span data-contrast="none">prog.cgi</span>
/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
))
)
// ...
// ... 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 )) ) // ...
// ... 
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/
http://[ROUTER]/HNAP1/ - these are then all then handled by
<span data-contrast="auto">prog.cgi</span>
prog.cgi. 

The Vulnerability 

When a SOAP request is made to the 
<span data-contrast="auto">SetVirtualServerSettings</span>
SetVirtualServerSettings SOAP endpoint, the function at 
<span data-contrast="auto">00461918</span>
00461918  in 
<span data-contrast="auto">pro</span><span data-contrast="auto">g</span><span data-contrast="auto">.cgi</span>
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 
<span data-contrast="auto">LocalIPAddress</span>
LocalIPAddress
Enabled
Enabled 
set to “true”, an 
<span data-contrast="auto">InternalPort</span>
InternalPort of “9” and a 
<span data-contrast="auto">ProtocolType</span>
ProtocolType of “UDP” is sent, the function 
<span data-contrast="auto">CheckArpTables</span>
CheckArpTables (named by me, based at 
<span data-contrast="auto">0046163c</span>
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
// ...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
// ...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 
<span data-contrast="auto">/dev/null</span>
/dev/null.  The 
<span data-contrast="auto">CheckArpTables</span><span data-contrast="auto">()</span>
CheckArpTables() function attempts to check the device ARP records, by calling the 
<span data-contrast="auto">arp</span>
arp system command and 
<span data-contrast="auto">grep</span>
greping the output. However, the user-controlled value passed as the 
<span data-contrast="auto">LocalIPAddress</span>
LocalIPAddress is written directly into the command line format string with 
<span data-contrast="auto">snprint</span><span data-contrast="auto">()</span>
snprint(). This string is then passed directly to a function called 
<span data-contrast="auto">FCGI_popen</span><span data-contrast="auto">()</span>
FCGI_popen(), which is a library function imported from 
<span data-contrast="auto">libfcgi</span><span data-contrast="auto">.so</span>
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...
}
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... }
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 
<span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SCXW37152371 BCX0">libfcgi.so</span></span>
libfcgi.so that 
<span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SpellingErrorV2 SCXW37152371 BCX0">FCGI_popen</span></span><span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SCXW37152371 BCX0">()</span></span>
FCGI_popen() is essentially only a thin wrapper around the stdio 
<span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SpellingErrorV2 SCXW37152371 BCX0">popen</span></span><span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SCXW37152371 BCX0">()</span></span>
popen() library function. Arguments passed to 
<span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SpellingErrorV2 SCXW37152371 BCX0">FCGI_popen</span></span><span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SCXW37152371 BCX0">()</span></span>
FCGI_popen() get passed directly to 
<span class="TextRun SCXW37152371 BCX0" xml:lang="EN-US" data-contrast="auto" lang="EN-US"><span class="NormalTextRun SpellingErrorV2 SCXW37152371 BCX0">popen</span><span class="NormalTextRun SCXW37152371 BCX0">()</span></span>
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;
}
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; }
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
<span data-contrast="auto">LocalIPAddress</span>
LocalIPAddress value is not sanitized or checked in any way, a crafted command injection string can be passed as the 
<span data-contrast="auto">LocalIPAddress</span>
LocalIPAddress, which will then be written to the 
<span data-contrast="auto">arp</span>
arp command format string, and passed (almost) directly to 
<span data-contrast="auto">popen</span><span data-contrast="auto">()</span>
popen().  Copy Of Ads 480 120

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 
<span data-contrast="auto">FCGI_popen</span><span data-contrast="auto">()</span>
FCGI_popen() function as a drop-in replacement for 
<span data-contrast="auto">popen</span><span data-contrast="auto">()</span>
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 
<span data-contrast="auto">FCGI_popen</span><span data-contrast="auto">()</span>
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.
Share

About Onekey

ONEKEY is the leading European specialist in Product Cybersecurity & Compliance Management and part of the investment portfolio of PricewaterhouseCoopers Germany (PwC). The unique combination of the automated ONEKEY Product Cybersecurity & Compliance Platform (OCP) with expert knowledge and consulting services provides fast and comprehensive analysis, support, and management to improve product cybersecurity and compliance from product purchasing, design, development, production to end-of-life.

CONTACT:
Sara Fortmann

Senior Marketing Manager
sara.fortmann@onekey.com

euromarcom public relations GmbH
team@euromarcom.de

RELATED RESEARCH ARTICLES

Unblob 2024 Highlights: Sandboxing, Reporting, and Community Milestones
Critical Vulnerabilities in EV Charging Stations: Analysis of eCharge Controllers
Security Advisory: Unauthenticated Command Injection in Mitel IP Phones

Ready to automate your Product Cybersecurity & Compliance?

Make cybersecurity and compliance efficient and effective with ONEKEY.