Information#
CTF#
- Name : CONFidence CTF 2019 Teaser
- Website : confidence2019.p4.team
- Type : Online
- Format : Jeopardy
- CTF Time : link
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?
1 |
|
Ok let's give them what is asked and get a hint:
1 | $ curl -v https://gameserver.zajebistyc.tf/admin/login.php -H 'Cookie: otadmin={"hash": "0A"}' |
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 | 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" |
This make a lots of possibilities even if it is far less than bruteforcing MD5 without a clue:
1 | irb(main):005:0> 10**18 * 6**14 |
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 | $ php -a |
And we know this trick with type juggling:
1 | php > var_dump(48 == "48hnj"); |
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 | php > var_dump(json_decode('{"hash": 556}', true)['hash'] == "556CC23863FEF20FAB5C456DB166BC6E"); |
So I did a quick bash script:
1 |
|
The flag was p4{wtf_php_comparisons_how_do_they_work}
.