We Ran the Trivy Payload on an iron.sh VM. The Attacker Got Nothing

·MMatthew Slipper

We ran the real TPCP payload inside an iron.sh VM. The malware executed perfectly - and exfiltrated five proxy tokens that authenticate to nothing.

Right now, a compromised GitHub Action could run in your CI pipeline, scrape secrets from runner memory, and POST them to an attacker-controlled server. You would see a successful run in your logs. You would not see any credential theft. This happened to thousands of organizations on March 19, 2026, and most of them still don't know.

If this ran in your pipeline today, the attacker would get real credentials.

We took the actual payload TeamPCP used to compromise Aqua Security's Trivy vulnerability scanner, and ran it on an iron.sh VM running iron-sensor. The malware executed flawlessly. It scraped process memory, harvested credentials, encrypted everything, and attempted to exfiltrate.

Here's what the attacker would have received.

The Punchline

GITHUB_TOKEN=IRONSH_PROXY_agent-github-agent-token_0c2a713c
ANTHROPIC_API_KEY=IRONSH_PROXY_openclaw-anthropic_e9043de6
TELEGRAM_BOT_TOKEN=IRONSH_PROXY_openclaw-telegram_fce2a17b

Three proxy tokens that authenticate to nothing, because iron.sh VMs never contain real credentials.

When your agent makes a legitimate API call, iron.sh's secrets proxy injects the real credential outside the VM. Inside the VM, ANTHROPIC_API_KEY is an opaque token. The malware finds it, collects it, encrypts, packages it into tpcp.tar.gz, and tries to POST it to a typosquatted scan.aquasecurtiy[.]org.

Since the typosquat isn't whitelisted, the egress proxy drops the request. But even if it hadn't, the attacker gets proxy tokens that don't work outside the VM. The malware doesn't need to be blocked to be harmless.

Exfil attempt in the iron.sh console.

So the secrets were fake and the exfil was blocked. But we also got to watch the whole thing happen. iron-sensor recorded 496 events in 13 seconds. We captured every syscall, every file read, every blocked network request.

Same Payload, Two Outcomes

Typical Environmentiron.sh VM
Secrets in environmentReal GITHUB_TOKEN, real API keys, real OAuth tokensOpaque proxy tokens (IRONSH_PROXY_*)
Process memory scrapeDumps real credentials from /proc/PID/environDumps proxy tokens, nothing else exists
AWS IMDS requestReturns IAM role credentials{"error":"egress denied"}
Exfil to C2Encrypted payload reaches scan.aquasecurtiy.orgBlocked at the egress firewall
Even if exfil succeedsAttacker has your GitHub token, your API keys, your cloud credentialsAttacker has unusable proxy tokens that don't work outside the VM
DetectionPotentially a SIEM alert hours laterReal time
OutcomeRotate everything, incident response, breach disclosureRead the logs, move on

The malware runs identically in both environments. It doesn't fail in ours. It succeeds at stealing nothing.

Same attack. Completely different outcome.

What the Attack Does

The TPCP payload is not a generic info-stealer. It was built to exploit the specific trust model of CI/CD environments.

It starts by hunting for GitHub Actions runner processes. It then reads /proc/PID/environ to scrape environment variables directly from memory, bypassing any sanitization of printenv. iron-sensor flagged this at severity 0.

{
  "event_id": "01KMTYG9C22AJD4JZ0F7WBDAXH",
  "severity": 0,
  "exe": "/usr/bin/bash",
  "rule_matched": "proc_mem_access",
  "path": "/proc/3668/environ"
}

Then, a base64-encoded Python script runs a systematic credential sweep: AWS IMDS, Kubernetes secrets, GCP service account files, database connection strings, SSH keys, crypto wallets. It reads /etc/shadow. It checks your authorized_keys. Everything gets written to a single staging file.

{
  "event_id": "01KMTYG9FSKXPBJ7563XHRXXNV",
  "exe": "/usr/bin/dash",
  "argv": [
    "/bin/sh",
    "-c",
    "kubectl get secrets --all-namespaces -o json 2>/dev/null || true"
  ],
  "rule_matched": "shell_spawn"
}
{
  "event_id": "01KMTYG9FWWVWK1Q3ZSBHAC3ZQ",
  "exe": "/usr/bin/dash",
  "argv": [
    "/bin/sh",
    "-c",
    "cat $GOOGLE_APPLICATION_CREDENTIALS 2>/dev/null || true"
  ],
  "rule_matched": "shell_spawn"
}
{
  "event_id": "01KMTYGK9J6JWYCR1CY6BN3T02",
  "ts": "2026-03-28T19:25:09.81034665Z",
  "exe": "/usr/bin/grep",
  "argv": [
    "grep",
    "-rE",
    "api[_-]?key|apikey|api[_-]?secret|access[_-]?token",
    ".",
    "--include=*.env*",
    "--include=*.json",
    "--include=*.yml",
    "--include=*.yaml"
  ]
}
// And about 20 more of these

It's worth noting that some of these aren't environment variables. kubectl get secrets --all-namespaces is a network call to the K8s API that would return real secrets. On an iron.sh VM, that call hits the same default-deny egress firewall as everything else. The request never reaches the cluster.

The staging file gets encrypted with a random session key, the session key gets RSA-wrapped with TeamPCP's public key, and the bundle gets POSTed to the typosquat C2. In a CI log full of Aqua Security network traffic, the request to aquasecurtiy[.]org blends right in.

This Is Probably Your Setup

Let's be specific. If you're running AI coding agents, CI/CD pipelines, or automated tooling that touches secrets, your environment is a trusted process with real credentials and outbound network access. That's the attack surface TeamPCP used.

The TPCP payload could run in your pipeline today and you would not notice. It executes before the real Trivy scan, the scan completes normally, and the only evidence is an outbound POST to a domain that looks like it belongs to your security vendor. If you're not monitoring egress and process-level behavior on your runners you'd find out when the attacker uses your stolen credentials against you.

The question isn't whether your dependencies will be compromised. The dependency you use to check for compromised dependencies was just compromised. The question is what the attacker gets when it happens.

Get Started

Run this in your terminal:

curl -fsSL https://iron.sh/install | bash
irons onboard

That's it. You'll get a VM with proxied secrets and default-deny egress ready in 60 seconds.