# [CRIT] LiteLLM Vulnerability Chain Lets Low-Privilege Users Take Over AI Gateway Servers

**Source:** The Hacker News
**Published:** 2026-06-15
**Article:** https://thehackernews.com/2026/06/litellm-vulnerability-chain-lets-low.html

## Threat Profile

LiteLLM Vulnerability Chain Lets Low-Privilege Users Take Over AI Gateway Servers 
 Swati Khandelwal  Jun 15, 2026 Artificial Intelligence / Vulnerability 
A default low-privilege account on a LiteLLM proxy can climb to full admin and run code on the server by chaining three vulnerabilities, researchers at Obsidian Security disclosed
LiteLLM is a widely deployed open-source AI gateway that brokers calls to more than 100 model providers behind one OpenAI-compatible interface.
A server takeover …

## Indicators of Compromise (high-fidelity only)

- **CVE:** `CVE-2026-47101`
- **CVE:** `CVE-2026-47102`
- **CVE:** `CVE-2026-40217`
- **CVE:** `CVE-2026-42271`

## MITRE ATT&CK Techniques

- **T1539** — Steal Web Session Cookie
- **T1555.003** — Credentials from Web Browsers
- **T1190** — Exploit Public-Facing Application
- **T1528** — Steal Application Access Token
- **T1098.001** — Account Manipulation: Additional Cloud Credentials
- **T1195.002** — Compromise Software Supply Chain
- **T1068** — Exploitation for Privilege Escalation
- **T1078.003** — Valid Accounts: Local Accounts
- **T1098** — Account Manipulation
- **T1059.004** — Command and Scripting Interpreter: Unix Shell
- **T1059.006** — Command and Scripting Interpreter: Python
- **T1554** — Compromise Host Software Binary
- **T1546** — Event Triggered Execution

## Kill chain phases observed

_(none detected from narrative keywords)_

## Recommended hunts

### LiteLLM virtual API key minted with wildcard allowed_routes (CVE-2026-47101)

`UC_57_4` · phase: **exploit** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(Web.http_method) as method values(Web.user) as user values(Web.dest) as dest from datamodel=Web.Web where Web.http_method="POST" (Web.url="*/key/generate*" OR Web.url="*/key/update*" OR Web.url="*/team/new*" OR Web.url="*/team/update*" OR Web.url="*/team/member_add*") by Web.src Web.url Web.http_user_agent | `drop_dm_object_name(Web)` | join type=left src [search index=* sourcetype=*waf* OR sourcetype=*nginx* OR sourcetype=*proxy* method=POST (uri_path="*/key/generate*" OR uri_path="*/key/update*") ("allowed_routes" AND ("/*" OR "\"/*\"")) | stats values(_raw) as raw_body by src] | where isnotnull(raw_body) | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
```

### LiteLLM /user/update self-promotion to proxy_admin (CVE-2026-47102)

`UC_57_5` · phase: **install** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count values(Web.http_method) as method min(_time) as firstTime max(_time) as lastTime from datamodel=Web.Web where Web.http_method="POST" Web.url="*/user/update*" by Web.src Web.dest Web.user Web.url | `drop_dm_object_name(Web)` | join type=left src [search index=* sourcetype IN (*waf* *nginx* *proxy* *litellm*) method=POST uri_path="*/user/update*" "user_role" "proxy_admin" | stats values(_raw) as evidence values(_time) as evidence_time by src] | where isnotnull(evidence) | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
```

### LiteLLM Python proxy spawning shell / network tool (CVE-2026-40217 / CVE-2026-42271 post-exploit)

