Anatomy of an “LLMjacking” worm: how an exposed LiteLLM proxy got my server cryptojacked — and how to check if you’re exposed
Published June 2026. A first-hand incident writeup. Victim host details are redacted; attacker indicators (IOCs) are shared in full so others can detect and block them.
TL;DR
- I run a small fleet of services on a single Ubuntu VPS. I had three LiteLLM proxy containers exposed to the internet on
0.0.0.0:4000-4002with no authentication (noLITELLM_MASTER_KEY). - An automated worm found them, used the open proxy to get code execution as root inside the container, and dropped a cryptojacking + credential-stealing kit: an XMRig-class miner (~170% CPU), a self-spreading internet scanner, a fake-
sshdbackdoor, a watchdog, and — the modern twist — a fake MCP / JSON-RPC “credential stealer” that specifically hunts for LLM API keys (sk-ant-…,sk-proj-…, OpenAI/Anthropic/Google/Groq/OpenRouter) alongside cloud and SSH secrets. - This is “LLMjacking”: stealing AI inference credentials to resell, layered on classic cryptomining. It’s a fast-growing 2025-2026 campaign class that targets exposed AI infrastructure (LiteLLM, Ollama, Ray, ComfyUI…).
- The single root cause: an unauthenticated service on
0.0.0.0. Everything else followed from that. - This post explains the full chain, lists the IOCs, and gives you a copy-paste prompt to point a coding agent at your own box to check and fix it.
How I found it
The tell was sustained high load — a load average of 6-14 on a 4-core box, with the CPU pegged and nothing legitimate to explain it. top showed a process called .ssh-keyd-47945 burning ~172% CPU. That “ssh key daemon” does not exist. It was an XMRig-class Monero miner wearing a disguise.
The entry vector (the part that matters most)
LiteLLM Proxy gates all access behind one setting: LITELLM_MASTER_KEY. The docs are explicit that it’s optional — and if you don’t set it, the proxy requires no credential at all. Anyone who can reach the port can call your models (billed to your provider keys), read your request logs, mint API keys, and modify the running config — and LiteLLM’s config/callback system can load arbitrary Python. So an open proxy is admin-equivalent and a direct path to code execution. No CVE required (though if you’re on a vulnerable version, CVE-2026-42208 — a pre-auth SQL injection, CVSS 9.3, added to CISA’s KEV and exploited within ~36 hours of disclosure — is an even faster door).
Because the container ran with --net=host, the in-container processes bound and beaconed on the host’s public IP. --net=host shares the network namespace, not the filesystem — but combined with root-in-container and harvested credentials, that was more than enough.
The attack chain
- Initial access: worm reaches the open LiteLLM proxy on
0.0.0.0. - Execution: drops a payload into the container’s
/tmpand runs it as root. - Mining:
/tmp/.ssh-keyd-47945(XMRig-class) connects to a Monero pool (port 8029 is a known Kryptex pool port) and pins ~2 cores. - Worming: a fake
supervisord(/tmp/python3, ×6) mass-scans the internet (200+ SYN probes), pulling its payload fromhttp://<victim>:81/private/python3.6and re-spawning on each new host. - Persistence: Docker
--restart=always+ an/app/networkd“watchdog” that re-launches the miner if you kill it, plus a fake/tmp/sshdbackdoor beaconing to C2. - Credential theft (the LLMjacking part): Python scripts presented as fake MCP / JSON-RPC servers grep
/app /root /home /etc /opt /run/secrets /proc/*/environand the AWS metadata endpoint forAKIA*,sk-ant-api,sk-proj,AIzaSy,gsk_,sk-or,OPENAI_API_KEY,ANTHROPIC_API_KEY— and hide the exfil inside the MCP tooldescriptionfield (a weaponization of the documented “tool-poisoning” technique). - Anti-forensics: every binary is deleted-from-disk but still running (memory-resident), and process names masquerade as
sshd,networkd,rsyslog-hlp,supervisord.
Attribution: at the TTP level this is a confident match for the 2025-2026 cryptojacking + LLMjacking wave (closest named relatives: TeamPCP and PCPJack, both documented hitting LiteLLM and stealing Anthropic/OpenAI keys). At the exact-indicator level the infrastructure was previously unreported — so the IOCs below are likely net-new.
What the attacker could steal
Assume everything reachable by a root process for the dwell window is gone. In my case: cloud keys, AWS access keys in plaintext files, LLM provider keys across dozens of files, database passwords, app secrets (JWT/SMTP/webhook), SSH keys, and — most dangerously — crypto wallet/signing keys on disk. If you run anything with funds attached, those are top priority (sweeper bots drain leaked Solana/ETH keys in seconds).
Indicators of Compromise (block / hunt these)
C2 / mining infrastructure
120.224.114.212:5001 5.180.174.162:8029 (Kryptex Monero pool — payout endpoint)
47.76.189.208:443 47.243.49.62:80 (Aliyun-hosted C2 beacons)
107.173.13.221 94.159.100.80 46.226.165.63 213.21.233.224
206.189.246.93 134.209.128.51 81.19.141.248
C2 domain: 525423.11232347.xyz
Miner UDP listener: 0.0.0.0:34916
Worm staging URL: http://<target>:81/private/python3.6 (children run with PORT=5001)
SHA-256
9855858c22d882ce6d1eae36e0d903e2320d2949db7329cff9170ec45b2603c5 (miner)
ff2bc34b0890ee6bd6de05e1345607c7f84bef234a19c2a8b4e2a74115890e72 (dropper / fake-supervisord)
7954c1632bee352c8f599a0e529fbe3c35fedab7e9fec2a6e156abb6cf7e1769 (fake sshd backdoor)
5722b0b5cd22e161ffac38e6b8fb5a15b26820a08f47f11f9d51d996bf699405 (watchdog)
Host artifacts
/tmp/.ssh-keyd-47945 /tmp/.rsyslog-hlp-* /tmp/python3 /tmp/sshd /app/networkd
/tmp/bridge_miner_patterns /tmp/bridge_clients/client_<id>.info
fake MCP stealer scripts: _r.py _ks.py aws.py kx.py _t2.py c.py _c.py
Process tell: a (deleted) executable still running from /tmp or /dev/shm; system-daemon names (sshd, networkd) running from the wrong path.
Are YOU vulnerable? (2-minute manual check)
Run these. Any surprising answer is a problem:
# 1. What is exposed to the internet? (anything here that isn't deliberately public + authenticated is a risk)
sudo ss -tlnp | grep -vE '127.0.0.1|::1'
# 2. LiteLLM / LLM proxy reachable with NO auth? (200 = WIDE OPEN — fix now)
curl -s -o /dev/null -w '%{http_code}\n' http://<your-ip>:4000/v1/models # expect 401, not 200
# 3. Containers running dangerously?
docker ps -q | xargs -I{} docker inspect --format \
'{{.Name}} net={{.HostConfig.NetworkMode}} priv={{.HostConfig.Privileged}} user={{.Config.User}}' {}
# -> look for net=host, priv=true, or empty user (=root)
# 4. Already infected? (deleted-but-running binaries / miner names)
for p in /proc/[0-9]*; do l=$(readlink $p/exe 2>/dev/null); case "$l" in /tmp/*|/dev/shm/*|*"(deleted)"*) echo "$p $l";; esac; done
pgrep -af '\.ssh-keyd|rsyslog-hlp|/tmp/python3|networkd' | grep -v systemd-networkd
# 5. SSH wide open?
sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication'
Or just let an agent do it.
You are a defensive security engineer auditing THIS Linux host for the 2025-2026 "exposed-service →
cryptojacking + LLMjacking worm" class of compromise (the kind that hits unauthenticated LiteLLM/Ollama/
Ray proxies, exposed databases, and admin panels). This is my own server and you are authorized to inspect it.
Work in THREE phases. Do NOT change anything in Phase 1 or 2. Ask for my explicit approval before Phase 3.
=== PHASE 1 — READ-ONLY AUDIT (run these, report results) ===
1. Public attack surface: `sudo ss -tlnp | grep -vE '127.0.0.1|::1'` — list every service listening on a
non-loopback address. For each, identify the process/container and whether it REQUIRES authentication.
2. Unauthenticated services: probe each exposed HTTP service for a no-auth 200 (especially any LLM proxy —
LiteLLM `/v1/models`, Ollama `/api/tags`, etc.), exposed databases (Postgres/Redis/Mongo on 0.0.0.0 with
weak/default creds), and admin panels/dashboards. Flag anything reachable without credentials.
3. LiteLLM specifically (if present): is `LITELLM_MASTER_KEY` set? is it bound to 0.0.0.0? what image version?
(flag if no master key, or bound public, or version < 1.83.10-stable / in the 1.82.7-1.82.8 supply-chain window).
4. Container privilege: for every container, report `--net=host`, `--privileged`, `docker.sock` mounts, and
whether it runs as root. `docker ps -q | xargs -I{} docker inspect --format '{{.Name}} net={{.HostConfig.NetworkMode}} priv={{.HostConfig.Privileged}} user={{.Config.User}}' {}`
5. Active-infection check (IOCs):
- deleted-but-running exes from temp dirs:
`for p in /proc/[0-9]*; do l=$(readlink $p/exe 2>/dev/null); case "$l" in /tmp/*|/dev/shm/*|/var/tmp/*|*"(deleted)"*) echo "$p $l";; esac; done`
(NOTE: a `(deleted)` exe at a SYSTEM path like /usr/lib or /usr/bin is usually a benign post-upgrade
artifact — only treat /tmp, /dev/shm, /var/tmp paths as suspicious.)
- miner/worm process names: `pgrep -af '\.ssh-keyd|rsyslog-hlp|/tmp/python3|/tmp/sshd|networkd' | grep -v systemd-networkd`
- sustained high load with no legit cause (`uptime`, `ps -eo pcpu,comm --sort=-pcpu | head`).
- connections to known-bad infra (grep `ss -tnp` for): 120.224.114.212, 5.180.174.162, 47.76.189.208,
47.243.49.62, 107.173.13.221, 94.159.100.80, 46.226.165.63, 213.21.233.224, 206.189.246.93,
134.209.128.51, 81.19.141.248 ; domain 525423.11232347.xyz ; miner UDP port 34916 ; pool ports 5001/8029.
- these SHA-256 anywhere: 9855858c22d882ce6d1eae36e0d903e2320d2949db7329cff9170ec45b2603c5,
ff2bc34b0890ee6bd6de05e1345607c7f84bef234a19c2a8b4e2a74115890e72.
6. SSH posture: `sudo sshd -T | grep -iE 'permitrootlogin|passwordauthentication|maxauthtries'` and review
`/root/.ssh/authorized_keys` + each user's `authorized_keys` for unexpected/foreign keys.
7. Firewall: is there a default-deny inbound policy? `sudo iptables -S | head -40` (or nft/ufw status).
8. Secrets exposure: are plaintext provider keys / `.env` files readable by containers running as root?
=== PHASE 2 — REPORT ===
Summarize findings by severity (Critical/High/Medium). Explicitly state: "ACTIVE COMPROMISE SUSPECTED" if any
Phase 1.5 IOC hit, else "no active infection indicators found". List the exact fixes you propose for Phase 3.
=== PHASE 3 — FIX (only after I approve; preserve evidence; don't break running services) ===
If COMPROMISED: preserve evidence first (tar the malware artifacts, copy logs), then disable persistence and
stop the affected container BEFORE killing processes (they respawn otherwise), then recreate clean from image.
Then, regardless:
- Bind every non-public service to 127.0.0.1 (or republish `-p 127.0.0.1:PORT:PORT`); expose externally only
via an authenticated reverse proxy or SSH tunnel.
- Add authentication to any open service (e.g. set LITELLM_MASTER_KEY; strong DB passwords; panel auth).
- Re-run containers least-privilege: drop `--net=host`, run `--user` non-root, `--cap-drop=ALL`,
`--security-opt no-new-privileges`, pin image digests, upgrade vulnerable versions.
- Apply a default-deny firewall (allow only 22/80/443 + services I confirm must be public).
- Harden sshd: `PasswordAuthentication no`, `PermitRootLogin prohibit-password`, install fail2ban/CrowdSec.
- If any secret was reachable by a compromised/exposed service, tell me to ROTATE it (list which), and move
secrets out of plaintext `.env` into Docker secrets / sops-age.
Show me each command before running it. Stop and ask if anything is ambiguous or could disrupt a service.
The five rules that would have prevented this
- No unauthenticated service is ever exposed on
0.0.0.0/your public IP. Bind to127.0.0.1; reach it over an SSH tunnel or behind an authenticated reverse proxy. (This one rule prevents the whole chain.) - Run containers least-privilege: no
--net=host, no--privileged, nodocker.sockmount; non-root,--cap-drop=ALL,--security-opt no-new-privileges. - LiteLLM specifically: always set
LITELLM_MASTER_KEY, bind to localhost, keep the image upgraded (≥ 1.83.10-stable). - Default-deny firewall — only 22/80/443 public; everything else loopback/VPN.
- Treat any leaked secret as burned. Rotate from a clean machine; keep wallet keys off internet-facing hosts entirely.
I rebuilt the network posture, removed the proxy, locked down access, rotated credentials, and deployed runtime detection (Falco + auditd). But the cheapest fix was always the one I skipped: don’t put an unauthenticated service on the public internet.
Questions or want the full IOC set in a machine-readable form? Reach out. Stay patched.
