Local File Inclusion (LFI)

Local File Inclusion (LFI)

🕵️ LFI: cuando el servidor lee lo que tú quieres, no lo que debería 📂

Ya entendimos qué son las vulnerabilidades de inclusión de archivos y por qué ocurren. Ahora toca el paso práctico: explotarlas en distintos escenarios para leer archivos locales del servidor back-end. Un Local File Inclusion (LFI) bien explotado puede filtrar configuraciones, código fuente e incluso credenciales, así que merece la pena dominar sus variantes (puedes consultar más técnicas y payloads en la guía de HackTricks sobre File Inclusion).

🔍 Escenario básico: el parámetro de idioma

El ejercicio de esta sección muestra una aplicación web que permite a los usuarios configurar su idioma en inglés o español. Si seleccionamos, por ejemplo, Spanish, el contenido cambia a español y observamos que la URL incluye un parámetro language que ahora apunta a es.php.

Hay varias formas de que la aplicación cargue contenido distinto según el idioma: puede extraer datos de una tabla diferente en la base de datos, o puede cargar una versión completamente distinta de la web. Sin embargo, como suele ocurrir, el método más común es incluir una plantilla o archivo mediante un motor de plantillas.

Si la aplicación realmente está incluyendo un archivo basado en ese parámetro, es posible que podamos cambiarlo para leer el contenido de otro archivo local. Dos archivos legibles muy comunes en la mayoría de servidores son /etc/passwd en Linux y C:\Windows\boot.ini en Windows.

💡 Paso 1 — Modificar el parámetro: cambiamos el valor de language de es a /etc/passwd.

El resultado confirma que la página es vulnerable: podemos leer el contenido del archivo passwd e identificar qué usuarios existen en el servidor back-end.

🧭 Path Traversal: sorteando el prefijo de directorio

En el ejemplo anterior leímos el archivo especificando su ruta absoluta (por ejemplo, /etc/passwd). Esto funciona cuando el valor del parámetro se usa directamente dentro de la función include(), sin ningún añadido:

include($_GET['language']);

En este caso, al intentar leer /etc/passwd, la función include() busca ese archivo directamente. Sin embargo, es habitual que los desarrolladores antepongan una cadena al parámetro, por ejemplo un directorio:

include("./languages/" . $_GET['language']);

🚫 ¿Qué salió mal? Si intentamos leer /etc/passwd, la ruta resultante pasada a include() sería ./languages//etc/passwd, que no existe, por lo que no conseguimos leer nada. El error detallado de PHP nos muestra la cadena completa que se pasó a include(), confirmando que ese archivo no existe dentro del directorio de idiomas.

Nota: los errores de PHP se habilitaron en esta aplicación solo con fines educativos, para entender cómo maneja nuestra entrada. En producción, este tipo de errores nunca deberían mostrarse. Además, todos estos ataques deberían funcionar igualmente sin depender de que se muestren errores.

💡 Paso 2 — Usar rutas relativas para recorrer directorios: podemos evitar esta restricción añadiendo ../ antes del nombre de archivo, que hace referencia al directorio padre. Por ejemplo, si la ruta completa del directorio de idiomas es /var/www/html/languages/, usar ../index.php haría referencia a /var/www/html/index.php.

Podemos usar este truco para retroceder varios directorios hasta llegar a la raíz (/) y luego especificar la ruta absoluta del archivo que queremos, por ejemplo:

../../../../etc/passwd

Esta vez conseguimos leer el archivo sin importar en qué directorio nos encontrásemos. Esta técnica funciona incluso si el parámetro completo se usa directamente en include(), por lo que conviene aplicarla por defecto: sirve en ambos escenarios. Además, si ya estuviéramos en la ruta raíz (/) y añadiéramos ../, seguiríamos en la raíz — así que, si no estamos seguros de la profundidad del directorio de la aplicación, podemos añadir ../ muchas veces (¡incluso cien!) sin que la ruta se rompa.

