Vuln research on the WAG54G home router

Intro to bug hunting on an embedded device


To avoid the typical intro preamble, let’s dive straight into the juicy bits of doing this research.

While we already had command line access to the WAG54G available through the serial console, we chose to find another way of gaining remote access to the device for some added flavor. Having console access is an important first step in understanding how the device works and beginning analysis. The WAG54G router comes with an option to enable remote management via Telnet, and can be enabled using its web interface:

WAG54G circuit board

Unfortunately the default shell is restricted and only exposes typical “admin” commands that are available through the routers web interface. This means we lose the ability to explore the underlying Linux OS and filesystem, and basically looks something like this:

WAG54G default configurator shell

Well that’s just not good enough. One option is to exploit a vulnerability in the shells command line parsing, which is a promising vector and quickly revealed trivial bugs like the following:

WAG54G configurator 'upgrade' command information disclosure

However it still feels a little dirty even having to see the custom shell prompt, surely there’s a better way of breaking out? The bug above is nice enough to show us the admin users shell is /bin/configurator. What if we can update that entry to point to /bin/sh?

Fortunately, Protip #1 of router hacking dictates: When thou needeth a shell, pop a command injection into the “diagnostic ping” page. Rumor has it that 80% of the time, it works every time.

WAG54G diagnostics ping test WAG54G ping_interval command injection payload

Voilà! The command injection in ping_interval overwrites /etc/passwd with a new entry for admin, and successfully breaks out of the restricted shell. The next time we try authenticating over Telnet, we’re greeted with a friendly busybox prompt:

Busybox shell

So far so good, but this hasn’t technically advanced any further than our previous blog post. The next steps are where things get better, we use the WAG54G devices tftp client to upload binaries and library dependencies onto our laptop for offline analysis.

tftp client uploading binaries onto laptop

Static analysis

Now we’re finally ready to begin the real adventures. To get a lay of the land, we ran the file command against the binaries to see how they’re linked:

mbp-wifi:Desktop daniel$ file ./httpd
./httpd: ELF 32-bit LSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked (uses shared libs), stripped

Learning that our target is a dynamically linked (and stripped) ELF executable compiled for the 32-bit MIPS little endian architecture. Having a copy of the MIPS instruction set documentation handy for reference, an iced-tea, and a copy of IDA Pro - we’ve got everything we need to begin hacking.

Bug #1

This vulnerability is not going to stop the press, but it was a personal moment of celebration. It’s significance shows that we understand enough about the MIPS instruction set to successfully find bugs!

The following screenshot shows a fragment of code which parses an HTTP request, attempting to break apart the request method from the URI by calling the strsep() libc function:

The problem is strsep() can set *curptr to NULL if the end of the source string is reached and no tokens are found. This condition is not tested for in the code, and as a result the dereference at 0x00407898 causes an access violation reading unmapped memory.

The following commands can be used to verify the issue (note the lack of whitespaces):

python -c 'print "GET" + "A" * 200 + "\n"' | nc 80

Which results in the following segmentation fault:

Bug #2

The next bug we came across was slightly more interesting. The code attempts to parse the Host: header while copying its value into the re_ip_des global char array. The problem is the use of strcpy() which performs an unbounded copy using an attacker supplied string, and the destination is of a fixed-length (~64 byte) buffer. By sending a large value in the header, an attacker can exploit this to overwrite other global variables on the heap - a construct which might be useful for privesc:

The following commands can be used to verify the issue:

python -c 'print "GET /index.asp HTTP/1.0\nHost: " + "B" * 9000 + "\n\n"' | nc localhost 80

In which the entry_config_buf variable is after re_ip_des and is overwritten with “BBBB”:

Bug #3

The final bug we came across is potentially the most interesting, however it’s triggered post-auth so would need to be combined with another vulnerability to be useful.

The issue itself is a result of calling fread() into a fixed-size global buffer, but using an unbounded Content-Length size which has been provided by the remote user:

What is nice about this construct is global_post_buffer is 10,000 bytes in size, and is directly followed by function pointers for handling mime types and other goodies. This makes exploitation relatively straight forward:

The following commands can be used to verify the issue:

python -c 'print "POST /apply.cgi HTTP/1.0\nAuthorization: Basic YWRtaW46YWRtaW4=\nContent-Length: 20000\n\n" + "A" * 20000' | nc localhost 80

NB: The auth token is required in this case, as POST requests are only handled after a connection has been authenticated.

Runtime analysis

To ensure that our understanding of MIPS assembly was correct, it was necessary to perform runtime testing on the binaries and ensure our triggers lead to a crash. While this activity could have been done against the WAG54G device itself, we chose to use an emulated environment with full control over tools and debuggers. We achieved this by using QEMU.


A great stack exchange post can be found here which details much of the needed steps to get ready. On our OSX laptop with the brew package manager, these are the commands that were used:

brew install qemu
mkdir linksys54g
cd linksys54g
curl -O -O
qemu-system-mipsel -M malta -kernel vmlinux-3.2.0-4-4kc-malta -had debian_wheezy_mipsel_standard.qcow2 -append "root=/dev/sda1 console=tty0" -m 512M

Once the machine has booted we can update it and install our binaries:

apt-get update
apt-get install gcc gdb vim screen curl
curl -O # binaries/shared libraries etc
tar -zxvf ./files.tar.gz
cp *.so /lib

The following screenshot shows what it looks like running the httpd in our emulated environment:


Although we are successfully running the executables in our own environment, several errors related to /dev/nvram access are causing “401 Unauthorized” errors. This may limit the amount of code we are able to exercise.

To resolve this issue, we decided to perform LD_PRELOAD hooking to intercept requests to the nvram device and send custom responses back. The hooks used an export of the WAG54G routers configuration, which was decoded with the tool here. The following Vim macro was used to convert the tools output into a C like structure, representing the devices configuration as key-value pairs:


The final version of the nvram hooking code can be downloaded here.

Re-executing the daemon with the hook shows no further errors, indicating we have successfully emulated the runtime found on the physical device.


Using the above steps, we have successfully started the process of vulnerability discovery and proof of concept development against the router. In a future blog post, we hope to explore ways in which these vulnerabilities can be exploited to enable flashing C&C firmware onto the device pre-auth.

If you’re wondering about the disclosure timeline for these bugs, long story short the WAG54G has reached end-of-life support. If you’re an internet hero, you can always release your own updates! or choose to flash your router with the great OpenWRT firmware.