Parmi toutes les technologies que nous rencontrons au cours de nos audits chez Atlab, PHP est sans aucun doute celle qui nous procure le plus de "shells". A sa décharge, nous le rencontrons plus souvent que les autres.
Le moteur PHP s'est doté, au fil des années, de nombreuses fonctionnalités de cloisonnement ou "sandboxing" permettant aux administrateurs d'isoler les différentes instances les unes des autres.
Ces options, correctement configurées, peuvent éviter qu'une application PHP ne déborde sur une autre et complexifient voire empêchent complètement toute progression vers le système d'exploitation.
Les options principales sont:
- safe_mode : Influence le comportement de nombreuses fonctions internes. L'exécution de commandes par exemple est restreinte aux exécutables appartenants au même compte utilisateur (UID) que PHP.
- open_basedir : Spécifie un répertoire au dessus duquel il est impossible de remonter, similaire à la commande "chroot" sous UNIX.
- disable_functions : Liste de fonctions internes à PHP qui sont désactivées.
- enable_dl : Autorise ou non le chargement de librairies PHP dynamiques.
C'est justement sur cette phase de progression du PHP vers le système d'exploitation que s'est focalisé Stefan Esser dans ses recherches intitulées State of the Art Post Exploitation in Hardened PHP Environments exposées récemment aux conférences Blackhat et Syscan (juillet 2009). Stefan Esser est un personnage très connu dans la sécurité informatique, particulièrement pour tous ses travaux tournants autour du moteur PHP. Il est en charge du Hardened PHP Project, auteur du patch Suhosin (qui apporte de nombreuses améliorations sur la sécurité du moteur PHP) et initiateur du Month Of PHP Bugs. Ce billet a pour but d'exposer nos propres travaux et découvertes, suite à la publication de ses recherches.
Stefan Esser expose une classe de vulnérabilités à part entière, qu'il affirme n'être présente que dans le moteur PHP. A défaut d'être nouvelles puisqu'il en avait déjà fait de belles démonstrations en 2007 lors de son Month Of PHP Bugs, les vulnérabilités de ce type ont été largement négligées, autant par les experts en sécurité que par les développeurs PHP. Elles sont pourtant aussi efficaces que difficiles à corriger parce qu'inhérentes à l'architecture du moteur. C'est donc le moment de rattraper le temps perdu...
Introduction
Venons en au fait; il s'agit de "vulnérabilités d'interruption" traduit littéralement de "Interruption Vulnerabilities".
Nous pouvons assimiler PHP à une machine virtuelle et considérer qu'il y a deux contextes d'exécution. Le premier contexte est celui du moteur PHP, du code en C, compilé dans un binaire CGI ou une librarie Apache. Le second contexte est celui du code utilisateur, en PHP, interpreté par le moteur PHP.
Les vulnérabilités d'interruptions consistent simplement à interrompre des fonctions internes dans le but de changer de contexte, repasser dans celui de l'utilisateur, re-retourner dans la fonction interne, etc. Ce sont ces oscillations entre les deux contextes qui créent ces vulnérabilités et dont nous détaillerons l'exploitation. En deux mots, il s'agit de modifier les données traitées par la fonction interrompue. Une fois l'interruption terminée, l'execution retourne dans la fonction interne du moteur PHP mais son environnement a changé et c'est l'échec !
La finalité est l'obtention d'un accès complet à la mémoire (lecture et écriture). Nous verrons comment désactiver toutes les protections du moteur et exécuter du code PHP non restreint. Nous mentionnerons un raccourci menant à l'exécution de commandes shell, toujours sans restriction. Nous terminerons sur l'exécution de code natif (shellcode) via trois techniques différentes, avec en bonus, un contournement de Suhosin et de grsecurity / DEP (qui empêchent l'exécution de code sur le tas).
Stefan Esser nous expose deux vulnérabilités (non-corrigées) du moteur PHP. Les étapes principales de l'exploitation sont les suivantes :
- Exploitation d'une fuite mémoire dans explode() afin d'obtenir un accès arbitraire en lecture seule.
- Recherche d'informations en mémoire (adresses et structures internes au moteur).
- Exploitation de usort() afin d'obtenir un accés arbitraire en lecture/écriture.
- Prise de contrôle du moteur PHP, désactivation des protections et/ou exécution d'un shellcode.
Avant de commencer, il est important de connaitre les principales structures utilisées dans le moteur PHP:
- HashTable : Objet représentant un tableau PHP pouvant être associatif (une table de hash), numérique (tableau classique avec indexes) ou les deux à la fois.
- Bucket : Entrée dans une HashTable (case d'un tableau PHP). Un Bucket est prévu pour faire parti d'une liste chainée. Chaque bucket contient un zval.
- zval : Structure stockant les variables PHP. Cette structure permet de faire abstraction du type de données qu'elle contient (entier, chaîne de charactères, tableau, object, ...)
- zvalue_value : Valeur d'une variable PHP. La valeur d'un zval est son zvalue_value. C'est une union de variables C, c'est à dire qu'elle peut être un long (nombre PHP), un pointeur sur HashTable (tableau PHP), un pointeur sur chaîne (chaîne de caractères PHP), etc.
- _zend_executor_globals : Structure principale stockant les informations sur le processus PHP en cours, sa configuration (variables INI), etc.
- _zend_ini_entry : Représentation d'une option de configuration PHP (variable INI).
Il est recommandé de se référer aux documents (rapport et présentation) de Stefan Esser ainsi qu'aux sources dans lesquelles ces structures sont amplement détaillées. N'oublions pas que certaines structures ont évoluées au fil des années et sont donc agencées différement selon les versions du moteur.
Enfin, il faut savoir que l'interruption d'une fonction peut être faite de plusieurs manières. Nous utiliserons la plus "simple", qui consiste à passer des arguments invalides/inattendus à l'appel d'une fonction, tout en ayant déclaré un gestionnaire d'erreur (grâce à set_error_handler()). Lors d'une erreur considérée fatale, c'est au gestionnaire d'erreur de terminer l'exécution par un appel à exit() ou die(). Nous sommes libres, si l'on a défini notre propre gestionnaire (et donc remplacé celui de PHP) de ne pas terminer l'exécution de code après une erreur, même fatale.
Exploitation d'une fuite mémoire dans explode() - 1ère partie
La fonction explode() découpe une chaîne de caractères en plusieurs morceaux selon un délimiteur. Elle prend donc deux arguments; le délimiteur et la chaine de caractères à découper. Le résultat est ensuite retourné sous forme de tableau.
Commençons directement par nous salir les mains avec le premier "proof-of-concept".
<?php
function leakErrorHandler()
{
if (is_string($GLOBALS['var'])) {
parse_str("2=9&254=2", $GLOBALS['var']);
}
return true;
}
$var = str_repeat("A", 128);
set_error_handler("leakErrorHandler");
$data = explode(new StdClass(), &$var, 1);
restore_error_handler();
var_dump($data);
?>
Ce code PHP doit afficher une chaine de 128 caractères dont la plupart sont non imprimables. Pour le reste de ce billet, nous utilisons une fonction 'hexdump' pour un affichage plus agréable. En remplaçant var_dump($data); par hexdump($data[0]); nous obtenons un résultat similaire au suivant :
00000000: 08 00 00 00 07 00 00 00 02 00 00 00 FF 00 00 00 ................
00000010: E8 69 7A 00 E8 69 7A 00 40 6A 7A 00 A0 51 7A 00 .iz..iz.@jz..Qz.
00000020: A6 1A 26 00 00 00 01 00 11 00 00 00 31 00 00 00 ..&.........1...
00000030: 39 00 00 00 B8 69 7A 00 19 00 00 00 11 00 00 00 9....iz.........
Nous venons d'obtenir le contenu d'une HashTable en mémoire. Nous pouvons le vérifier en comparant son contenu par rapport à une structure HashTable (encore merci à Stefan Esser pour ses schémas).

Détaillons nos actions:
- Nous allouons un espace mémoire de 128 octects.
- Nous déclarons notre gestionnaire d'erreur par un appel à set_error_handler().
- Nous appelons la fonction explode() avec trois arguments. Le premier, le délimiteur, est volontairement invalide et c'est ce qui déclenchera notre gestionnaire d'erreur. Le deuxième est la chaine de caractères à découper (128 octets de "A") que nous passons en référence en précèdant son nom par un &. Le troisième argument sert à ignorer la valeur du délimiteur.
- La fonction explode() s'interrompe car le premier argument est invalide (un objet au lieu d'une chaine de caractères). Une exception est levée et notre gestionnaire est appelé.
- Depuis notre gestionnaire, nous appelons parse_str() afin de tranformer notre chaine de caractères en un tableau PHP sans ré-allouer de variable (voir Note 2).
- Nous désactivons notre gestionnaire d'erreur grâce à restore_error_handler() puis nous affichons les données retournées par explode().
Que s'est-il réellement passé ?
Du point de vue du moteur, nous avons écrasé un pointeur par un autre. En effet, la valeur d'une variable est representée par l'union zvalue_value. En remplacant notre chaine de caractères par un tableau nous avons remplacer un char *val; par un HashTable *ht;.
A ce stade, si l'on pouvait afficher le contenu de nos zval, voici ce que nous pourrions voir avant et après appel du gestionnaire d'erreur :

Une fois retourné dans le code de explode(), celui-ci va parcourir notre chaine de caractères afin de la découper. Or, le pointeur qui sera déréferencé n'est plus celui pointant vers 128 octets de "A" mais celui d'une HashTable. PHP va donc lire 128 octets à partir de l'addresse d'une HashTable en mémoire.
Notons quelques détails supplémentaires :
- Le fait de passer notre variable par référence (&$var) n'est pas anodin. Lorsque nous y accèderons au travers de $GLOBALS['var'] nous modifierons la variable originale et non pas une copie de celle-ci.
- Le fait d'utiliser parse_str() nous permet de convertir une variable en conservant son zval original. Si nous utilisons $GLOBALS['var'] = array();, un nouveau zval et un nouveau zvalue_value seront alloués.
Lire une HashTable, quel intérêt ?
Hormis la mise en évidence de la fuite mémoire, le fait de lire une HashTable va nous permettre de déduire plusieurs choses concernant le système :
- Architecture Little/Big Endian
- Taille d'un int, long et pointeur.
- Adresse de la dtors (adresse dans la zone .text)
- Adresse de n'importe quelle variable PHP (voir 3ème partie).
Entre autre, ces informations permettront de rendre les exploits portables sur les différents systèmes et architectures.
Exploitation d'une fuite mémoire dans explode() - 2ème partie
En modifant un zval de type "string" en un zval de type "array" nous remplaçons en réalité un pointeur sur caractères par un pointeur sur HashTable. Qu'en est t-il des autres types de variables ?
Il se trouve qu'un long est stocké sur 4 octets tout comme un pointeur. Si l'on modifie notre zval "string" par un zval "long" nous allons écraser notre pointeur sur caractères par un long. Etant donné que nous contrôlons la valeur de ce long, alors nous contrôlons le pointeur.
Provoquer une fuite mémoire à une adresse arbitraire est donc trivial. Il suffit de remplacer l'appel à parse_str(). Exemple:
$GLOBALS['var'] += 0x08048000;
Après cette modification, nous obtenons bel et bien le header ELF de l'exécutable PHP (ou celui de Apache dans le cas où PHP est en module). L'utilisation de += n'est pas anodine. Cela permet, encore une fois, de modifier le zval existant sans en créer de nouveau. Lors de l'opération, le moteur va convertir $GLOBALS['var'] en nombre (zéro en l'occurence) avant d'y additionner notre valeur.
Attention, la taille d'un long n'est pas systèmatiquement la meme que celle d'un pointeur (typiquement, Windows en 64Bits). La technique devra être adaptée sur ces systèmes, mais nous ne nous attarderons pas dessus.
Exploitation d'une fuite mémoire dans explode() - 3e partie
A ce stade, nous avons donc un accès complet (en lecture) à la mémoire du processus PHP. Encore faut-il savoir où lire. Nous n'avons en effet aucune connaissance de l'agencement de la mémoire. D'une part il faut éviter une erreur de segmentation, d'autre part nous devons localiser les informations intéressantes en mémoire.
Grâce à la fuite du contenu d'une HashTable (1ère partie), nous allons voir qu'il est possible de récupérer l'adresse de n'importe quelle variable PHP.
Pour cela, reprenons le code de notre premier exemple. Au lieu de créer un tableau de deux cases, nous allons créer un tableau vierge puis y mettre la variable PHP dont nous désirons récupérer l'adresse.
$test = "toto42";
parse_str("", $GLOBALS['var']);
$GLOBALS['var'][0] = &$test;
Il suffit ensuite de récupérer le tableau, lire le premier bucket et récupérer son champ pData. Le champ pData est un pointeur sur zval, en l'occurence le zval correspondant à notre variable $test. Le champ pDataPtr est un pointeur sur le zvalue_value du zval.
Vérifions à l'aide de GDB. Nous ajoutons un appel à date() à la fin de notre code et nous lancons gdb:

Avant de continuer, nous vous conseillons de créer un jeu de fonctions qui surchargent la fuite mémoire présente dans explode(). Exemples :
- leak_mem($ptr, $len) : Retourne $len octets de mémoire à l'adresse $ptr.
- leak_var_zval($variable) : Retourne l'adresse du zval d'une variable.
- leak_var_zvalue($variable) : Retourne l'adresse du zvalue_value d'une variable.
- leak_int(), leak_long(), leak_ptr(), etc.
Exploitation de usort()
usort() est une fonction qui permet de trier un tableau "sur place", c'est à dire sans allouer de nouveau tableau. De plus, l'interruption de cette fonction se fait naturellement (sans recours à un gestionnaire d'erreur) puisqu'elle va appeler notre fonction de comparaison plusieurs fois afin de trier le tableau.
Le moteur PHP comporte des optimisations concernant la gestion de sa mémoire. Notamment, la création d'une nouvelle variable PHP ne signifie pas systématiquement un appel à malloc() (ou équivalent) au niveau du moteur. Inversement, la suppression d'une variable ne signifie pas que son espace mémoire correspondant sera free(). Le moteur s'occupe donc d'allouer ou de ré-utiliser de la mémoire lorsque cela est possible.
La vulnérabilité peut être illustrée par le code suivant :
function usercompare($a, $b)
{
global $arr ;
if (isset($arr[2])) {
unset($arr[2]);
}
return 0;
}
$arr = array (0 => "AAAAAAAAAAAAAAAAAAA",
1 => "BBBBBBBBBBBBBBBBBBB",
2 => "CCCCCCCCCCCCCCCCCCC",
3 => "DDDDDDDDDDDDDDDDDDD");
@usort ($arr , "usercompare");
L'exécution du code ci-dessus va provoquer un crash. En effet, nous supprimons la 2ème case du tableau, avant que celle-ci ne soit utilisée. Lorsqu'elle le sera, l'emplacement mémoire qu'elle occupait étant désormais inexistant, le moteur lira des données invalides provoquant un crash (dans la majorité des cas).
Une exploitation pertinente de usort() peut se résumer ainsi :
- Création d'un faux zval en mémoire.
- Récupération de l'adresse du faux zval (cf: exploitation de explode() - 3ème partie)
- Création d'un faux bucket en mémoire avec un pointeur sur le faux zval.
- Interruption de usort()
- Assignation du faux bucket à une variable (nouvelle et unique).
- Destruction d'une case du tableau.
Après une exploitation réussie, nous retrouvons notre faux zval dans une des cases du tableau. L'objectif ? Avoir placé un zval qui adresse la quasi-totalité de la mémoire sous forme d' une fausse chaine de caractères. Cela se représente facilement par un zvalue_value dont le pointeur (str) est à 0x00000000 et dont la longeur (len) est à 0x7fffffff (soit 2 Go). Les dévelopeurs PHP ayant choisi de stocker la longeur d'une chaine sur un int, nous sommes limité a 0x7fffffff.
A partir de là, lire ou écrire dans la mémoire du processus est aussi simple que:
$memory = &$arr[2];
$read = $memory[0x41414141];
$memory[0x41414141] = $write;
Maintenant que nous avons un accès complet à la mémoire aussi bien en lecture qu'en écriture, nous verrons dans un 2ème billet, comment utiliser cela afin de désactiver les protections du moteur, appeler des fonctionnalités désactivées, et même exécuter du code natif.
