
Stable POC for CVE-2026-25243 (Redis RESTORE double-free -> remote code execution)
Rocky Linux 8.10, aarch64, Redis 8.6.2, jemalloc 5.3.0 के विरुद्ध सत्यापित।
संदर्भ: https://www.zeroday.cloud/blog/redis-cve-2026-25243-deep-dive
TLDR; स्थिर एक्सप्लॉइट, विभिन्न OS डिस्ट्रो और आर्किटेक्चर पर काम करता है।
यह क्या है? Redis में एक मेमोरी भ्रष्टाचार भेद्यता जो एक प्रमाणित
हमलावर को Redis उपयोगकर्ता के रूप में मनमाने कमांड चलाने की अनुमति देती है। हमला
वास्तविक दुनिया में काम करता है और इसके लिए केवल एक RESTORE कमांड की आवश्यकता होती है — यह एक सामान्य Redis
ऑपरेशन है, न कि केवल-व्यवस्थापक। यह एक्सप्लॉइट एक सेकंड से भी कम समय में पूर्ण RCE प्रदर्शित करता है।
प्रभाव? कोई भी प्रमाणित Redis क्लाइंट इसे ट्रिगर कर सकता है, और क्षति पूर्ण है: Redis प्रक्रिया में मनमाना कोड निष्पादन (अक्सर कंटेनरों में root के रूप में चल रहा होता है)। Redis को स्वयं पैच किए बिना इसे कम करने का कोई तरीका नहीं है।
यह एक नज़र में कैसे काम करता है? Redis में एक सीरियलाइज़ेशन सुविधा (RESTORE)
है जो बाइनरी डेटा का एक ब्लॉब लेती है और इसे Redis ऑब्जेक्ट के रूप में पुनर्निर्मित करती है। जो कोड ब्लॉब के प्रारूप को मान्य करता है और जो कोड इसे डिसीरियलाइज़ करता है, वे कुछ अनुक्रमों को पार्स करने के तरीके पर असहमत हैं — यह एक बग है जिसका हमलावर हीप को भ्रष्ट करने के लिए शोषण करता है। एक बार हीप भ्रष्ट हो जाने पर, हमलावर Redis प्रक्रिया में किसी भी मेमोरी पते को पढ़ने और लिखने की क्षमता प्राप्त कर लेता है, और वहाँ से शेल कमांड निष्पादित करने के लिए सर्वर की आंतरिक स्थिति को हाईजैक कर लेता है।
वास्तविक एक्सप्लॉइट तकनीक: यह एक साधारण क्रैश नहीं है। यह एक हीप शोषण श्रृंखला है: भ्रष्ट → ओवरलैप → मनमाना R/W → सूचना रिसाव → सर्वर स्ट्रक्चर खोजें → फंक्शन पॉइंटर्स हाईजैक करें → RCE। एक्सप्लॉइट 9 चरणों में चलता है और रनटाइम पर कई पतों को लीक करने, बाइनरी स्ट्रक्चर पार्स करने और मेमोरी एलियासिंग का पता लगाने की आवश्यकता होती है। इसे विभिन्न आर्किटेक्चरों (x86-64, aarch64, आदि) पर काम करने योग्य बनाने वाली बात यह है कि सभी पते लक्ष्य से ही लीक किए जाते हैं, मान लिए नहीं जाते।
CVE-2026-25243 एकल प्रमाणित RESTORE कमांड से पहुंचने योग्य डबल-फ्री बगों की एक जोड़ी है। RESTORE key ttl <serialized-value> हमलावर-नियंत्रित RDB ब्लॉब को डिसीरियलाइज़ करता है; दोनों बग उस वैलिडेटर के बीच की खाई में रहते हैं जो ब्लॉब की जाँच करता है और उस कन्वर्टर के बीच जो इसे मूर्त रूप देता है।
बग 1 — लीगेसी zipmap रूपांतरण (CWE-415, वह पथ जो यह एक्सप्लॉइट उपयोग करता है)।
zipmap वैलिडेटर (zipmapValidateIntegrity()) और कन्वर्टर
(zipmapNext()) एक अनावश्यक लंबाई एन्कोडिंग के बारे में असहमत हैं। छोटी लंबाई
4 को कानूनी रूप से लंबे पाँच-बाइट रूप FE 04 00 00 00 में लिखा जा सकता है।
वैलिडेटर बाइट्स की एक संख्या उपभोग करता है, कन्वर्टर दूसरी — एक 4-बाइट
पार्सिंग डिसिंक्रोनाइज़ेशन। इसलिए कन्वर्टर उससे भिन्न संरचना पर चलता है
जिसे मान्य किया गया था, फ़ील्ड को डिक्शनरी में डाले जाने के बाद lpSafeToAdd() विफल हो जाता है, और सफाई पथ फ़ील्ड को दो बार मुक्त करता है: एक बार dictRelease() के माध्यम से और फिर से sdsfree() के माध्यम से।
बग 2 — स्ट्रीम उपभोक्ता PEL लोडिंग (CWE-415)।
rdbLoadStreamConsumersGroup() में, एक डुप्लिकेट एंट्री ID वाला उपभोक्ता PEL
दूसरे raxTryInsert() को विफल कर देता है, जो समूह के वैश्विक PEL द्वारा अभी भी स्वामित्व वाले streamNACK पर streamFreeNACK() को कॉल करता है। दो बार मुक्त।
(--vuln-type stream के साथ चुनने योग्य।)
या तो बग हमलावर को एक साथ मुक्त और संदर्भित मेमोरी का एक टुकड़ा देता है — हीप-ओवरलैप एक्सप्लॉइट के लिए क्लासिक प्रारंभिक बिंदु।
प्रभाव: एक प्रमाणित Redis क्लाइंट (कोई व्यवस्थापक अधिकार नहीं, RESTORE एक सामान्य डेटा कमांड है) redis उपयोगकर्ता के रूप में मनमाना कोड निष्पादन प्राप्त करता है — डिफ़ॉल्ट कंटेनर छवि में root।
नौ चरण, जिनमें से प्रत्येक एक कमज़ोर प्रिमिटिव को एक मज़बूत प्रिमिटिव में बदल देता है:
python3 exploit.py --host 127.0.0.1 --port 6379 \
--password mypassword --cmd 'id > /tmp/pwned123.txt'
सत्यापित करें:
cat /tmp/pwned123.txt
# uid=0(root) gid=0(root) groups=0(root)
प्रारंभिक बिंदु: एक्सप्लॉइट केवल x86-64 था और aarch64 लक्ष्य पर चरण 3 में विफल हो गया। अंतिम स्थिति: aarch64 Rocky Linux 8.10 पर एक सेकंड से भी कम समय में पूर्ण RCE, 116 Redis कमांड।
a) रनटाइम लक्ष्य फ़िंगरप्रिंटिंग (नया, चरण 0)। लक्ष्य के बारे में अब कुछ भी मान नहीं लिया जाता। INFO server + INFO memory Redis संस्करण, CPU आर्किटेक्चर (os: पंक्ति से), डिस्ट्रो परिवार (gcc_version से अनुमानित), एलोकेटर, और — सबसे महत्वपूर्ण — तीन सत्यापन एंकर देते हैं: process_id, executable, और सटीक stat_starttime (server_time_usec/1e6 - uptime_in_seconds)। बाद के चरण अनुमान लगाने के बजाय इनके विरुद्ध तुलना करते हैं।
b) आर्किटेक्चर-स्वतंत्र मेमोरी लेआउट। चार हार्डकोडेड x86-64 स्थिरांक (BINARY_ADDR_MIN/MAX, HEAP_ADDR_MIN/MAX) को प्रति-आर्किटेक्चर तालिका (ARCH_PROFILES) से बदल दिया गया है जो x86_64, aarch64 (39- और 48-बिट VA दोनों), riscv64, ppc64le और s390x को कवर करती है, प्रत्येक के लिए ET_EXEC और ET_DYN दोनों प्लेसमेंट के साथ, साथ ही किसी भी सूचीबद्ध न किए गए के लिए एक व्यापक जेनेरिक फ़ॉलबैक। यह वास्तविक कारण था कि एक्सप्लॉइट इस लक्ष्य पर विफल रहा: लीक किया गया पॉइंटर 0x0000ffff8a5fdf32 एक पूरी तरह से मान्य aarch64 mmap पता है जिसे x86-64 रेंज जाँच ने अस्वीकार कर दिया।
c) सर्वसम्मति-आधारित लीक सत्यापन (चरण 3)। हार्डकोडेड हीप विंडो पर भरोसा करने के बजाय, स्कैन अब मेमव्यू में हर संरचनात्मक रूप से मान्य 1337.NNNNNN ऑब्जेक्ट एकत्र करता है और उनमें से कम से कम दो को समान मेमव्यू आधार पता (ptr - offset_of_value) प्राप्त करने की आवश्यकता होती है। व्यवहार में 502 उम्मीदवार सहमत होते हैं, जो इस बात का प्रमाण है जो कोई रेंज तालिका नहीं दे सकती। पुष्टि किया गया पॉइंटर फिर रनटाइम पर हीप विंडो को कैलिब्रेट करता है। प्रारूप सत्यापन को भी (राउंड-ट्रिप-महंगे) लेखन-नियंत्रण परीक्षण से पहले स्थानांतरित कर दिया गया।
d) चरण 3 स्कैन सीमा (बग फिक्स)। स्कैन एक हार्डकोडेड 10 MB तक चलता था जबकि मेमव्यू 1 MB है, इसलिए यह अंत से आगे पढ़ता था, खाली उत्तर प्राप्त करता था और AssertionError: Empty data from memview के साथ रद्द हो जाता था। अब यह मेमव्यू के वास्तविक STRLEN द्वारा सीमित है, प्रति राउंड-ट्रिप 64 KB के बजाय 256 KB पढ़ता है, और व्यर्थ 6×1s रिट्री-स्लीप लूप हटा दिया गया है।
e) चरण 5 पुनर्लिखित: ELF-निर्देशित, क्रैश-मुक्त (सबसे बड़ा)। पुराना कार्यान्वयन एक इमेज पॉइंटर से आगे की ओर स्कैन करता था, पतों की जाँच करता था और जो भी लंबाई एक कचरा SDS हेडर दावा करता था उसे पढ़ता था। इस लक्ष्य पर यह पठनीय-केवल सेगमेंट के अंत से सीधे 0x715000 पर बिना मैप किए छेद में चला गया और सर्वर को मार डाला (getrangeCommand में SIGSEGV → memcpy)। अंधा स्कैनिंग सुरक्षित नहीं बनाया जा सकता। प्रतिस्थापन निर्धारणात्मक है:
7f 45 4c 46 02 हैं, और sdslen() अपना फ्लैग्स बाइट ptr[-1] से लेता है — इसलिए हाईजैक किए गए ऑब्जेक्ट को base+5 पर इंगित करने से e_ident[EI_CLASS]=0x02 फ्लैग्स बाइट बन जाता है, यानी SDS_TYPE_16, जिसकी लंबाई base+0 पर uint16 = 0x457f (0x7f45 बिग-एंडियन) है। ठीक 17791 का STRLEN ही ELF हस्ताक्षर है। बाइनरी की कोई स्थानीय प्रति आवश्यक नहीं है — हेडर लक्ष्य की अपनी मेमोरी से पढ़ा जाता है।PT_LOAD सेगमेंट की सटीक रनटाइम सीमाएँ प्राप्त हों (PIE लक्ष्यों के लिए ET_DYN लोड बायस को संभालते हुए)। प्रत्येक बाद का पठन एक वास्तविक मैपिंग तक सीमित है, इसलिए बिना मैप किए छेद वाला क्रैश अब संरचनात्मक रूप से असंभव है।f) चरण 4 कठोर किया गया। Lua वैलिडेटर पूरी प्रति-आर्क रेंज सूची लेता है (इसलिए 0x400000 पर एक गैर-PIE इमेज और 0xaaaa… पर एक PIE इमेज दोनों पहचाने जाते हैं) और कैलिब्रेटेड हीप विंडो को बाहर करता है। यह एक के बजाय कई उम्मीदवार लौटाता है, इसलिए एक खराब चयन की कीमत रन को समाप्त करने के बजाय एक रिट्री होती है।
g) चरण 7 स्व-सत्यापन। enable_debug_cmd एक हार्डकोडेड stat_starttime - 0x3c द्वारा स्थित किया गया था। अब अपेक्षित stat_starttime मान INFO से ठीक-ठीक ज्ञात है (30 दिनों के बजाय 3-सेकंड की विंडो), स्ट्रक्चर पठन विंडो 4 KB से बढ़कर 32 KB हो गई (stat_starttime ऑफसेट 0x9e0 पर बैठता है, पुरानी सीमा से काफी आगे), और — निर्णायक रूप से — प्रत्येक उम्मीदवार ऑफसेट को एक लाइव ओरेकल के साथ सत्यापित किया जाता है: बाइट सेट करें, DEBUG SET-ACTIVE-EXPIRE 1 भेजें, और देखें कि क्या सर्वर इसे स्वीकार करता है। गलत अनुमान अगले प्रयास से पहले पुनर्स्थापित कर दिए जाते हैं, इसलिए फ्लैग किसी भी बिल्ड पर मान लिए जाने के बजाय पाया जाता है। -0x3c अभी भी पहले आज़माया जाता है और 8.6.2 (ऑफसेट 0x9a4) के लिए सही पुष्टि की गई है।
h) लेखन वास्तव में लैंड करते हैं (चरण 5)। setrangeCommand() dbUnshareStringValue() को कॉल करता है, जो मान को डुप्लिकेट करता है जब तक कि encoding == RAW && refcount == 1 न हो। एन्कोडिंग बाइट अब हाईजैक किए गए पॉइंटर के माध्यम से पहले लेखन से पहले शून्य कर दी जाती है, इसलिए लेखन एक निजी प्रति के बजाय लक्ष्य पते तक पहुँचते हैं।
i) पेलोड सरलीकृत। सभी बैककनेक्ट/रिवर्स-शेल तंत्र, ASCII बैनर और जोड़ा गया ;sleep 5 हटा दिया गया। पेलोड ठीक /bin/sh -c '<--cmd>' है और कुछ और नहीं। --cmd डिफ़ॉल्ट रूप से id > /tmp/pwned123.txt है।
j) गति। चरण 4 8 के बजाय 3 उम्मीदवार एकत्र करता है; चरण 5 ~10^5 बाइट जाँचों को ~40 बल्क रीड से बदल देता है; चरण 3 256 KB रीड का उपयोग करता है और स्थानीय सत्यापन में विफल उम्मीदवारों के लिए राउंड-ट्रिप छोड़ देता है। पूरी श्रृंखला: 116 कमांड, <1 सेकंड।
परिणाम: लक्ष्य कंटेनर में /tmp/pwned123.txt में uid=0(root) gid=0(root) groups=0(root)।
/bin/sh है, जिसकी POSIX और FHS को आवश्यकता होती है।redis_version (7.x और 8.x समर्थित) से चुने जाते हैं। स्ट्रक्चर फ़ील्ड ऑफसेट (executable=24, exec_argv=32) LP64 ABI से अनुसरण करते हैं, और enable_debug_cmd हार्डकोडेड होने के बजाय रनटाइम पर खोजा और सत्यापित किया जाता है।डिफ़ॉल्ट zipmap पथ पर मापे गए 13/13 सफल रन (5 + 8 लगातार), प्रत्येक ≤1 सेकंड में पूरा हुआ। केवल बार-बार निष्पादन के तहत तीन समस्याएँ सामने आईं और अब उन्हें ठीक कर दिया गया है:
k) चरण 0 में SAVE प्रतिस्पर्धा। एक रन ERR Background save already in progress के साथ रद्द हो सकता था जब पिछले रन का (या redis का अपना) बैकग्राउंड सेव अभी भी चल रहा था। SAVE को अब 15 सेकंड तक पुनः प्रयास किया जाता है और, विफल होने पर, रन बिना चेकपॉइंट के आगे बढ़ता है, रद्द होने के बजाय।
l) लक्ष्य के पुनः आरंभ होने के दौरान पुनः कनेक्ट (--connect-retries, डिफ़ॉल्ट 10)। एक विफल प्रयास हीप को भ्रष्ट छोड़ देता है, इसलिए अगले रन का FLUSHALL ज़हरीले चंक्स को मुक्त करता है और सर्वर को नीचे ले जाता है। यह सेकंडों बाद पुनः आरंभ होता है और पूरी तरह से शोषण योग्य है, इसलिए चरण 0 विफल होने के बजाय पुनः कनेक्ट और पुनः प्रयास करता है। हमारे अपने सत्यापन त्रुटियाँ (असमर्थित संस्करण/आर्किटेक्चर) कभी पुनः प्रयास नहीं की जातीं। इसने स्ट्रेस टेस्टिंग के दौरान लगभग 3 में से 1 रन पर दिखाई देने वाली आंतरायिक "stage 0 failed with an empty error" को हटा दिया।
m) 8.6.x के लिए sizeof(streamNACK) सही किया गया। --vuln-type stream पथ ने गलत jemalloc आकार वर्ग स्प्रे किया क्योंकि स्ट्रक्चर को 24 या 32 बाइट मान लिया गया था। 8.6.2 में यह 64 बाइट है (delivery_time, delivery_count, consumer, cgroup_ref_node, streamID id, pel_prev, pel_next)। सही आकार के साथ स्ट्रीम पथ अब चरण 2 पर "key overlap not found" के साथ विफल होने के बजाय चरण 5 तक पहुँचता है।
--vuln-type stream 8.6.2 पर विश्वसनीय नहीं है। आकार सुधार के साथ यह डबल-फ्री, ओवरलैप, R/W प्रिमिटिव और ELF पार्स से गुजरता है, फिर कीस्पेस को अस्थिर कर देता है: सर्वर setrangeCommand में NULL+8 पर o->ptr पढ़ते समय मर जाता है, यानी एक कुंजी लुकअप एक भ्रष्ट ऑब्जेक्ट लौटाता है। यह जो 64-बाइट चंक मुक्त करता है वह अन्य जीवित आवंटनों के साथ साझा किया जाता है, जो इसे zipmap पथ की तुलना में कहीं अधिक संपार्श्विक-भारी बनाता है। डिफ़ॉल्ट --vuln-type zipmap का उपयोग करें, जो 13/13 है।--random-heap-massage (पहले 100k यादृच्छिक कुंजियाँ) सफल होता है लेकिन हमेशा नहीं — स्प्रे किया गया हीप कभी-कभी डबल-फ्री चंक को ऐसी जगह रखता है जहाँ कोई मार्कर कुंजी नहीं उतरती। पुनः चलाने पर सफल होता है।| चरण | प्राप्त प्रिमिटिव | तंत्र |
|---|
| 0 | लक्ष्य प्रोफ़ाइल | INFO server / INFO memory → संस्करण, आर्किटेक्चर, डिस्ट्रो, pid, निष्पादन योग्य पथ, प्रारंभ समय, एलोकेटर |
| 1 | डबल फ्री | malformed zipmap (या stream) RESTORE |
| 2 | मेमोरी साझा करने वाली दो कुंजियाँ | मुक्त चंक पर मार्कर कुंजियाँ स्प्रे करें, एलियासिंग का पता लगाएं, फिर एक कुंजी के SDS हेडर को उसके जुड़वाँ के माध्यम से अधिलेखित करके उसे 1 MB "मेमव्यू" में फुलाएँ |
| 3 | मनमाना R/W | मेमव्यू के अंदर एक INCRBYFLOAT ऑब्जेक्ट खोजें, उसके ptr फ़ील्ड को हाईजैक करें: उस कुंजी पर GETRANGE/SETRANGE अब किसी भी पते को पढ़/लिख सकते हैं |
| 4 | इमेज पॉइंटर | redis-server इमेज के अंदर किसी मान के लिए हीप को पीछे की ओर स्कैन करें |
| 5 | &server | ELF हेडर तक नीचे जाएँ, प्रोग्राम हेडर पार्स करें, लिखने योग्य सेगमेंट डंप करें, server.pid मिलाएँ |
| 6 | मेमोरी में पेलोड | मेमव्यू में "/bin/sh", "-c", "<cmd>" और एक argv सरणी लिखें |
| 7 | हाईजैक किया गया स्ट्रक्चर | server.executable, server.exec_argv, और server.enable_debug_cmd अधिलेखित करें |
| 8 | RCE | DEBUG CRASH-AND-RECOVER → restartServer() → execve(server.executable, server.exec_argv, environ) |
.data/.bss को पठनीय बना देता है। अधिलेखित बाइट्स सहेज कर पुनर्स्थापित कर दी जाती हैं।server.pid को INFO से प्राप्त pid से मिलाएँ — एक सटीक 8-बाइट समानता परीक्षण — फिर server.executable को डीरेफ़रेंस करके और स्ट्रिंग की INFO के executable से तुलना करके पुष्टि करें। पुराना कोड एक ढीली सात-क्षेत्र आकार अनुमानिका स्वीकार करता था; स्ट्रक्चर अब सकारात्मक रूप से पहचाना जाता है।