The D-Link DIR-878 is a Wi-Fi router that reached its End of Life. Many vulnerabilities were discovered on this device. Today, my task was to help a colleague of mine in its master thesis research project. His project concerns analyzing, studying and locating well known vulnerabilities in firmware.

He shared me a curated list of vulnerabilities which needed some analysis so that their data could be added to its thesis. It is important to note that even though I performed this analysis, I have no merit in the content of his thesis whatsoever.

The vulnerability I am going to discuss, already had a Writeup (archived), but it was very concise and giving little information. My job was to retrieve more information and document better the extent of the specific vulnerability.

This was one of the first MIPS reverse engineering tasks I ever done, and I am quite happy with the results.

As you might notice by this post’s date, I started the report using my blog template this morning. After finishing the report, I asked my colleague if I could publish my results.

Follows a some-what technical report revisited to be published here. I hope you enjoy this one, I surely had a nice time writing it. Have a nice read!

Vulnerability Analysis: CVE-2021-44882 (Command Injection)

D-Link device DIR_878_FW1.30B08_Hotfix_02 was discovered to contain a command injection vulnerability in the twsystem function. This vulnerability allows attackers to execute arbitrary commands via a crafted HNAP1 POST request.

Firmware retrieval

My colleague provided me with the firmware image. The firmware version that was analyzed is version 1.20B05.

Firmware extraction

Sha256 hash

22724f7e1978f1eeb8567ccc80dd9ca7bc4647ccb6f850d48bd4d2162bdccf7e  DIR_878_FW120B05.BIN

The file and binwalk utilities were used to check the file type and content, without success:

$ file DIR_878_FW120B05.BIN
DIR_878_FW120B05.BIN: data
$ binwalk DIR_878_FW120B05.BIN

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------

The hexdump tool was used to check the start of the file:

Hexdump output showing SHRS starting

Magic signature at the start of the firmware

Searching for the (supposed) magic string SHRS yielded positive results:

Searching for 'SHRS file magic' yielded results for a decryptor

Decrypting SHRS D-Link firmware

It was possible to extract the firmware using unblob:

Extracting the firmware using unblob

Extracting the firmware using unblob

$ cd DIR_878_FW120B05.BIN_extract/
$ tree --charset ascii
.
|-- 0-11183836.shrs_extract
|   |-- 0-11183836.shrs.decrypted
|   `-- 0-11183836.shrs.decrypted_extract
|       |-- 0-160.unknown
|       |-- 11181231-11182080.unknown
|       `-- 160-11181231.lzma_extract
|           |-- lzma.uncompressed
|           `-- lzma.uncompressed_extract
|               |-- 0-6357448.unknown
|               |-- 16548583-16562624.unknown
|               |-- 6357448-6370037.gzip_extract
|               |   `-- gzip.uncompressed
|               |-- 6370037-9000924.unknown
|               `-- 9000924-16548583.lzma_extract
|                   |-- lzma.uncompressed
|                   `-- lzma.uncompressed_extract
|                       |-- bin
|                       |   |-- ac
[....]

103 directories, 1429 files

The full listing of the firmware content is available here.

The directory lzma.uncompressed_extract was then moved to the current working directory as rootfs:

$ cd ..
$ mv 'DIR_878_FW120B05.BIN_extract/0-11183836.shrs_extract/0-11183836.shrs.decrypted_extract/160-11181231.lzma_extract/lzma.uncompressed_extract/9000924-16548583.lzma_extract/lzma.uncompressed_extract' 'rootfs'

Firmware analysis

Binary 1: prog.cgi

Sha256 hash

e2bfcdec07a6ef5c5367180e2d8fe85dc6b6fbf28757b279272e9e7a5a48f880 prog.cgi

Paper found on a similar binary: https://www.usenix.org/system/files/sec21fall-chen-libo.pdf (might be useful for citations)

The target prog.cgi file was located:

$ find rootfs/ -name "prog.cgi"
rootfs/bin/prog.cgi
$ file rootfs/bin/prog.cgi 
rootfs/bin/prog.cgi: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

Then, references to the vulnerable functions (according to the writeup), were searched:

$ readelf -a rootfs/bin/prog.cgi | rg twsystem
   482: 004b2850     0 FUNC    GLOBAL DEFAULT  UND twsystem
  004d64e0 -30864(gp) 004b2850 004b2850 FUNC    UND twsystem