Consejo: siempre es más limpio y profesional usar el número mínimo de ../ necesario, especialmente al escribir un informe o un exploit. Se puede calcular cuántos directorios de profundidad hay hasta la raíz y usar exactamente esa cantidad. Por ejemplo, /var/www/html/ está a 3 directorios de la raíz, por lo que basta con ../../../.

💡 Prefijo de nombre de archivo

En el ejemplo anterior, el parámetro language se usaba después del directorio, lo que nos permitía recorrer rutas para leer passwd. En otras ocasiones, la entrada puede añadirse tras una cadena distinta, como un prefijo que forma el nombre completo del archivo:

include("lang_" . $_GET['language']);

🚫 ¿Qué salió mal? Si intentamos el recorrido directo con ../../../etc/passwd, la cadena final sería lang_../../../etc/passwd, que no es una ruta válida. El error confirma que ese archivo no existe.

💡 Paso 3 — Anteponer una barra: en lugar de usar el path traversal directamente, podemos anteponer un / a nuestra carga útil. Esto hace que el prefijo se interprete como un directorio, permitiéndonos omitir el nombre de archivo original y recorrer directorios con normalidad — logrando así leer /etc/passwd.

Nota: esta técnica no siempre funciona, ya que en este ejemplo puede que no exista un directorio llamado lang_/, por lo que la ruta relativa podría no ser correcta. Además, cualquier prefijo añadido a nuestra entrada puede romper otras técnicas de inclusión de archivos; en próximas secciones veremos cómo abordar esto con wrappers y filtros de PHP o con RFI.

🚫 Extensiones adjuntas

Otro caso muy común es que se añada una extensión al parámetro language, así:

include($_GET['language'] . ".php");

Esto es habitual porque evita tener que escribir la extensión cada vez que se cambia de idioma, y también puede considerarse una medida de seguridad, ya que restringe la inclusión a archivos PHP. En este caso, si intentamos leer /etc/passwd, el archivo incluido sería en realidad /etc/passwd.php, que no existe, por lo que la explotación falla directamente.

Existen varias técnicas para evitar esta restricción, que se tratarán en próximas secciones.

Ejercicio: intenta leer cualquier archivo PHP (por ejemplo, index.php) mediante LFI y observa si obtienes su código fuente en crudo o si el archivo se renderiza como HTML.

🔐 Ataques de segundo orden (Second-Order)

Como hemos visto, los ataques LFI pueden adoptar formas muy distintas. Otro vector común, algo más avanzado, es el Second Order Attack. Ocurre porque muchas funcionalidades de una aplicación web pueden estar extrayendo archivos del servidor back-end de forma insegura, basándose en parámetros controlados indirectamente por el usuario.

Por ejemplo, una aplicación puede permitir descargar el avatar propio a través de una URL como /profile/$username/avatar.png. Si registramos un nombre de usuario malicioso, por ejemplo ../../../etc/passwd, es posible cambiar el archivo que se extrae por otro archivo local del servidor, obteniéndolo en lugar del avatar.

En este caso, estaríamos envenenando una entrada de la base de datos (nuestro nombre de usuario) con una carga útil LFI maliciosa. Después, otra funcionalidad de la aplicación usaría ese valor envenenado para realizar el ataque —al descargar el avatar en función del nombre de usuario—. De ahí el nombre Second-Order: el ataque se ejecuta en una etapa posterior a la inyección del payload.

Los desarrolladores suelen pasar por alto este tipo de vulnerabilidades porque protegen la entrada directa del usuario (por ejemplo, un parámetro ?page), pero confían ciegamente en valores ya almacenados en su base de datos, como el nombre de usuario en este caso. Si logramos envenenar ese valor durante el registro, el ataque se vuelve posible.

La explotación de LFI mediante ataques de segundo orden sigue la misma lógica que hemos visto en esta sección. La única diferencia es que necesitamos identificar una función que incluya un archivo basándose en un valor que controlamos de forma indirecta, y luego manipular ese valor para explotar la vulnerabilidad.

Nota: todas las técnicas descritas en este artículo deberían funcionar frente a cualquier vulnerabilidad LFI, independientemente del lenguaje o framework de back-end utilizado.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio