11 คะแนน โดย nuremberg 15 일 전 | 4 ความคิดเห็น | แชร์ทาง WhatsApp

สรุปหนึ่งบรรทัดโดยรวม

เป็นช่องโหว่ Pre-Auth CPU exhaustion ที่เกิดใน MultiPartParser ของ Django เมื่อเนื้อหาส่วนที่มี Content-Transfer-Encoding: base64 ประกอบด้วยช่องว่างเป็นหลัก โดยคำขอเพียงประมาณ 2.5MB หนึ่งครั้งสามารถทำให้ใช้เวลาในการประมวลผลมากกว่าปกติเกิน 2,100 เท่า (CVE-2026-33033)

สรุป

  • สามารถทริกเกอร์ได้โดยไม่ต้องยืนยันตัวตน แม้เป็นเซิร์ฟเวอร์ที่ใช้การตั้งค่าเริ่มต้น
    • เนื่องจาก CSRF middleware เข้าถึง request.POST ก่อนเข้าสู่ view จึงทำให้ MultiPartParser ทำงานโดยอัตโนมัติ ดังนั้นแม้จะเป็น endpoint ที่ต้องยืนยันตัวตน ก็ยังเสียเวลาไปหลายวินาทีตั้งแต่ขั้นตอนตรวจสอบ CSRF แล้ว
  • คำขอขนาด 20MB เพียงหนึ่งครั้งสามารถยึด worker เดี่ยวไว้ได้นานประมาณ 1 นาที
    • หากเป็นการตั้งค่า gunicorn ทั่วไปที่รันด้วย worker 4~16 ตัว คำขอพร้อมกันเพียงหลักสิบก็ทำให้เซิร์ฟเวอร์แทบใช้งานไม่ได้
  • Django ใช้ MultiPartParser ในการประมวลผลคำขอ multipart/form-data และเนื่องจาก CSRF middleware เข้าถึง request.POST ก่อนเข้าสู่ view จึงทำให้ parser นี้ถูกเรียกใช้งานเสมอแม้ไม่มีการยืนยันตัวตน
  • แกนของช่องโหว่คือโครงสร้างที่มีสามเลเยอร์คูณกัน
    • (Layer 1) base64 alignment while-loop: เมื่อเอาช่องว่างออกจากชังก์แล้ว สถานะ remaining != 0 จะคงอยู่ ทำให้ field_stream.read(1) ถูกเรียกซ้ำกับสตรีมที่เหลือทั้งหมดหลังจากนั้น
    • (Layer 2) ต้นทุน O(C) ที่ซ่อนอยู่ของ LazyStream.read(1): ทุกครั้งที่เรียก read(1) ภายในจะดึงบัฟเฟอร์ราว 64KB ออกมาทั้งก้อน แล้วผลัก 65,535 ไบต์กลับเข้าไปด้วย unget() ซ้ำไปมา
    • (Layer 3) bytes concatenation แบบ O(C) ของ unget(): มีการสร้างอ็อบเจ็กต์ใหม่จาก bytes + self._leftover ทุกครั้ง
  • คำขอขนาด 2.5MB เพียงหนึ่งครั้งสามารถก่อให้เกิดการคัดลอกหน่วยความจำภายในรวมประมาณ 86GB และบน M2 จะยึด worker หนึ่งตัวเต็ม ๆ ไว้ราว 5.3 วินาที ส่วนที่ 20MB จะใช้เวลาประมาณ 1 นาที
  • ภายใน unget() มีโค้ด sanity check (_update_unget_history) อยู่แล้ว แต่การโจมตีครั้งนี้มีรูปแบบที่ขนาดของ unget() ลดลงทีละ 1 ในทุกครั้งแบบ monotonic decreasing จึงไม่มีวันเข้าเงื่อนไขการตรวจจับ (number_equal > 40)
  • แกนสำคัญของแพตช์จากทีม Django คือเปลี่ยน read(4 - remaining)read(self._chunk_size) เพื่อให้อ่านครั้งละ 64KB แทนการอ่านทีละ 1~3 ไบต์ ส่งผลให้จำนวนครั้งของการเรียก read ลดจาก 2.5 ล้านครั้งเหลือประมาณ 40 ครั้ง
  • แม้ค่าเริ่มต้นของ client_max_body_size ใน Nginx จะเป็น 1MB แต่ก็มักถูกผ่อนปรนใน endpoint สำหรับอัปโหลดไฟล์ และค่าเริ่มต้นของ LimitRequestBody ใน Apache httpd คือ 1GB ดังนั้นการพึ่งพา proxy เพียงอย่างเดียวจึงไม่รับประกันการป้องกัน
  • เป็นช่องโหว่ที่ค้นพบโดยใช้ Claude Code + Codex และน่าสนใจที่เฟรมเวิร์กซึ่งผ่านการขัดเกลามาเกือบ 20 ปีแล้วยังมี Pre-Auth DoS หลงเหลืออยู่

4 ความคิดเห็น

 
kalista22 14 일 전

ลุยยยย

 
tangokorea 15 일 전

มีใครลองทำอันนี้เองบ้าง?

 
nuremberg 15 일 전

มี PoC ที่ทำขึ้นเพื่อการสาธิตถูกอัปไว้บน GitHub

https://github.com/ch4n3-yoon/CVE-2026-33033-PoC

 
tangokorea 15 일 전

สุดยอดครับ