$ strings rootfs/bin/prog.cgi | rg SetNetworkSettings
<string>http://purenetworks.com/HNAP1/SetNetworkSettings</string>
/SetNetworkSettings/IPAddress
/SetNetworkSettings/SubnetMask
/SetNetworkSettings/DeviceName
/SetNetworkSettings/LocalDomainName
/SetNetworkSettings/IPRangeStart
/SetNetworkSettings/IPRangeEnd
/SetNetworkSettings/LeaseTime
/SetNetworkSettings/Broadcast
/SetNetworkSettings/DNSRelay
SetNetworkSettings
Terminal showing output of the initial analysis of the prog.cgi binary

Initial analysis of the prog.cgi binary

Looking at etc_ro/lighttpd/lighttpd.conf it was possible to confirm that the prog.cgi program was the one used to dispatch /HNAP1/ requests:

Configuration of the lighttpd HTTP server, showing that prog.cgi is used as the CGI handler

lighttpd.conf HTTP CGI configuration

The file was loaded in Ghidra:

Loading prog.cgi in Ghidra

Loading prog.cgi in Ghidra

Trying to importing it without any additional configuration threw the following warnings:


------------------------------------------------

Linking the External Programs of 'prog.cgi' to imported libraries...
  [libnvram.so.0] -> not found in project
  [libc.so.0] -> not found in project
  [libfcgi.so.0] -> not found in project
  [libjson-c.so.2] -> not found in project
  [libm.so.0] -> not found in project
  [libcrypto.so.1.0.0] -> not found in project
  [libnotifyrc.so] -> not found in project
  [librcm.so] -> not found in project
  [libssl.so.1.0.0] -> not found in project
------------------------------------------------

Resolving External Symbols of [/prog.cgi] - 197 unresolved symbols, no external libraries configured - skipping

It was possible to solve this warning by going into the Options and enabling the Load System Libraries From Disk and specifying the rootfs/lib as the only path to use for the resolutions of the dependencies:

Enabling Load System Libraries from Disk in Ghidra and specifying the path of rootfs/lib using Edit Paths

Loading System Libraries configuration

Thereafter, the prog.cgi was opened in the Ghidra CodeBrowser and analyzed with standard options. The resulting Symbol Tree contained many symbols:

Symbol Tree for prog.cgi, showing many symbols

prog.cgi Symbol Tree

By searching for the SetNetworkSetting string, it was possible to locate the ModuleInitNetwork which calls the primitive websFormDefine to link the SetNetworkSettings method with the function 0x0043a08c in .text:

Decompiled code of ModuleInitNetwork function, showing how the SetNetworkSettings function is hooked

prog.cgi ModuleInitNetwork

At address 0x0043a2e4 the SetNetworkSettings function, it was possible to note how the DeviceName variable is stored in local variable local_124 after the function call:

0043a2c8   addiu      a1=>s_/SetNetworkSettings/DeviceName_004bb3f0,   = "/SetNetworkSettings/DeviceName"
0043a2cc   lw         v0,-0x7dc8(gp)=>->webGetVarString                = 0041a364
0043a2d0   nop
0043a2d4   move       t9,v0
0043a2d8   bal        webGetVarString                                  undefined webGetVarString()
0043a2dc   _nop
0043a2e0   lw         gp,local_138(sp)
0043a2e4   sw         v0,local_124(sp)
0043a2e8   lw         v0,local_124(sp)
Disassembly of the first portion of the vulnerability in SetNetworkSettings

prog.cgi SetNetworkSettings (1/2)

At address 0x0043a828 of the SetNetworkSettings, it was possible to note how the DeviceName variable is retrieved from the stack and passed as a second parameter to the function nvram_safe_set from libnvram.so:

0043a820   lui        v0,0x4c
0043a824   addiu      a0=>s_lan0_management_link_004bb330,v0,-0x4cd0   = "lan0_management_link"
0043a828   lw         a1,local_124(sp)
0043a82c   lw         v0,-0x7a6c(gp)=>->libnvram.so.0::nvram_safe_set  = 004b2f80
0043a830   nop
0043a834   move       t9,v0
0043a838   jalr       t9=>libnvram.so.0::nvram_safe_set                undefined nvram_safe_set()
Disassembly of the second portion of the vulnerability in SetNetworkSettings

prog.cgi SetNetworkSettings (2/2)

The first parameter being lan0_management_link, it safe to assume that the previously retrieved DeviceName is stored in nvram without applying any filter.

Note that to reach this second portion of the code, all needed values (suchsh as LocalDomainName) must be present.

