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 Magic signature at the start of the firmwarehexdump tool was used to check the start of the file:
Searching for the (supposed) magic string Decrypting SHRS D-Link firmwareSHRS yielded positive results:
It was possible to extract 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

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:

lighttpd.conf HTTP CGI configuration
The file was loaded 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:

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:

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:

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)

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()

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.

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:

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:

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:

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

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:

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
It was possible to note that this functions triggers only on start_vlanwanall (line 94).

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:

rc vulnerable code in start_dev_mgt_link (1/2)

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)

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:

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

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.