Tuesday, 22 September 2015

CSAW CTF 2015 - Web 200 in two steps (using PHP's awfulness)

Some friends and I participated in this year's CSAW CTF under the name "Northwest Beer Drinkers". We placed 89th out of a total of 1,100+ teams, so I guess we can boast about being in the top-100 this year (woo). Sadly I couldn't participate myself too much this year as I had a family gathering to attend to, but I did spend some time early on and managed to solve Web 200.

Web 200, or "Lawn Care Simulator", was a simple web application that plays on the joke about "growth hackers". It was written to look like it was PHP-based, complete with a login page, registration form, and a suggestion that you join "their company". It definitely was quite tongue in cheek and they even went out of their way to make the grass grow in the blue square if you pressed the "grow" button.

When you attempt to login, it hashes the password field before sending off the form. This is done via an embedded JavaScript that makes use of the MD5 function in the CryptoJS library. The code executes as follows:

function init(){
            document.getElementById('login_form').onsubmit = function() {
                var pass_field = document.getElementById('password'); 
                pass_field.value = CryptoJS.MD5(pass_field.value).toString(CryptoJS.enc.Hex);

The registration page also refuses to let you sign up for an account, citing that it is currently in "private beta". It tries to play a trick on you in the form that it's looking for a hash value from the initial page, supplied by an improperly placed Git repository (which is important to note for later in this writeup), but I couldn't find a way to make use of this hash in the form, so I decided to attack the login mechanism instead since it was doing some weird hashing before sending the form off.

And this is where we sort of quickly solve the problem.

I decided to see what would happen if I just logged in with no credentials at all using Python Requests. The intention here was to see what sort of error would be produced and then use that to solve the problem. However, it sort of went sideways...
>>> import requests
>>> data = { 'username': '', 'password': '' }
>>> r = requests.post('', data=data)
>>> r.text
u'<html>\n<head>\n    <title>Lawn Care Simulator 2015</title>\n    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>\n    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> \n    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet"></link>\n</head>\n<body>\n<h1>

As we can see, this worked. At the time when the flag was revealed, I was not 100% certain that this was an intentional way to get the flag but while reviewing my notes and other information, I became conflicted.

If we go back to my remark about the Git repository, it was the repo for the entire source code to this challenge. And as it turns out in another write-up, it was key in solving the challenge for that person.

I did grab the source code from the challenge and took a look at why I was successful with fewer steps.
    require_once 'validate_pass.php';
    require_once 'flag.php';
    if (isset($_POST['password']) && isset($_POST['username'])) {
        $auth = validate($_POST['username'], $_POST['password']); 
        if ($auth){
            echo "<h1>" . $flag . "</h1>";
        else {
            echo "<h1>Not Authorized</h1>";
    else {
        echo "<h1>You must supply a username and password</h1>";
So now we know it uses validate function from validate_pass.php, so let's examine that to see why this worked.

function validate($user, $pass) {
    require_once 'db.php';
    $link = mysql_connect($DB_HOST, $SQL_USER, $SQL_PASSWORD) or die('Could not connect: ' . mysql_error());
    mysql_select_db('users') or die("Mysql error");
    $user = mysql_real_escape_string($user);
    $query = "SELECT hash FROM users WHERE username='$user';";
    $result = mysql_query($query) or die('Query failed: ' . mysql_error());
    $line = mysql_fetch_row($result, MYSQL_ASSOC);
    $hash = $line['hash'];

    if (strlen($pass) != strlen($hash))
        return False;

    $index = 0;
        if ($pass[$index] != $hash[$index])
            return false;
        # Protect against brute force attacks
    return true;
While my PHP is not up to snuff, it should at least from what I understand here have came up with no results unless the database itself had a row that was completely blank for both the hash and username field. If the result variable was not able to be created then it should have died outright, but it was able to fetch a row and feed it into the line variable.

In any event, the solution was not to ram it with a bunch of requests but to just post a blank username and blank password. Whether or not this is the official solution I am not 100% sure.

Edit: PHP is an awful language

I had a chat with a friend of mine (pr0zac) who knows PHP better than me and he pointed that that mysql_fetch_row returns "false" if no rows are found. What likely happened here is that SQL failed to return any rows but the query technically succeeded so mysql_query returned successfully.

Then mysql_fetch_row returned "false" when it executed and then strlen reading of that made the result "null".

After all that, it then just passes the check as it remains "null" after hashing and returns "true".

PHP is fucking awful.

1 comment:

  1. Hi,

    The method I used:
    - Find out that there was a /.git/ folder
    - Dump it, analyze the source code. You realize two things:
    - First the subscribe page check if the given user exists with "%user%". By putting "A" as username, the site reply that the user "~~FLAG~~" exists (so we found the user we have to take control of).
    - Second and last part, we can see in the login page source code that the password is checked letter by letter, and between each check, there is a sleep of 0.3 seconds. So we can make a time based attack to get the password of ~~FLAG~~.