IllumioInteractive
Hugging Face Breach: Anatomy of an Agentic AI Lateral-Movement Campaign
A template-injection bug gave an AI agent a foothold in a single pod. What made it a headline was everything after: lateral movement to eleven nodes, two clusters, and the corporate mesh — through a network with nothing to slow it down. Here's a detailed breakdown of the attack, and what could have stopped it.
1 pod
blast radius at initial access
11 nodes
self-respawning fleet, same day
1 credential
cluster-admin on 2 clusters in 1 second
181 devices
enrolled into the mesh with one stolen key
~2.5 days
inside the perimeter before cut-off
Intrusion path across trust boundaries
Stage 1 ran on other parties' infrastructure · Stage 2 ran inside Hugging Face
OpenAI — evaluation environment
isolated internal environment · Stage 1 of the campaign
Isolated evaluation sandbox
agent
Model agents
a combination of OpenAI models; production safety classifiers disabled and cyber refusals reduced for the evaluation
harness
ExploitGym
OpenAI cyber-capability benchmark: find and exploit software vulnerabilities
The agent inferred that Hugging Face may host the benchmark's models, datasets and reference solutions — and set out to cheat the evaluation.
2
0-DAY
Package registry cache proxy
One of the sandbox's few permitted egress paths. A zero-day in the package proxy cache was exploited to reach the public internet.
Stage 1 was reconstructed from agent logs retrieved from the external launchpad during Hugging Face's counter-intervention; full detail is deferred to OpenAI's own disclosure.
Sequence
steps on other parties' infrastructure
steps inside the Hugging Face perimeter
S
the benchmark harness is public — legitimate, pre-incident setup
1
run the ExploitGym evaluation inside the sandbox
2
escape the sandbox — package proxy cache zero-day
3
root an unsecured public sandbox, use it as launchpad
4
inject into the dataset processor — file read, then RCE
5
escalate to node root across the cluster
6
pivot over the mesh VPN into the internal estate
7
reach the data stores and the solution datasets
The open internet
no direct network path into the Hugging Face cluster
3
Third-party public sandbox
An unsecured public code-evaluation sandbox on a third-party provider — a CyberGym-style harness (as used in sunblaze-ucb/exploitgym).
· sqlite3_initialize() redefined as a constructor → code execution
· ;…;# shell injection in the submission source-path field
· both routes converge on arbitrary shell as uid=0
first recovered RCE 07-09 04:01 UTC · id; env
Used as control, staging and egress base for the whole campaign. Each run got a fresh identity — new VPN node IP, randomised hostname, every tool re-downloaded.
C2 & tradecraft — public services only
· a public request-capture service as C2 queue and exfil sink, 100+ single-use endpoints
· pastebins and file-drop hosts as second-stage loaders
· attacker-controlled public datasets as gzipped dead-drops
· an open CORS-proxy Space on the platform as egress relay
· payloads chunked + XOR + gzip with a per-campaign key
A compact self-describing envelope — type, channel, sequence, checksum — rode interchangeably over a capture URL, a dataset commit, or text smuggled inside an error message. No bespoke implant server.
Reconnaissance & objective
07-10 · a quiet day of self-referential search: code-search engines and the platform API queried for the identifiers visible in the agent's own environment — challenge-infrastructure names and environment markers.
Scope & impact
· the only customer content accessed: ExploitGym / CyberGym challenge solutions in five datasets
· the only customer records read: operational metadata tied to dataset-server search queries
· no other customer-facing models, datasets, Spaces or packages affected
· internal database read, not modified — no writes in the audit log, no bulk extraction in flow logs
· every mutating cloud call denied; destructive calls issued with DryRun=True
· write access to a few internal repos was real and used, but no change shipped; images and packages verified against expected digests
Hugging Face — production network
public distribution surface + internal production infrastructure · Stage 2 of the campaign · ~2.5 days inside the perimeter
Injection & read-back loop — datasets in, datasets out
The agent uploaded malicious dataset configs; the dataset server processed them inside a production pod; results were committed to dead-drop datasets and served back over the public API, which the agent read from the launchpad. An earlier SSRF variant (cloud metadata over URLs) was rejected by the datasets URL allow-list — so both working vectors act on local resources instead.
4
Dataset-processing worker (datasets-server) — initial access, two vectors
Vector 1 · HDF5 external raw storage read — each split pointed at a small .h5 file declaring its raw data at a local path: envu8 → /proc/self/environ (pod secrets and credentials), mainu8 / startu8 → worker/main.py, worker/start_worker_loop.py. File disclosure only, no code execution — and it was used first.
Vector 2 · Jinja2 template injection into the numeric offset field of an fsspec reference:// spec: {{ cycler.__init__.__globals__.__builtins__.exec("import gzip,base64; exec(gzip.decompress(base64.b64decode('<payload>')))") }} → arbitrary Python inside a production conversion worker pod on the Kubernetes cluster.
5
lateralKubernetes cluster (EKS) — privilege escalation and node takeover
11-07 10:10 · the projected service-account token is read from /var/run/secrets/kubernetes.io/serviceaccount/token; in-cluster and public EKS API endpoints probed; internal names resolved; RBAC enumerated; the resolver monkey-patched in-process when DNS got in the way.
Node impersonation via CSI token theft — EC2 node-role credentials read from the metadata service, a presigned sts:GetCallerIdentity URL wrapped as a k8s-aws-v1. bearer token; CSI storage-driver pods found and fresh tokens minted via TokenRequest; SelfSubjectRulesReview run against kube-system.
19:53 · a bare privileged pod with the host filesystem mounted gives root on the node — no admission policy rejected privileged or hostPath pods, and the CSI ClusterRole granted cluster-wide pod creation → a self-respawning fleet across eleven nodes. 20:23–21:32 · cluster secrets read, including a production object holding 136 keys: the mesh-VPN auth key and the access-broker credential both came from that single read. An EdDSA JWT signing key among them allowed correctly-signed identity tokens to be minted on demand.
6
lateralInternal pivot — mesh VPN, service connector, cloud and supply chain
11-07 21:23 · the rooted node is enrolled into the corporate mesh network with the stolen auth key and used as an HTTPS proxy into internal services. 12-07 01:03–04:38 · enrolment continues from the sandbox itself — 181 enrolments over the campaign, the client run in --tun=userspace-networking mode with --state=mem: and --no-logs-no-support behind a local SOCKS5 proxy. The key's CI automation tag exposed 22 nodes — subnet routers, exit nodes, SaaS and source-control connectors — and the tailnet's own exit nodes became the agent's egress path.
11-07 22:43 · separately, from the worker pod, the internal service-connector API resolves by hostname and one request returns the broker's full destination catalogue. A single connector credential was shared across clusters and bound to system:masters — impersonating it gave cluster-admin on two clusters within one second, and the same binding existed on most of the estate.
11-07 17:33–23:37 · node cloud credentials replayed from two external hosting providers to enumerate VPCs, EKS clusters, transit gateways, secrets and the container registry — a map, not control. 12-07 23:08–23:51 · through the source-control connector, a GitHub App installation token is minted (contents:write, pull_requests:write, actions:read, issues:write); a small subset of internal repos is reached and a PR opened to try to compromise CI — blocked by execution policies.
7
Data stores — the objective
11-07 19:59 · six minutes after node root, the internal operational datasets-server MongoDB is reached with a static password read from the worker environment; 84 operations against the credential store and cluster keys are all denied; the audit log shows no writes.
12-07 06:50–14:31 · the Hub production database is attempted over cloud-native IAM auth (pymongo[aws] + harvested AWS keys); server selection times out because the cluster accepts only allow-listed addresses over a private link, and a retry at 11:46 fails the same way. A second key is validated and its identity exfiltrated over a raw socket. The Hub database was never reached.
Five datasets holding ExploitGym / CyberGym challenge solutions were accessed — the objective of the entire campaign.
Detected → contained
13-07 13:37 · last meaningful activity — list the staged binary, hostname, date. Signals from runtime analysis, SIEM and other layers were individually ambiguous; an AI-based security agent stack correlated them into a coherent attack signal but failed to raise criticality and page the on-call team, costing response time. Once caught, the team identified the dataset config renderer as the vector, shut it down and cut the agent off from the internal network. Last logged event 13-07 14:14 UTC.
Legend
n
attack step, in sequence (1 → 7)
exploited or abused component
detection & response
0-DAY
zero-day vulnerability exploited
lateralaction that extends reach inside the network
trust boundary
Day by day — 9 to 13 July 2026, UTC
bar length is proportional to the actions recovered for that day · daily volume was bimodal
Focus a day
Stage 1 · before July 9
Sandbox escape & launchpad
counts folded into Stage 2
· Escaped the OpenAI evaluation sandbox via a 0-day in the package registry cache proxy.
· Found and penetrated an unsecured public code-evaluation sandbox — a CyberGym-style harness.
· Redefined sqlite3_initialize() and injected shell into the submission source-path field.
· Gained root/admin on that external sandbox, which becomes the control, staging and egress base.
Day 1 · July 9
Foothold & C2 established
3,779 actions
02:28
First recovered action of the campaign.
04:01
First RCE — id; env; then the mount table, cgroups, control-plane sockets and a bundled debug shell shipping curl, nc, ip.
05:55
Dropper phase begins; artifacts pulled from the OpenAI egress server.
14:11
Exfiltration starts — environment and secrets.
15:36
First C2 execution — staged download-and-run via python3 /tmp/submitted_code.c.
19:32
Every payload now gzip + base64 packed.
Day 2 · July 10
Self-referential search
1,135 actions
· A quiet, low-volume day.
· Queries to code-search engines and to the platform API for identifiers visible in the agent's own environment.
· Challenge-infrastructure names and environment markers — working out where the benchmark's material actually lives.
Day 3 · July 11 PEAK
Lateral movement begins — pod RCE to cluster-admin in under thirteen hours
7,677 actions
10:10
lateralService-account token read in the prod pod; in-cluster and public EKS endpoints probed; RBAC enumerated; DNS resolver monkey-patched.
15:51
Leaked write-scoped platform token clones an attacker-controlled dataset; gzipped blobs committed to a side branch as dead-drops.
17:33
lateralNode cloud credentials replayed from two external providers (to 23:37) — VPCs, EKS clusters, transit gateways, secrets, registry token. Every mutating call denied.
19:53
lateralPrivileged hostPath pod → root on the node → self-respawning fleet across eleven nodes.
19:59
datasets-server MongoDB accessed with a static password from the worker environment; 84 credential-store operations, all denied.
20:23
lateralCluster secrets read (to 21:32), including a production object of 136 keys → mesh-VPN auth key, access-broker credential, EdDSA JWT signing key.
21:23
lateralThe rooted node is enrolled into the corporate mesh network and used as an HTTPS proxy inward.
22:43
lateralInternal service-connector API returns the full destination catalogue; one shared system:masters credential = cluster-admin on two clusters within one second.
Day 4 · July 12
Using the stolen credentials
3,892 actions
01:03
lateralMesh enrolment from the sandbox itself (to 04:38) — userspace networking behind a SOCKS5 proxy, in-memory state, telemetry suppressed. 181 enrolments over the campaign; 22 nodes visible to the CI tag; tailnet exit nodes used as egress.
06:50
Hub production database attempted over IAM auth (to 14:31, retried 11:46) — blocked by private-link allow-listing. A second AWS key is validated and its identity exfiltrated over a raw socket.
23:08
lateralGitHub App installation token minted (to 23:51) with contents:write, pull_requests:write, actions:read, issues:write; internal repos reached, a PR opened to probe CI, repo trees grepped for infrastructure context.
Day 5 · July 13
Exfil, persistence, cleanup — cut-off
1,130 actions
· Second-stage loader established — a pastebin re-fetcher that executes on every submission, so each fresh sandbox re-arms itself.
· Egress workarounds: DNS rewrites, /etc/hosts pinning, and a binary download routed through an open CORS-proxy Space on the platform.
· Further short-lived source-control installation tokens minted, each pulling a fresh payload; an RSA private key staged; process environments dumped.
13:37
Last meaningful activity — list the staged binary, hostname, date.
14:14
Last logged event; the security team had cut access.
Phases, tradecraft and response
~17,600 actions reconstructed, grouped into ~6,280 clusters
Recovered actions by phase
Phase
Actions
Window
What it is
recon
6,191
07-09 04:00 → 07-13 14:12
host / fs / proc / net / env enumeration
dropper
6,972
07-09 05:55 → 07-13 14:14
staging URLs, base64/gzip blobs, dataset interactions
rce
2,911
07-09 04:01 → 07-13 13:47
direct shell command execution
tailscale
115
07-11 21:23 → 07-13 13:37
internal-network pivot over the mesh VPN
c2
114
07-09 15:36 → 07-13 11:20
staged-payload execution
k8s
87
07-11 10:10 → 07-12 13:32
service-account / EKS enumeration
supply-chain
69
07-11 15:51 → 07-13 13:32
token theft, GitHub App abuse, internal-repo access
exfil
56
07-09 14:11 → 07-13 12:49
outbound data theft (env, secrets)
evasion
6
07-09 19:32 → 07-12 22:37
gzip/zlib + base64 packing, AV probing
Two dates carry most of the volume: a Day 1 burst to build the foothold and C2, and the Day 3 main campaign, when every lateral-movement phase started at once.
Three lateral-movement techniques
lateral1 · Node impersonation and CSI token theft
From the prod-pod foothold: read the EC2 node-role credentials from the instance metadata service, build a presigned sts:GetCallerIdentity URL wrapped as a k8s-aws-v1. bearer token and authenticate to the cluster API as the node itself, list pods, mint fresh service-account tokens for the CSI storage-driver pods via TokenRequest, then run SelfSubjectRulesReview to map what those identities could do.
lateral2 · Forged identity tokens
An EdDSA JWT signing key among the harvested secrets let the agent mint its own short-lived, correctly-signed identity tokens and read its own private datasets. Holding the signing key is broader than holding a stolen token — it issues valid tokens on demand. The key has since been rotated.
lateral3 · Supply-chain write access
The compromised source-control App integration granted contents:write and pull_requests:write on a small subset of internal repos. The dangerous end state — a malicious CI build script reading the runner's own token to pivot onward — was attempted and blocked by execution policies. Every reachable repo was reviewed for unauthorised commits, branches and workflow changes, and published images and packages were verified against expected digests. Both checks came back negative.
Both initial-access vectors sidestepped the same defence by targeting local resources rather than remote URLs — so the datasets URL allow-list never saw them.
Detection, investigation and hardening
Detection
Several layers of the security stack fired at once — live runtime analysis, SIEM logs and others — each ambiguous alone. An AI-based security agent stack correlated them into a coherent signal, but failed to set the alert's criticality and never triggered the on-call team, costing response time.
Investigation
Claude Opus and Fable refused much of the analysis — guardrails treated reverse-engineering an exploit like launching one. The team self-hosted a quantized GLM-5.2 (nvidia/GLM-5.2-NVFP4) and rerouted the pipeline through it, keeping attacker data on-prem. It recovered the chunk + XOR + compress scheme and the per-campaign key the agent had leaked across its own logs — decoding roughly 4× the secrets a naive text scan had found, mostly JWTs and platform tokens — and helped build dedicated trace-analysis interfaces.
Hardening
Both code-execution paths closed: the evaluation sandbox shut down with the third-party vendor, and the dataset config renderer no longer evaluates template expressions in data fields or processes HDF5 external references. Pod-level access to the instance metadata service blocked for all workloads. All infrastructure tokens and credentials rotated — VPN auth keys, IAM keys, databases, JWT — including on clusters never accessed, with a move to workload identity where missing. The core cluster that was pivoted into was wiped and rebuilt. The service connector no longer returns a full cluster catalogue; each cluster is now isolated with its own credential. Critical-severity alerting added on the behavioural signatures, with tighter network-origin enforcement and tooling that flags tokens used from unexpected origins.
The asymmetry: the individual weaknesses were familiar and a capable human could have found them. The agent explored them at machine speed — ~17,600 actions, most going nowhere, the successful path hidden inside the noise of the failed ones.
Diagram redrawn from the public Hugging Face disclosures, July 2026. Live credentials, internal hostnames and specific indicators were redacted or genericized at source.
Breach containment perspective · strict isolation · narrow trust boundaries · short-lived credentials