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:
Searching for the (supposed) magic string SHRS
yielded positive results:
It was possible to extract 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
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:
The file was loaded 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:
Thereafter, the prog.cgi
was opened in the Ghidra CodeBrowser
and analyzed with standard options. The resulting Symbol Tree
contained many symbols:
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
:
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)
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()
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.
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:
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:
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
:
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
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:
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
):
It was possible to note that this functions triggers only on start_vlanwanall
(line 94).
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:
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)
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:
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
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.