Decompiled code showing the SetNetworkSettings vulnerability

prog.cgi SetNetworkSettings decompiled code

It is worth noting that intra process communication is done through NVRAM. The prog.cgi HTTP cgi handler communicates to the rc service by using the libnotifyrc library to abstract part of this communication.

To trigger the exploit on the rc service (which will be analyzed next), the start_vlanwanall notification must be sent to the rc service. It is possible to do so, by calling the function SetVLANSettings.

In a similar fashion to the previous analyzed function, the SetVLANSettings located at 0x0046d180 was linked to a HTTP request using the websFormDefine function, this time in the InitModuleInternet function:

Decompiled code of ModuleInitInternet function, showing how the SetVLANSettings function is hooked

prog.cgi ModuleInitInternet

At the end of the SetVLANSettings function (located at 0x0046f2f0 in .text) it was possible to see that a call to the wrapper serviceApplyAction function was made:

Code showing how prog.cgi communicates with rc

prog.cgi SetVLANSettings epilog

Looking inside this function, it was possible to find that it is just a wrapper for the libnotifyrc functions notify_rc and notify_rc_and_wait:

Code of the serviceApplyAction

prog.cgi serviceApplyAction

Thus, triggering the exploit is possible by sending a valid SOAP request to SetVLANSettings enabling the VLAN. This function will notify the rc service and trigger the exploit.

Another interesting detail regarding the serviceApplyAction function, is that after calling the libnotifyrc functions, it will call kill(1,1);, which would send SIGHUP to PID 1 (aka init). This obviously hints at init (aka PID 1) being the rc service.

To confirm this thesis, the sbin/init file was checked:

$ ls -lah rootfs/sbin/init 
lrwxrwxrwx 1 user user 14 jul 23 10:23 rootfs/sbin/init -> ../bin/busybox

Sadly, this linked to busybox. Fortunately, kernel command-line parameters may still specify a different init binary. To locate the kernel command-line, the root= string was searched through all extracted binaries:

$ rg --binary 'root=' DIR_878_FW120B05.BIN_extract/
DIR_878_FW120B05.BIN_extract/0-11183836.shrs_extract/0-11183836.shrs.decrypted_extract/160-11181231.lzma_extract/lzma.uncompressed_extract/6370037-9000924.unknown: binary file matches (found "\0" byte around offset 8)

DIR_878_FW120B05.BIN_extract/0-11183836.shrs_extract/0-11183836.shrs.decrypted_extract/160-11181231.lzma_extract/lzma.uncompressed_extract/9000924-16548583.lzma_extract/lzma.uncompressed: binary file matches (found "\0" byte around offset 113)

DIR_878_FW120B05.BIN_extract/0-11183836.shrs_extract/0-11183836.shrs.decrypted_extract/160-11181231.lzma_extract/lzma.uncompressed: binary file matches (found "\0" byte around offset 0)

The first match was analyzed:

$ cp 'DIR_878_FW120B05.BIN_extract/0-11183836.shrs_extract/0-11183836.shrs.decrypted_extract/160-11181231.lzma_extract/lzma.uncompressed_extract/6370037-9000924.unknown' target
$ strings target | rg 'root='
3Disabling rootwait; root= is invalid.
Please append a correct "root=" boot option; here are the available partitions:
console=ttyS1,57600n8 root=/dev/ram0 rdinit=/sbin/preinit
root=

The kernel command-line was identified as the following:

console=ttyS1,57600n8 root=/dev/ram0 rdinit=/sbin/preinit

Looking at the sbin/preinit file it was confirmed that rc was not only the ‘main’ system configuration service, but as the name might have suggested it was also the Run Command aka init:

$ file rootfs/sbin/preinit 
rootfs/sbin/preinit: symbolic link to ../bin/rc

Binary 2: rc

Sha256sum

028e512cebf4ce97d80c7ba9045b42d1a508c08b845610195a1c9e38189fca51  rc

The target rc file was located:

$ find rootfs/ -name "rc"
rootfs/bin/rc
$ file rootfs/bin/rc
rootfs/bin/rc: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped

Then, references to the vulnerable functions (according to the writeup), where searched:

$ readelf -a rootfs/bin/rc | rg twsystem
   223: 0046bb00     0 FUNC    GLOBAL DEFAULT  UND twsystem_nowait
   356: 0046b300     0 FUNC    GLOBAL DEFAULT  UND twsystem
  0048a5a8 -31912(gp) 0046bb00 0046bb00 FUNC    UND twsystem_nowait
  0048a7bc -31380(gp) 0046b300 0046b300 FUNC    UND twsystem
