In the first part of this write-up, we unpacked the firmwares, looked for interesting differences, found a vulnerability and validated it's presence with a PoC-POST-Request. If you haven't reddit yet, I suggest you to do so to better understand what we are doing here.
Table of Contents
Static Analysis
Unfortunately/Fortunately, the last bug we found can only be exploited if you include a valid session id into the request - not exactly brilliant. But during binary-diffing of the firmware, we found another RE-candidate - webcm
to call it by name. So, let's fire up IDA Pro, load the old version, patchdiff2
it and see, what we get:
This looks pretty similiar and promising: a lot of identical functions, no import of system
anymore, but imported functions for Inter-process-communication, that were also used to patch the first vulnerability. Let's again take a look at the matched, but not-identical function to find the juicy part:
Okay, whatever this is, it's slighty more complicated than the function we've seen before, that was only responsible for updating /var/temp_lang
and informing ctlmgr
.
First, let's see, if it's worthwhile to look at it in more detail:
Again, an additional strlen(something) <= 3
check
and the same "something" is checked byte by byte, if it's less than 0x1A after a subtraction of 0x61. In case you don't speak ascii:
>>> for i in range(0x61, 0x61+0x1A): ... print chr(i), ... a b c d e f g h i j k l m n o p q r s t u v w x y z
These are again pretty much the same checks as for the language parameter seen in part 1. And finally, the vulnerable code block:
Oh, déjà vu! Let's put that function in a context to get a grasp of it's actual function and to extract the path to the the unsafe system()
-call:
And here, the pretty straightforward parse_cgivars
-function is called during _ftext
:
So, this time we won't be able to conveniently "switch" to Lua code.
To be more pecise:
A Server Side Includes (SSI) Interpreter by Texas Instrument is created by a call to TI_Interpreter_Create
(from libtiinterpreter.so
) in the main function (_ftext
), whose variables are filled (TI_Interpreter_SetVariable
) during all this cgi-parsing-stuff and which is finally run and destroyed previous to _ftext
's function epilogue.
Backtracking the path to the unsafe code shouldn't be that hard, right?
Well, even though it's pretty easy to reach the changedFunc
(at 0x4028B8), it's at least not trivial to understand the control flow inside this function.
There is a pretty big if() {...} else if() {...} else if() {...} ...
respectively switch-case
-statement right in the middle of the function, that string-compares one parameter to another function parameter and to a series of memory locations, that are referenced by different registers.
Since the first "comparee" is set in parse_cgivars
("soapdata") and you can find the other interesting register values in the next screenshot (can you spot 'em?), this big chunk of code is rather comprehensible.
However, we are presented with something much smaller and more tricky right in the beginning:
The function takes two completely different paths, depending on the initial value of 0x5620($s7) => 0x415620
. Furthermore, it's possible to reach this decision again after taking the left path - but what is stored at 0x415620 at this point?
Static Analysis is cool and all, but some runtime information would make our life so much easier - but wait, that's in fact possible. No, I won't install gdb on my Fritz!Box; and we don't even need the actual device!
I want to show you, how to dynamically analyze ("debug") embedded systems without the necessity of possessing them by taking advantage of qemu
's gdb
integration and IDA Pro
's remote debugging features! :)
Dynamic Analysis - Remote Debugging
I hope, you enjoy ascii-graphics as much as I do:
+––––––––––––––––––––––––––––––––––––––––––––––––––––––––+ | | | Linux | | | | | | +––––––––––––––––––––––––––––––––––––––––––+ | | | | | | | Virtual Box | | | | | | | | +–––––––––––––––––––––––––––––––––––––+ | | | | Windows | | | | | | | | | | +––––––––––––––––––––––––––––––+ | | | | | | | | | | | IDA Pro | | | | | | | | | | | | | | | +––––+––––––+–––––––––––––––––––––––––+––––+ | | | | | | | | | | | +––––––––––––––––––––––––––––––––––––––––––+ | | | \|/ | | | | chroot in firmware-filesystem-/ +––+––+ | | | | |12345| | | | | +–––––––––––––––––––––––––––––+––+––+–+ | | | | | | | | | | qemu-mipsel -g 12345 webcm | | | | | | \|/ | | | | | +––––––––––––––––––––––––––––+––––+ | | | | | | | | | | | webcm-binary | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | +––––+–––+–––––––––––––––––––––––––––––––––+ | | | +––––––––––––––––––––––––––––––––––––––––––––––––––––––––+
Before you instantly grab the latest pre-built QEMU
-version (the "generic and open source machine emulator and virtualizer" we'll be using) from your distro's repository, you should keep in mind, that our MIPS binaries are dynamically linked to MIPS libraries. Since we want to prevent mixing the target's libraries with those on our host system, we have to run QEMU in a chrooted environment. However, this makes our host's libraries inaccessible to QEMU itself, thus requires QEMU to be statically linked. You could either search for static builds of QEMU in your distro's repository or build it from source (that's what I will do here).
To ease your building process, here is a list of dependencies I had to install manually:
fabian@7a69:~$ sudo apt-get install zlib1g-dev libglib2.0 autoconf libtool libsdl-console libsdl-console-dev
Download and extract the sources and build your statically linked version of QEMU:
fabian@7a69:~/qemu$ ./configure --static && make && sudo make install
[This will take quite long. If you want to speed up the building process and limit the qemu installation to only support mips, you can specify this with the --target-list=
-parameter.]
So, let's test the installation. Copy the corresponding QEMU binary into the extracted filesystem-directory and run an example binary. The file
-command in part I told us, that we are dealing with "MSB" and therefore Big Endianess.
fabian@7a69:~/blog/Projekte/Fritz!Box/unpatched/original/filesystem$ cp $(which qemu-mips) . fabian@7a69:~/blog/Projekte/Fritz!Box/unpatched/original/filesystem$ sudo chroot . ./qemu-mips ./bin/busybox qemu: uncaught target signal 4 (Illegal instruction) - core dumped
Damnit, let's try another the other Big-Endian-MIPS-QEMU-Executable:
fabian@7a69:~/blog/Projekte/Fritz!Box/unpatched/original/filesystem$ sudo chroot . ./qemu-mipsn32 ./bin/busybox qemu: Unsupported syscall: 4090 qemu: Unsupported syscall: 4090 qemu: Unsupported syscall: 4004 qemu: Unsupported syscall: 4001 qemu: uncaught target signal 11 (Segmentation fault) - core dumped
This doesn't look good, either. A quick search reveals, that the FRITZ!Box Fon WLAN 7360
runs on a VR9
-SoC (aka VRX288, PSB80920, PSB80920EL or PSB80920EL V1.1) that contains a 32-bit MIPS 34Kc CPU with 500MHz. However, it seems, that qemu doesn't support this cpu at the moment.
fabian@7a69:~/blog/Projekte/Fritz!Box/unpatched/original/filesystem$ ./qemu-mips -cpu help MIPS '4Kc' MIPS '4Km' MIPS '4KEcR1' MIPS '4KEmR1' MIPS '4KEc' MIPS '4KEm' MIPS '24Kc' MIPS '24Kf' MIPS '34Kf' MIPS '74Kf' fabian@7a69:~/blog/Projekte/Fritz!Box/unpatched/original/filesystem$ ./qemu-mipsn32 -cpu help MIPS '4Kc' MIPS '4Km' MIPS '4KEcR1' MIPS '4KEmR1' MIPS '4KEc' MIPS '4KEm' MIPS '24Kc' MIPS '24Kf' MIPS '34Kf' MIPS '74Kf' MIPS 'R4000' MIPS 'VR5432' MIPS '5Kc' MIPS '5Kf' MIPS '20Kc' MIPS 'MIPS64R2-generic' MIPS 'Loongson-2E' MIPS 'Loongson-2F' MIPS 'mips64dspr2'
Since we (respectively I) don't have access to another unpatched firmware version for a device with a supported cpu and I really want to analyze the binaries dynamically, we must find a workaround. Although, it's not absolutely necessary in this case, a working debugging environment will come in handy during future investigations. Furthermore, the setup is pretty dope imho, so I wanted to document the required steps.
We are already aware of the added security checks and we know from part 1, that can run arbitrary commands as early as the system()
-call is reached. This allows us to simply grab a fixed firmware for a supported device from the FTP server and circumvent the checks on the fly.
fabian@7a69:~/fritz.box/7240_5_54/original/filesystem$ sudo chroot . ./qemu-mipsel ./bin/busybox BusyBox v1.19.3 (2012-05-21 13:40:41 CEST) multi-call binary. Copyright (C) 1998-2011 Erik Andersen, Rob Landley, Denys Vlasenko and others. Licensed under GPLv2. See source distribution for full notice. Usage: busybox [function] [arguments]... or: busybox --list[-full] or: function [arguments]... BusyBox is a multi-call binary that combines many common Unix utilities into a single executable. Most people will create a link to busybox for each function they wish to use and BusyBox will act like whatever it was invoked as. Currently defined functions: [, [[, arp, arping, ash, basename, brctl, bunzip2, bzcat, bzip2, cat, chgrp, chmod, chown, chroot, cmp, cp, cut, date, dd, df, dirname, dmesg, dnsdomainname, du, echo, egrep, env, ether-wake, expr, false, fgconsole, fgrep, find, flock, free, ftpget, ftpput, getopt, grep, groups, gunzip, gzip, halt, hostname, id, ifconfig, ifdown, ifup, inetd, init, insmod, iostat, ip, ipaddr, iplink, iproute, iprule, iptunnel, kill, killall, killall5, ln, login, logname, ls, lsmod, md5sum, mkdir, mkfifo, mknod, mkswap, modprobe, more, mount, mpstat, mv, nbd-client, nc, netstat, nice, nohup, nslookup, passwd, pidof, ping, ping6, pivot_root, pmap, poweroff, printenv, printf, ps, pstree, pwd, pwdx, readlink, realpath, reboot, renice, reset, rm, rmdir, rmmod, route, sed, seq, setconsole, setserial, sh, sleep, smemcap, sort, stat, stty, swapoff, swapon, switch_root, sync, sysctl, tail, tar, tee, telnetd, test, tftp, time, top, touch, tr, traceroute, true, tty, ubimkvol, ubirmvol, ubirsvol, ubiupdatevol, umount, uname, uniq, unxz, unzip, uptime, vi, wc, wget, whois, xargs, xz, xzcat, zcat
Well, this looks promising.
The webcm
-binary uses the Common Gateway Interface and therefore should take the request parameters as environment variables (and stdin). A quick look at the referenced strings confirms this.
Of special interest are the REQUEST_METHOD
and QUERY_STRING
environment variables.
fabian@7a69:~/fritz.box/7240_5_54/original/filesystem$ ./qemu-mipsel usage: qemu-mipsel [options] program [arguments...] Linux CPU emulator (compiled for mipsel emulation) Options and associated environment variables: Argument Env-variable Description -h print this help -g port QEMU_GDB wait gdb connection to 'port' [...] -E var=value QEMU_SET_ENV sets targets environment variable (see below) [...]
We can conveniently set them with the -E
-parameter. Moreover, the help shows us another neat feature of QEMU. We can start a gdb remote stub on a given port with the -g
option. Later, this will allow us to attach IDA Pro to the process.
So, let's write a simple script to start everything with the correct parameters:
#!/bin/bash PORT="12345" INPUT="$1" if [ "$UID" != "0" ] || [ "$INPUT" == "" ] || [ "$INPUT" == "-h" ] then echo -e "\nUsage: sudo $0 \n" exit 1 fi cp $(which qemu-mipsel) ./qemu-mipsel chroot . ./qemu-mipsel -E REMOTE_ADDR="127.0.0.1" -E QUERY_STRING="$INPUT" -E REQUEST_METHOD="GET" -g $PORT ./usr/www/cgi-bin/webcm rm -f ./qemu-mipsel
Let's start the script with example-parameters (e.g. sudo ./run.sh "foo=bar"
) and load the same binary into IDA. QEMU will pause execution until we attach a debugger. For this purpose, we need to configure IDA to connect to the remote GDB server. Go to Debugger -> Process options
and fill in the IP address of the machine where you ran Qemu, together with the correct port number.
We will set a break point (F2
) at the beginning of _ftext
, start the debugger (F9
), ignore the warning and step through the binary (F4
/F7
/F8
).
You will notice, that - without interception - it would just output
Content-type: text/html
Internal socket error.
because cgi_comm_init
fails due to our emulated environment.
Therefore, set a breakpoint at
la $t9, unk_76728B6C jalr $t9 ; cgi_comm_init la $a0, aWebcm beqz $v0, loc_40196C ; <-- bp here (.text:00401920)
and increment v0
(Rightclick or press +
on the numblock) once reached.
We finally end up at
.text:00401A38 bal parse_cgivars
,
take the short path ("soapdata" is not keyvalue_found
) and land in our changed_Func
.
Following resembles the situation, when the first if-else" is hit:
Unfortunately, the binary does not go straight to the system call, but takes the left path to the big switch-case statement.
However, it seems, that we can't influence this, so let's see, what happens next.
Our variable name ("foo") is subsequently compared to a few strings
until we finally reach
.text:00402BF0 la $a1, aVar # "var:" .text:00402BF8 jalr $t9 # safe_strncmp .text:00402BFC li $a2, 4 .text:00402C00 bnez $v0, loc_402CF0
which checks, if the first 4 bytes of our parameter name is equal to var:
. If it's not, we won't come far. So either change it to "var:foo" on the fly or restart the process and attach IDA again.
Our parameter name is then compared to "var:lang", which of course fails.
Nevertheless, the function NameWithoutVarPrefix
is called, returns "var:foo"[4:] == "foo" (supposing that the string really starts with "var:") and _TI_Interpreter_SetVariable("foo", "bar")
is called. (A quick reminder: TI_Interpreter is the SSI-Interpreter from Texas Instruments)
We reach the decision in the beginning again and fortunately, this time 0(0x4159F0) in the .bss
-section is set to zero, so we take the other path.
(An exception will be thrown while the application tries to open "/var/temp_lang" in our chroot, so change the permissions of this file accordingly.)
The file /var/temp_lang
will be unlinked/deleted and the 127 byte big buffer used for the system-call (in the unpatched version) is filled by making use of the original content of the file:
Damnit! However, you still remember the comparison to "var:lang", that failed? Let's make it succeed and see, what happens. (Note the properly URL-encoded QUERY_STRING
)
fabian@7a69:~/fritz.box/7240_5_54/original/filesystem$ sudo ./run.sh "var%3Alang%3Dbar%3B%20.%2Fexecute%20something%20vicious"
This time, we reach the added security checks, but can easily circumvent them on the fly (breakpoints at 0x00402C4C
and 0x00402C70
to redirect the conditional jumps):
This time, we will reach again the NameWithoutVarPrefix
chunk, however, before this, the var_lang_set
-flag is set:
As previously seen, the function now passes through the the other path. Here, the content of /var/temp_lang
is compared to the newly entered value (first by a strlen
-comparison and in case of equality by strcmp
) and since we did not enter an empty string, the new language-setting is stored in this file. After that - you may already know - the ctlmgr is notified of this event by the usual unsafe snprintf
- system()
combination! Yiiiiihu, finally!
Please excuse my choice of words, but we have finally found a easy and cool way to compromise the router with a simple GET-Request: Pwned! :)
But only if the router runs on an unpatched firmware.
In the firmware revision, the arguments are split into an array and the unsafe system("msgsend ctlmgr temp_lang_changed USER_INPUT)
-call is replaced by the safe execvp("ctlmgr", ["ctlmgr", "temp_lang_changed", "USER_INPUT"])
equivalent (which executes the binary itself and does not spawn a shell, hence eliminates this attack vector) - there is no additional Inter-process communication implementation on this device, however.
The Exploit
With this in mind, it's rather easy to tinker a Metasploit module, since we don't have to mess with different offsets on different devices, memory layout, etc.
The Requirements:
- It should give direct access to metasploit's payload armory (e.g. rerverse_shell and bind_shell access)
- It should allow custom commands
- It should allow us to compromise the router directly (in the case we are already connected to the network)
- It should support generation of HTML, that contains the malicious GET-Request and is directly hosted on a web-server, so we can simply send this link to the victim
There is a great blog post from exactly one year ago on the metasploit blog, that introduces the possibility to craft MIPS executables and shows an example on how to serve it to the router.
I made use of this, customized it where needed and came up with this PoC-Module.
A much cleaner version (even though it's not as feature-rich), written mostly by m-1-k-3, is already merged into Metasploit's master branch and is available at exploit/linux/http/fritzbox_echo_exec.
You can see them in action at the end of this article.
(Note, that I won't go into detail about this topic, since I want so save it up for a more interesting case)
In case, you don't have Metasploit installed/don't want to boot into Kali to quickly test your router, here is a "testpage". If you click on this link, you agree, that I may execute an echo
-command on your router with all it's possible side effects:
Please change "fritz.box" to your device's IP-address in case you're using a strange setup and wait a few seconds for a possible result.
Since the command's output is again sent back in the HTTP response header, you will see following warning in case of exposure to this vulnerability:
And the full repsonse (intercepted via Burp Suite):
Conclusion
Firstly, if you haven't updated your Fritz!Box yet, because you are not using the remote administration anyway and "never change a running system", you should be finally convinced now. ;)
I hope, I could give you an understanding on some reverse engineering techniques (particularly (dynamic) analysis of embedded devices), Fritz!Box internals and especially on this vulnerability (fight the FUD!).
Although this write-up was rather extensive, we've seen how easy it is to spot the interesting fixes and backtrack the path. Again, we've recognized the importance of updating all devices in the household. Embedded devices in general and routers in particular are an interesting target for both security researches and criminals, since they are often
- unpatched (if a patch exists at all)
- as full of holes as Gruyere
- accessible from the internet
- and very powerful (DNS-Settings, DSL-Credentials, Port Forwarding).
Furthermore, if you are still here, I want to thank you for taking the time to read through all of this. I know, this took quite a while to go through, however, it took even longer to write, trust me. ;)
I would also like to hear some feedback, corrections or comments either in the comments-section, via mail or on /r/netsec.
Cheers!
Acknowledgements
- Michael Messner (http://www.s3cur1ty.de/) for simplifying the Metasploit PoC a lot and submitting the PR
- Felix Wilhelm from insinuator for providing the unpatched firmware sample and his inital analysis
- /dev/ttys0 for their great articles on Exploiting Embedded Devices
Last but not least, here are some further impressions of this project - it may not have ended yet. ;)