`UC_57_6` · phase: **install** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(Processes.process) as cmdline values(Processes.process_name) as child values(Processes.parent_process) as parent_cmd from datamodel=Endpoint.Processes where Processes.parent_process_name IN ("python","python3","python3.10","python3.11","python3.12","uvicorn","gunicorn") (Processes.parent_process="*litellm*" OR Processes.parent_process="*uvicorn*litellm*" OR Processes.parent_process="*gunicorn*litellm*") (Processes.process_name IN ("sh","bash","dash","zsh","nc","ncat","socat","curl","wget","perl","ruby") OR Processes.process="*bash -i*" OR Processes.process="*/dev/tcp/*" OR Processes.process="*os.system*" OR Processes.process="*import pty*") by Processes.dest Processes.user Processes.parent_process_name Processes.process_name | `drop_dm_object_name(Processes)` | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
```

**Defender KQL:**
```kql
let LookbackDays = 7d;
DeviceProcessEvents
| where Timestamp > ago(LookbackDays)
| where InitiatingProcessFileName in~ ("python","python3","python3.10","python3.11","python3.12","uvicorn","gunicorn")
| where InitiatingProcessCommandLine has_any ("litellm","litellm.proxy","litellm_proxy")
| where FileName in~ ("sh","bash","dash","zsh","ksh","nc","ncat","socat","curl","wget","perl","ruby")
   or ProcessCommandLine has_any ("bash -i","/dev/tcp/","os.system","subprocess.Popen","import pty","socket.socket","sh -i")
| where AccountName !endswith "$"
| project Timestamp, DeviceName, AccountName,
          ParentImage = InitiatingProcessFolderPath,
          ParentCmd = InitiatingProcessCommandLine,
          ChildImage = FolderPath,
          ChildCmd = ProcessCommandLine,
          SHA256
| order by Timestamp desc
```

### LiteLLM config.yaml callbacks modified (post-RCE persistence)

`UC_57_7` · phase: **install** · confidence: **High** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(Filesystem.action) as action values(Filesystem.process_name) as writer from datamodel=Endpoint.Filesystem where Filesystem.action IN ("modified","created","renamed") (Filesystem.file_name IN ("config.yaml","litellm_config.yaml","proxy_config.yaml","litellm.config.yaml") OR Filesystem.file_path="*/litellm/*config*.yaml" OR Filesystem.file_path="*/etc/litellm/*") by Filesystem.dest Filesystem.file_path Filesystem.user | `drop_dm_object_name(Filesystem)` | where NOT match(writer,"(?i)^(vim|nano|vi|emacs|code|nvim)$") | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
```

**Defender KQL:**
```kql
let LookbackDays = 7d;
DeviceFileEvents
| where Timestamp > ago(LookbackDays)
| where ActionType in ("FileModified","FileCreated","FileRenamed")
| where FileName in~ ("config.yaml","litellm_config.yaml","proxy_config.yaml","litellm.config.yaml")
   or FolderPath has_any ("/litellm/","/etc/litellm/","/opt/litellm/","/app/litellm/")
| where InitiatingProcessFileName !in~ ("vim","nano","vi","emacs","code","nvim","helm","kubectl")
| project Timestamp, DeviceName, FileName, FolderPath, PreviousFolderPath,
          InitiatingProcessAccountName, InitiatingProcessFileName,
          InitiatingProcessCommandLine, InitiatingProcessFolderPath, SHA256
| order by Timestamp desc
```

### Access to LiteLLM /guardrails/test_custom_code or MCP preview endpoints (CVE-2026-40217 / CVE-2026-42271)

`UC_57_8` · phase: **exploit** · confidence: **Medium** · AI-generated for this article

**Splunk SPL (CIM):**
```spl
| tstats summariesonly=t count min(_time) as firstTime max(_time) as lastTime values(Web.http_method) as method values(Web.url) as url values(Web.status) as status values(Web.user) as user from datamodel=Web.Web where Web.url="*/guardrails/test_custom_code*" OR Web.url="*/mcp/preview*" OR Web.url="*/mcp_preview*" OR Web.url="*/v1/mcp/preview*" by Web.src Web.dest Web.url Web.http_user_agent | `drop_dm_object_name(Web)` | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
```

### Infostealer — non-browser process accessing browser cookie/login DBs

`UC_BROWSER_STEALER` · phase: **actions** · confidence: **High**

**Splunk SPL (CIM):**
```spl
| tstats `summariesonly` count min(_time) as firstTime max(_time) as lastTime
    from datamodel=Endpoint.Filesystem
    where (Filesystem.file_path="*\Google\Chrome\User Data\*\Login Data*"
        OR Filesystem.file_path="*\Google\Chrome\User Data\*\Cookies*"
        OR Filesystem.file_path="*\Microsoft\Edge\User Data\*\Login Data*"
        OR Filesystem.file_path="*\Mozilla\Firefox\Profiles\*\logins.json*"
        OR Filesystem.file_path="*\Mozilla\Firefox\Profiles\*\cookies.sqlite*")
      AND NOT Filesystem.process_name IN ("chrome.exe","msedge.exe","firefox.exe","brave.exe","opera.exe")
    by Filesystem.dest, Filesystem.process_name, Filesystem.file_path, Filesystem.user
| `drop_dm_object_name(Filesystem)`
```