$ strings rootfs/bin/rc | rg 'hostname %s'
hostname %s
Terminal showing the output of the initial analysis of rc

Initial analysis of the rc binary

The target rc file was loaded in Ghidra, making sure that external symbols would be resolved (see notes on binary above). The file was opened in CodeBrowser and the default analysis was run. The binary contained symbols:

Symbol Tree of rc, showing many symbols

rc Symbol Tree

It was confirmed that the start_dev_mgt_link (0x0042ff24) gets called by start_lan_up (0x004106d8) which in turns is called by handle_notifications (0x004510ec):

handle_notification calling start_lan_up

handle_notification calling start_lan_up

It was possible to note that this functions triggers only on start_vlanwanall (line 94).

start_lan_up calling start_dev_mgt_link

start_lan_up calling strat_dev_mgt_link

In the vulnerable function start_dev_mgt_link it was possible to see that the lan0_management_link string was composed using fprintf and a custom strcat function. Then the nvram_safe_get function is used to retrieve the user-controlled payload discussed previously:

Vulnerable code part 1 of 2

rc vulnerable code in start_dev_mgt_link (1/2)

Custom function used to concatenate two strings

rc custom strcat

Looking at the disassembly, it was possible to see that after the nvram_safe_get function call at 0x004300a4, the resulting user-controlled payload was stored on the stack in local_120:

004300a4   jalr       t9=>libnvram.so.0::nvram_safe_get                undefined nvram_safe_get()
004300a8   _nop
004300ac   lw         gp,local_128(sp)
004300b0   sw         v0,local_120(sp)
004300b4   lw         v0,local_120(sp)
Disassembly of the first portion of the vulnerable code

rc start_dev_mgt_link disassembly (1/2)

The actual vulnerability is found at the end of the start_dev_mgt_link function at address 0x00430608, where the user-controlled payload is directly concatenated to a command string without any filtering or checking of the input. Then, the string is used to call system via the twsystem function:

Vulnerable code part 2 of 2

rc vulnerable code in start_dev_mgt_link (2/2)

Looking at the disassembly, it was possible to see that before calling the snprintf function to construct the twsystem parameter, the vulnerable function loaded the user-controlled local_120 as third parameter:

004305ec   move       a0,v1
004305f0   li         a1,0x80
004305f4   move       a2=>s_hostname_%s_00471fc0,v0                    = "hostname %s"
004305f8   lw         a3,local_120(sp)
004305fc   lw         v0,-0x7bc8(gp)=>->libc.so.0::snprintf            = 0046b7b0
00430600   nop
00430604   move       t9,v0
00430608   jalr       t9=>libc.so.0::snprintf                          int snprintf(char * __s, size_t 
Disassembly of the second portion of the vulnerable code

rc start_dev_mgt_link disassembly (2/2)

Summary

The vulnerable D-Link firmware exposed a way for an attacker to execute commands as init (PID 1). An attacker being able to reconfigure the device can achieve Remote Command Execution. The exploit relies on the lack of input validation by the prog.cgi CGI daemon and by the rc init service.

The prog.cgi CGI HTTP handler is used first to inject a malicious payload in the NVRAM of the device, more specifically in the lan0_management_link key via the SetNetworkSettings HTTP SOAP request. In this malicious request, the DeviceName can be used to inject commands as PID 1 (rc).

After the initial setup, it is possible to use the SetVLANSettings HTTP SOAP request enabling the VLAN (start_vlanwanall). This will cause the prog.cgi to send a notification to the rc service and trigger the exploit.

Once the notification is reaches the rc daemon, it is handled by the handle_notification function. In case of start_vlanwanall command, this function will call the vulnerable start_dev_mgt_link function, which will concatenate the user string stored in the lan0_management_link NVRAM variable to a command and feed it into twsystem(), which will be happy to execute the user-provided commands.

To mitigate this vulnerability, proper input validation must be put in place.

Conclusion

What at first tought seemed like a trivial two stage command injection, revealed to be much more under the hood.

It was super fun reversing this simple C binary. Tomorrow I should be working on another vulnerability on the same firmware. Thus, if I am lucky it might be possible that I will post two days in a row >:B .

That being said, it is also possible that we could at some point buy the device to try and develop some PoC exploits.