
Laboratorio de demostración seguro por defecto que muestra cómo el endurecimiento de contenedores (imágenes distroless, no root, sistema de archivos de solo lectura, secretos inyectados en tiempo de ejecución) puede neutralizar una RCE crítica de Next.js/React Server Actions (CVE-2025-55182 “React2Shell”), con despliegues seguros vs. inseguros lado a lado y registros de explotación.
Este proyecto demuestra una vulnerabilidad crítica de ejecución remota de código (RCE) en una aplicación Next.js (específicamente mediante Server Actions) y cómo el endurecimiento de la infraestructura neutraliza eficazmente el ataque incluso cuando la vulnerabilidad de código permanece.
Compara un despliegue estándar "Inseguro" con un despliegue endurecido "Seguro" que utiliza imágenes Distroless y sistemas de archivos de solo lectura.
Las vulnerabilidades de software son inevitables. Cuando el código falla, tu infraestructura debe impedir que el atacante amplíe su punto de apoyo.
Existe una RCE crítica (CVE-2025-55182, también conocida como React2Shell) en la implementación de React Server Components (RSC) utilizada por Next.js.
spawnSync) sin autenticación.curl, wget, ls, cat) para robar secretos o descargar malware.
child_process.spawnSync() de Node.js. Esto ejecuta binarios directamente, sin necesidad de un shell (/bin/sh).chmod +x) y lo ejecuta.Los siguientes registros demuestran cómo se ven los intentos de ataque desde la perspectiva de la aplicación. Este contraste pone de relieve de forma vívida la eficacia de las medidas de seguridad.
logs/server.safe.log)Los registros muestran fallos repetidos (ENOENT).
spawnSync intenta ejecutar ls, id, curl. La imagen Distroless simplemente no tiene estos binarios. No se trata solo de la falta de un shell; las herramientas en sí han desaparecido.[Instrumentation] Logging initialized. Writing to: /app/logs/server.safe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"2. Verify Binary was Written","verification":{...},"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"","stderr":"","error_obj":{"message":"spawnSync /tmp/hello_test EACCES","code":"EACCES"},"success":true}`'
logs/server.unsafe.log)Los registros confirman la ejecución exitosa de comandos y la manipulación del sistema de archivos.
[Instrumentation] Logging initialized. Writing to: /app/logs/server.unsafe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"Hello from Go binary!\\n","stderr":"","error_obj":null,"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"id","args":[],"stdout":"uid=0(root) gid=0(root) ...","stderr":"","status":0,"signal":null}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"cat","args":["/app/.env"],"stdout":"","stderr":"cat: can\'t open \'/app/.env\': No such file or directory\n","status":1,"signal":null}`'
(Nota: En los registros inseguros, cat /app/.env falla arriba porque el archivo se llama .env en la raíz, pero ls -la en los registros completos revelaría la estructura de directorios.)
Intentando ejecutar comandos shell estándar.
id, ls, cat .env y acceder a datos sensibles.spawnSync /bin/sh ENOENT. No hay shell para ejecutar comandos.Intentando omitir la "falta de herramientas" subiendo un binario personalizado.
/tmp/malware.chmod +x.EROFS: read-only file system.¿Puede un atacante cargar un binario en una variable y ejecutarlo directamente desde la memoria?
global.payload = "...") y luego ejecutarlo.child_process de Node.js (spawn, exec) requieren una ruta de archivo. No pueden ejecutar directamente un búfer o una cadena.memfd_create (una syscall para crear un archivo anónimo en la RAM).memfd_create de forma nativa. Acceder a ella requeriría un complemento de C++ (como ffi-napi) preinstalado en node_modules.gcc, make), un atacante no puede construir este complemento sobre la marcha.Las imágenes "Distroless" contienen solo tu aplicación y sus dependencias de ejecución. No contienen gestores de paquetes, shells ni herramientas UNIX estándar.
ls), descargar archivos (curl) ni escalar privilegios fácilmente.Configura el runtime de tus contenedores para montar el sistema de archivos raíz como solo lectura.
docker-compose.yml:
read_only: true
tmpfs:
- /tmp:noexec # CRITICAL: explicitly block execution!
/tmp (escritura exitosa), pero la ejecución falla con EACCES (Permiso denegado) debido al flag noexec. Esto equilibra la funcionalidad (tmp escribible) con la seguridad.No incluyas archivos .env en tus imágenes de contenedor. Si un atacante puede leer archivos (p. ej., cat .env), tus secretos están comprometidos.
environment de Docker).Inicia el entorno:
Tanto la aplicación segura como la insegura están definidas en un único archivo docker-compose.yml.
docker compose up --build -d
Ejecuta los exploits: Puedes ejecutar los exploits contra los puertos específicos para ver la diferencia.
Apuntando a la aplicación insegura (Puerto 3001):
# 1. Standard RCE (LotL) - SUCCEEDS
python exploit/poc.py http://localhost:3001
# 2. Advanced Attack (BYOL) - SUCCEEDS
python exploit/poc_advanced.py http://localhost:3001
Apuntando a la aplicación segura (Puerto 3000):
# 1. Standard RCE (LotL) - FAILS (ENOENT)
python exploit/poc.py http://localhost:3000
# 2. Advanced Attack (BYOL) - FAILS (EACCES/EROFS)
python exploit/poc_advanced.py http://localhost:3000
Limpieza:
docker compose down
| Característica | ❌ Entorno inseguro (Puerto 3001) | ✅ Entorno seguro (Puerto 3000) |
|---|
| Imagen base | node:20-alpine (Contiene ls, curl, wget, etc.) | gcr.io/distroless/nodejs20-debian12 (Sin shell, sin herramientas) |
| Sistema de archivos | Escribible (valor predeterminado estándar de Docker) | Solo lectura (read_only: true) |
| Secretos | Archivo .env en el disco (Vulnerable a cat .env) | Variables de entorno (Inyectadas en tiempo de ejecución) |
| Usuario | root (predeterminado) | No root (aplicado por Distroless) |