**Defender KQL:**
```kql
DeviceFileEvents
| where Timestamp > ago(7d)
| where InitiatingProcessAccountName !endswith "$"
| where FolderPath has_any (@"\Google\Chrome\User Data\", @"\Microsoft\Edge\User Data\", @"\Mozilla\Firefox\Profiles\")
| where FileName in~ ("Login Data","Cookies","logins.json","cookies.sqlite")
| where InitiatingProcessFileName !in~ ("chrome.exe","msedge.exe","firefox.exe","brave.exe","opera.exe")
| project Timestamp, DeviceName, InitiatingProcessAccountName, InitiatingProcessFileName, FolderPath, FileName, ActionType
```

### OAuth consent / suspicious app grant

`UC_OAUTH_ABUSE` · phase: **actions** · confidence: **High**

**Splunk SPL (CIM):**
```spl
| tstats `summariesonly` count min(_time) as firstTime max(_time) as lastTime
    from datamodel=Authentication.Authentication
    where Authentication.action="success"
      AND Authentication.signature IN (
        "Consent to application",
        "Add app role assignment grant to user",
        "Add OAuth2PermissionGrant",
        "Add delegated permission grant")
    by Authentication.user, Authentication.app, Authentication.src, Authentication.signature
| `drop_dm_object_name(Authentication)`
```

**Defender KQL:**
```kql
CloudAppEvents
| where Timestamp > ago(7d)
| where ActionType in ("Consent to application.","Add OAuth2PermissionGrant.","Add delegated permission grant.")
| project Timestamp, AccountObjectId, AccountDisplayName, ActivityType,
          ActivityObjects, IPAddress, UserAgent
```

### Trusted vendor binary / installer launching unusual children

`UC_SUPPLY_CHAIN` · phase: **exploit** · confidence: **Medium**

**Splunk SPL (CIM):**
```spl
| tstats `summariesonly` count min(_time) as firstTime max(_time) as lastTime
    from datamodel=Endpoint.Processes
    where Processes.parent_process_name IN ("setup.exe","installer.exe","update.exe")
      AND Processes.process_name IN ("powershell.exe","cmd.exe","rundll32.exe","regsvr32.exe","mshta.exe","wscript.exe","cscript.exe","wmic.exe","bitsadmin.exe")
    by Processes.dest, Processes.user, Processes.parent_process_name, Processes.process_name, Processes.process
| `drop_dm_object_name(Processes)`
```

**Defender KQL:**
```kql
DeviceProcessEvents
| where Timestamp > ago(7d)
| where AccountName !endswith "$"
| where InitiatingProcessFileName in~ ("setup.exe","installer.exe","update.exe")
| where FileName in~ ("powershell.exe","cmd.exe","rundll32.exe","regsvr32.exe","mshta.exe","wscript.exe","cscript.exe","wmic.exe","bitsadmin.exe")
| project Timestamp, DeviceName, AccountName, InitiatingProcessFileName, FileName, ProcessCommandLine
```

### IOC-driven hunts (use shared templates)

These are standard IOC-substitution hunts — the canonical SPL and KQL live once in [`_TEMPLATES.md`](../_TEMPLATES.md), so we don't repeat the same boilerplate on every CVE / hash / network-IOC briefing.

- **Asset exposure — vulnerability matches article CVE(s)** ([template](../_TEMPLATES.md#asset-exposure)) — phase: **recon**, confidence: **High**
  - CVE(s): `CVE-2026-47101`, `CVE-2026-47102`, `CVE-2026-40217`, `CVE-2026-42271`


## Why this matters

Severity classified as **CRIT** based on: CVE present, 9 use case(s) fired, 13 technique(s) inferred. Read the full article for actor attribution, tooling details, and any defanged IOCs in the body that aren't visible in the RSS summary.
