Trust Boundary Collapse: Two Campaigns That Bypassed Artifact Inspection
A technical analysis of the TeamPCP/LiteLLM supply chain compromise and the ClickFix/AmnesiaStealer macOS delivery chain
Executive Summary
Two separate campaigns demonstrated that modern security controls fail when malicious code arrives outside the expected inspection path. Both campaigns bypassed artifact inspection, which is the foundation of registry scanning, Gatekeeper, notarization, EDR file reputation, SBOM generation, and dependency vulnerability scanning.
TeamPCP poisoned the LiteLLM Python package through a compromised security scanner (Trivy). The malicious versions were live on PyPI for approximately three hours, exposing an estimated 2,500 organizations and 434,000 CI/CD pipelines. The payload harvested credentials, including Kubernetes service account tokens and cloud secrets, using a .pth file persistence mechanism that triggers on every Python interpreter start.
ClickFix delivers malware to macOS users by instructing them to paste commands into Terminal. This technique bypasses quarantine attributes, Gatekeeper, and notarization entirely because the user executes the command manually inside a trusted terminal. AmnesiaStealer uses ClickFix delivery to install a credential stealer that captures passwords, browser cookies, and cryptocurrency wallet keys. Its third stage launches a headless browser running the victim's live authenticated sessions.
Both campaigns share no infrastructure, tooling, or operators. They matter together because they expose a structural assumption shared by every mainstream security control: that malicious code arrives as an inspectable artifact at a controllable boundary before execution. Neither campaign fits that model.
Key recommendations: Pin dependencies to cryptographic hashes, not tags. Egress-allowlist CI runners. Eliminate long-lived secrets from runner environments using OIDC federation. Deploy macOS 26.4+ paste protection on non-engineering fleets. Alert on behavioral indicators like unexpected .pth file creation and bulk credential access. Treat session invalidation as a first-class step in incident response playbooks.
0. Correcting the framing
The tasking described the LiteLLM incident as "stolen CI/CD credentials allowed attackers to inject info-stealing malware." That description is true only at the last hop, and the imprecision matters operationally.
A formal Analysis of Competing Hypotheses yields two viable branches:
| Hypothesis | Mechanism | Confidence | Evidence |
|---|---|---|---|
| H1: Token harvest from compromised scanner | Trivy CI misconfiguration exposed aqua-bot PAT → attacker harvested PyPI_PUBLISH token from LiteLLM's own runner where Trivy ran as a build step → published poisoned litellm 1.82.7/1.82.8 | Moderate-to-high | Cycode has documented LiteLLM's Trivy-using pipeline inadvertently exfiltrating the PyPI publishing tokens. The attack chain is technically coherent and matches all observable artifacts. |
| H2: Initial compromise of co-founder GitHub account | Attackers gained direct access to LiteLLM co-founder/CEO GitHub account → used scoped PyPI publish token + account privileges to close issue #24512 as "not planned" and commit to unrelated repos with message "teampcp update" | Moderate | ReversingLabs assesses the PyPI compromise was likely caused by initial compromise of a GitHub account belonging to LiteLLM's co-founder and CEO. Endor Labs and THN Labs both hedge with "likely." Account-control observables (closing issues, cross-repo commits) are unexplained by H1 alone. |
Why this matters: H1 predicts that hash-pinning dependencies and egress-allowlisting runners would have prevented the incident. H2 predicts those controls would have stopped the payload but not the account takeover and its collateral damage (issue closure, cross-repo contamination). Both branches converge on the same remediation conclusion — the distinction is whether you also need GitHub account hardening and multi-party authorization for high-privilege operations.
If the story is "protect your publish tokens," you get secret rotation and two-factor authentication. If the story is "any tool your pipeline executes inherits your pipeline's secrets, and any privileged account is a pivot point," you get hash-pinned dependencies, egress control on runners, ephemeral OIDC credentials, AND multi-party authorization for high-privilege operations. Only the second framing would have prevented this incident.
1. Key judgments
- Both campaigns defeat artifact-inspection security models, by different routes. TeamPCP poisoned the artifact before it reached the registry, so scanning the registry returned clean. ClickFix delivers no artifact at all — the victim types the payload retrieval into a trusted terminal by hand, skipping quarantine, Gatekeeper, and notarization entirely. (High confidence.)
- The compromised component in the LiteLLM chain was the security scanner itself. Trivy is invoked by pipelines specifically to detect supply chain compromise, and it is granted broad read access to satisfy that mandate. That access profile makes it a near-ideal credential harvesting platform when subverted. (High confidence.)
- The "2,500 organizations / 434,000 pipelines" figure is reconstructed exposure, not confirmed compromise. CloudSEK explicitly states these numbers should not be read as proof that every listed organization was successfully compromised or that every credential was stolen. Analysts citing this figure without that caveat are propagating a scoping error. (High confidence.)
- Both campaigns prioritize live session access over static credentials. LiteLLM's payload enumerated Kubernetes service account tokens and stood up privileged pods. AmnesiaStealer's third stage drives a headless browser running the victim's authenticated sessions. Session material is not covered by password rotation and frequently survives MFA. (High confidence.)
- Detection engineering must move from "is this artifact known-bad" to "is this process behaving like a harvester." Both payloads are legible at the behavioral layer but invisible at the signature layer. Behavioral indicators include mass credential-file reads, unexpected
.pthwrites, andsecurity unlock-keychaincalls from non-Apple binaries. (Moderate-to-high confidence.) - The AI toolchain is exposed by dependency topology, not by AI-specific vulnerability. LiteLLM was caught in a campaign that was not aimed at AI infrastructure. Its blast radius was large because it sits at a credential concentration point. (Moderate confidence)
Part I — TeamPCP: The LiteLLM Supply Chain Compromise
1.1 Actor overview
TeamPCP is a financially motivated criminal cluster active from at least mid-2025. The group is tracked variously as Altered Spider (CrowdStrike), UNC6780 (Google Threat Intelligence Group), and under operator aliases PCPcat, PersyPCP, ShellForce, DeadCatx3, and CipherForce. The group operates as a hybrid: botnet operator, initial access broker, and data-leak crew, with reported brokering relationships to LAPSUS$-adjacent extortion actors.
Trend Micro applied a formal Analysis of Competing Hypotheses to attribution and assessed with moderate confidence that this is a small criminal team fluent in Western internet culture, not a state proxy. The supporting indicators are cultural rather than technical: self-branding strings in the payload, an anti-forensic timestamp set to the Back to the Future date (1985-10-26), and an English-language imageboard-register code comment. Victimology reinforces this assessment — the target set is opportunistic and credential-rich (fintech, employment platforms, e-commerce) rather than strategically selected.
Attribution caveat: The campaign included a wiper component with geographic triggers, which is inconsistent with pure financial motivation and could indicate a false flag, a multi-party operation, or an operator with independent ideological motivation. Treat single-actor attribution as moderate confidence at best.
1.2 Attack chain
The cascade is best understood as four distinct trust failures, each of which independently would have been survivable.
[Feb 27, 2026] Trivy CI misconfiguration
pull_request_target workflow exposed to fork PRs
→ aqua-bot Personal Access Token exfiltrated
│
[Mar 1, 2026] Aqua discloses, begins credential rotation
Rotation was non-atomic; attacker retained visibility
into refreshed tokens ← FAILURE POINT
│
[Mar 19, 17:43] Force-push of 76/77 tags in trivy-action
+ all tags in setup-trivy
Tag-pinned consumers silently receive malicious code
│
[Mar 20-23] Cascade: npm worm (self-propagating, ICP canister C2),
Docker Hub images pushed with separately stolen creds,
Checkmarx KICS action tags, OpenVSX extensions
│
[Mar 24, 10:39] LiteLLM CI runs compromised Trivy as a build step
PYPI_PUBLISH token harvested from runner environment
→ litellm 1.82.7 published (proxy_server.py injection)
[Mar 24, 10:52] → litellm 1.82.8 published (.pth interpreter hook added)
[Mar 24, ~13:5] PyPI quarantines both versions
The 13-minute pivot is the most analytically interesting artifact in the campaign. Version 1.82.7 injected the payload into proxy_server.py, where execution requires the module to be imported. Thirteen minutes later, version 1.82.8 retained that injection and added litellm_init.pth, a roughly 34 KB file dropped into site-packages that executes on every Python interpreter start, regardless of whether LiteLLM is imported at all.
That is not a pre-planned two-stage release. That is an operator watching their own telemetry, concluding the import-gated trigger was under-firing, and re-engineering the execution primitive live. It implies deep familiarity with CPython's startup path and a short OODA loop. Endor Labs additionally identified three payload iterations within proxy_server.py, consistent with real-time refinement.
1.3 The .pth persistence primitive
Files with a .pth extension in site-packages are processed by the site module at interpreter startup. Lines beginning with import are executed, not merely parsed as path entries. This is documented CPython behavior, intended to support namespace package registration and similar setup tasks.
CPython has considered restricting this behavior (issues #113659 and #78125). Issue #113659 was resolved narrowly: modern Python skips hidden .pth files. But the broader proposal to remove code execution from .pth handling remains open, on backwards-compatibility grounds. Volexity previously observed .pth abuse in CVE-2024-3400 exploitation, so this is not a novel technique. It is a known-accepted risk that finally got weaponized at scale.
Practical consequence for responders: On a host that installed 1.82.8, any Python process was sufficient to detonate the payload: a cron job, a pip invocation, a completely unrelated service, or even a developer's local machine. The campaign was discovered when a researcher's development laptop crashed from the fork bomb — not a CI runner. The argument "we don't use LiteLLM directly, we only pull it transitively through DSPy/CrewAI/MLflow" is not a mitigating factor. It is the same exposure.
1.4 Payload architecture
The payload is a four-layer nested structure of base64-encoded Python, with only the outermost layer touching disk as a distinct file.
| Layer | Role | Notes |
|---|---|---|
| 0 | Launcher | Single line inside the .pth; decodes L1 and spawns it via subprocess.Popen |
| 1 | Orchestrator | Embeds RSA-4096 public key, L2 and L3 blobs; handles encryption and exfiltration |
| 2 | Collector | Credential harvester, 50+ secret categories, Kubernetes lateral movement |
| 3 | Persistence | Polling backdoor written to disk as sysmon.py |
Execution gating. The payload checks for GITHUB_ACTIONS in the environment variable. This check gates the breadth of collection, not whether the payload runs — the .pth file executes at every Python interpreter start regardless of context (the campaign was discovered on a developer's laptop, not a runner). Presence of GITHUB_ACTIONS triggers the full credential sweep targeting high-density secret environments. Locale and registry checks provide geofencing.
Collection scope [Demonstrated]. The collector targets SSH private keys across every home directory and /root; AWS credential files, environment variables, and a full embedded SigV4 signing implementation used to make authenticated ListSecrets / GetSecretValue calls against Secrets Manager and DescribeParameters against SSM Parameter Store. The malware does not merely steal the key material sitting on disk. It uses those keys to pull the secrets those keys can reach.
Additional targets include IMDS v1/v2, GCP application default credentials, Azure credentials (~/.azure/), kubeconfigs and in-cluster service account tokens, Docker/npm/Vault/Git credentials, database configs, Terraform state files, Jenkins/GitLab/Drone CI files, WireGuard configs, TLS private keys, /etc/shadow, shell history, ten cryptocurrency wallet formats (heavy emphasis on Solana validator keypairs), and recursive .env variant search to depth 6.
Kubernetes lateral movement [Capability]. For each node returned by the API, the collector constructs a privileged pod manifest with hostPID, hostNetwork, and host filesystem mounted at /host. It deploys the pod to kube-system. The pod chroots into the host to write the persistence backdoor and register it as a systemd unit. Trend Micro flags this as present in code but not confirmed exploited in the wild.
Exfiltration cryptography. Collected data is encrypted with a random AES-256-CBC session key derived via PBKDF2. The session key is wrapped with a hardcoded RSA-4096 public key using OAEP padding. Both are bundled as tpcp.tar.gz for HTTPS POST. The hybrid scheme means captured network traffic cannot be decrypted by responders. You cannot determine what was taken from the PCAP alone, only that exfiltration occurred and roughly how much.
Persistence and C2. Layer 3 is written to ~/.config/sysmon/sysmon.py and registered as a user-level systemd unit named sysmon.service, described as "System Telemetry Service" with Restart=always. It sleeps 300 seconds, then polls the C2 /raw endpoint on a roughly 3,000-second interval. A returned URL that differs from the value cached in /tmp/.pg_state is downloaded, made executable, and run detached.
The kill switch. Before executing any stage-2 URL, the daemon checks whether the response contains the string youtube. If so, execution is skipped. At the time of Trend Micro's analysis the endpoint was serving a 43-byte YouTube URL. Every implanted host was globally deactivated by a single server-side edit. The design is pragmatic rather than clever: a benign-looking YouTube link passing through network monitoring raises no flags, and it lets the operator suspend a campaign without touching individual hosts. [Inferred]
1.5 Infrastructure and tradecraft notes
- Dual-domain architecture. Exfiltration goes to
models[.]litellm[.]cloud(registered approximately one day before the release, engineered to blend into legitimate LiteLLM proxy traffic). Second-stage polling goes tocheckmarx[.]zone(brand-squatting a security vendor to survive DNS allowlists built on "trusted security vendors"). - Hosting. Both C2 nodes run on AS205759, a bulletproof provider structured across a Netherlands/Ukraine jurisdictional seam to introduce abuse-reporting friction.
- JARM correlation. Identical TLS fingerprints appear across both nodes, inconsistent with commodity nginx and consistent with AdaptixC2, an open-source Go C2 framework. This is the highest-value hunting pivot: JARM fingerprinting enables passive discovery of additional team servers without prior IOC knowledge.
- Disclosure suppression. Within minutes of the GitHub issue being filed, approximately 121 compromised accounts and 196+ bot comments flooded the thread with generic praise spam. This follows the same pattern seen during the Trivy disclosure. [Inferred]
- Discovery mechanism. The campaign was found because the payload contained a fork bomb bug that pegged CPU and triggered OOM kills in production. Absent that defect, the harvester would plausibly have run silently for days. Treat unexplained resource exhaustion following a dependency update as a supply chain indicator.
1.6 Blast radius and how to read the numbers
CloudSEK's reconstruction places exposure at 2,500+ organizations and 434,000 CI/CD pipelines, with named exposure matches including Nvidia, AWS, Samsung, Salesforce, Cisco, Siemens, FedEx, Volkswagen, and LSEG. The malicious versions were live for approximately three hours.
Three analytic points:
- Three hours is not a small window in an automated ecosystem. Scheduled jobs, dependency resolvers, ephemeral runners, and cache layers copy artifacts far faster than humans revoke them. PyPI quarantined the entire project within approximately three hours according to Snyk and Phoenix Security.
- "Exposure match" does not equal "compromise." Reconstructed exposure is a hypothesis set for investigation, not a victim list.
- The correct scoping assumption is credential-scope, not package-scope. Any secret reachable by the LiteLLM process should be treated as compromised.
1.7 Detection logic for LiteLLM and TeamPCP
Highest-value host indicator: unexpected .pth creation. The original Elastic prebuilt rule (Python Path File (pth) Creation, rule id 7f65f984-5642-4291-a0a0-2bbefce4c617) was designed for a different threat model and filters out the exact event it is meant to detect — its filter_installers excludes /pip and /python writers, but litellm_init.pth shipped inside the wheel and was written by pip at install time.
The discriminating features are content and size, which are far harder to evade:
title: Suspicious .pth File in site-packages
id: 8c2f1e40-4b6a-4a2b-9f11-2ad7c0e51b93
status: experimental
description: >
Detects .pth files in site-packages that exceed normal size or contain
code-execution primitives. Legitimate .pth files are path entries or a
one-line namespace import (~100 bytes max). The litellm payload was
~34 KB of base64-encoded nested Python. Requires auditd or Sysmon-for-Linux
with file creation tracking enabled and Contents collection.
logsource:
product: linux
category: file_event
definition: >
Requires kernel audit rules logging execve + openat for write
(auditd: -a always,exit -F arch=b64 -S openat -k pth_watch)
OR Sysmon-for-Linux with file creation and content hashing enabled.
detection:
selection:
TargetFilename|contains:
- '/site-packages/'
- '/dist-packages/'
TargetFilename|endswith: '.pth'
anomaly:
- FileSize > 2048
- Contents|contains: ['exec(', 'eval(', 'base64', '__import__',
'subprocess', 'Popen']
condition: selection and anomaly
falsepositives:
- Bundled build tooling that ships nonstandard .pth files
- Editable installs (pip install -e) via nonstandard wrappers
level: high
Build-time baseline. Hash the full set of .pth files in site-packages at image build time. Alert on any deviation at runtime. This is cheap, durable, and directly covers the persistence primitive regardless of how the file was written or what process wrote it.
Behavioral rule: credential read followed by anomalous egress. A bulk credential-read alert alone fires on Trivy, EDR agents, backup daemons, cloud-init, and Ansible. The discriminating signal is a sequence: bulk credential access followed within N seconds by egress to a destination outside the runner allowlist. This requires near-zero FP because it captures intent, not just capability.
# SEQUENCE RULE — credential read → anomalous egress
# Requires network flow data correlated with file_event by PID or timestamp window.
Phase 1 — Credential read (within 60-second window):
Process reads ≥ N files matching ≥ 3 of these classes:
A. ~/.ssh/id_*, /etc/ssh/ssh_host_*_key
B. ~/.aws/credentials, ~/.config/gcloud/**, ~/.azure/**
C. ~/.kube/config, /var/run/secrets/kubernetes.io/serviceaccount/token
D. **/.env, **/.env.production, **/.env.local
E. ~/.docker/config.json, ~/.npmrc, ~/.vault-token, ~/.git-credentials
F. wallet.dat, validator-keypair.json, UTC--*, keystore/**
G. /etc/shadow
Allowlist by code-signing identity, NOT path:
Exclude processes with valid Apple Developer ID signature OR
known-good binary hash from the build baseline.
Path-based allowlists are trivially defeated by argv[0] renaming.
Thresholds split by fleet:
CI runner: N = 2 (one ~/.ssh/id_* read is already anomalous)
Dev laptop: N = 8 (full sweep threshold)
Production: N = 4
Phase 2 — Anomalous egress (within 300 seconds of Phase 1):
Outbound connection to any destination NOT in the runner allowlist.
On CI runners this means: only registry URLs, package mirrors, and
approved API endpoints. Everything else is an incident.
Condition: Phase 1 AND Phase 2
Priority: critical
Network indicators. Watch for egress from CI runners to models[.]litellm[.]cloud, litellm[.]cloud, checkmarx[.]zone, 83.142.209.11, 46.151.182.203. More usefully: alert on any runner egress to a domain registered within the last 30 days.
Triage commands for a suspected host:
# 1. Was the malicious version ever resident?
find / -name "litellm_init.pth" 2>/dev/null
pip index versions litellm 2>/dev/null
pip show litellm 2>/dev/null | grep -i version
# 2. Persistence artifacts
systemctl --user list-units 'sysmon*' 2>/dev/null
ls -la ~/.config/sysmon/ 2>/dev/null
stat /tmp/.pg_state /tmp/pglog 2>/dev/null
# 3. Any .pth in site-packages not owned by a package manager
find / -path "*/site-packages/*.pth" -mmin +0 -exec ls -la {} \; 2>/dev/null
# 4. Lockfile and cache forensics
grep -rn "1\.82\.[78]" --include="*.lock" --include="poetry.lock" \
--include="requirements*.txt" --include="uv.lock" . 2>/dev/null
ls ~/.cache/pip ~/.cache/uv 2>/dev/null
1.8 MITRE ATT&CK mapping
| Tactic | Technique | Observed |
|---|---|---|
| Initial Access | T1195.002 Compromise Software Supply Chain | Tag force-push; poisoned PyPI release |
| Initial Access | T1078 Valid Accounts | Residual aqua-bot PAT after non-atomic rotation |
| Execution | T1059.006 Python | Multi-layer base64 in-memory payloads |
| Persistence | T1543.002 Systemd Service | sysmon.service, Restart=always |
| Privilege Escalation | T1611 Escape to Host | Privileged pod + chroot to host filesystem |
| Defense Evasion | T1027 Obfuscated Files or Information | 3-layer base64; service masquerades as telemetry |
| Credential Access | T1552.001 Credentials In Files | SSH, cloud, .env, wallets |
| Credential Access | T1552.005 Cloud Instance Metadata API | IMDSv1/v2; SigV4 calls to Secrets Manager/SSM |
| Discovery | T1082 System Information Discovery | hostname, uname -a, whoami, ip addr |
| Lateral Movement | T1610 Deploy Container | Privileged pods on every cluster node |
| Exfiltration | T1041 Exfil Over C2 Channel | HTTPS POST of tpcp.tar.gz |
| Exfiltration | T1573.001 Encrypted Channel: Symmetric | AES-256-CBC session key + RSA-4096-OAEP wrap |
| C2 | T1071.001 Application Layer Protocol: Web | 50-minute polling to checkmarx[.]zone |
Part II — ClickFix and AmnesiaStealer on macOS
2.1 Why ClickFix works
ClickFix is not a vulnerability. It is a delivery technique that routes around macOS's application trust path by making the user the execution primitive.
The normal path for a downloaded application involves the quarantine extended attribute, Gatekeeper's code-signing evaluation, and notarization checks. A command typed or pasted into Terminal traverses none of those controls. There is no bundle to sign, no com.apple.quarantine xattr to evaluate, and no notarization ticket to verify. From the OS's perspective, the user has deliberately run a program. Every downstream control that assumes "malware arrives as a file" is bypassed at the front door.
2.2 The delivery layer: fingerprinting gates as a filtering system
Microsoft Threat Intelligence documented an important tradecraft shift in this ecosystem in August 2026. The infrastructure — 250+ front-end domains generated algorithmically using a file<word><word> dictionary pattern (examples: filecopperbasket, fileoceanhammer, filemarblegarden) — moved from serving the lure openly to gating it behind a server-side fingerprinting decision.
Previously, retrieving the page returned the full attack in HTML: paste instructions, clipboard logic, obfuscated shell command, encoded staging URL. Passive scanning recovered everything.
Now the domains return a roughly 2.5 KB JavaScript profiler that collects data and posts it back: navigator.platform (expecting MacIntel), user agent, language, screen size, color depth, WebGL-derived GPU strings, timezone offset, touch-event support, and anti-instrumentation counters.
The server then returns one of: the ClickFix lure (for qualifying Macs), a benign decoy, or nothing.
The defensive implication: a benign response from one of these domains does not mean the domain is benign. Per-request server-side selection means your scanner's clean verdict and your user's malicious verdict can come from the same URL in the same minute. Reputation systems built on crawler observation will systematically under-report this infrastructure.
2.3 AmnesiaStealer: stage analysis
Jamf Threat Labs disclosed AmnesiaStealer on 2026-08-13. The name derives from the "Amnesia Panel" backend and appears again in the config decryption key. The lure is a counterfeit GitHub page reproducing the dark theme, Octocat mark, and a forged "Verified Publisher" badge. The download button is replaced by a "Terminal installation" box framed as an advanced-user convenience.
The same counterfeit template has been observed delivering Atomic (AMOS) and MacSync. The lure infrastructure is a shared service across families, which means IOC-based blocking of one family's domains provides essentially no coverage against the next.
Stage 1 — Dropper
Copying the command yields a base64 blob decoding to a single command with b= (matching BUILD_ID in the payload's encrypted config) and t= (a request-identifying token) parameters. The retrieved shell script is short, silent, self-deleting, and pulls a password-protected ZIP. Password protection on the archive defeats automated static unpacking by scanning gateways.
Stage 2 — Rust collector
A Mach-O binary with an XOR-encrypted embedded configuration, changeable at build time without code modification. Config contents include C2 endpoints and a CLIPPER_ENABLED flag governing a clipboard-hijacking module targeting BTC, BCH, ETH, TRON, LTC, XMR, SOL, XRP, and ATOM.
Password capture. The binary presents a native macOS dialog styled as an Installer prompt. Entered passwords are validated locally against the directory service via dscl, looping with "Incorrect password. Please try again." until correct. Because the same password unlocks the login keychain, authorizes sudo -S, and often matches the org's SSO password, it converts one social engineering success into full local credential authority. The password is then written to disk in cleartext as pwd and ~/.pwd.
Collection. With the password in hand, the binary performs:
security unlock-keychain -pagainst the login keychain- Apple Notes read via
sudo cat - Telegram session data extraction
- Safari cookie theft
- A sweep of
~/Desktop,~/Documents,~/Downloadsfor various file types - Extraction from 16 Chromium-family browsers for Cookies, Login Data, History, Bookmarks, and more
The Safe Storage manipulation is the best detection opportunity in the whole chain. AmnesiaStealer reads Chrome's Safe Storage key from the Keychain to derive the master keys protecting profile data. On macOS 26, when normal retrieval fails, it deletes the Safe Storage entry and writes a replacement key that it already knows. Newly stored passwords and cookies then become decryptable by the attacker.
Legitimate browsers do not delete and recreate Safe Storage entries via the security command. This is a high-fidelity, near-zero-false-positive signal.
Persistence. A root LaunchDaemon impersonates Apple's crash reporting service: com.apple.ReportCrash.agent with a random numeric suffix. Detection note: the genuine Apple service is com.apple.ReportCrash. The .agent suffix plus a numeric tail is the tell.
Stage 3 — stream_module (the differentiator)
Fetched on receipt of a remote_stream command from the C2, this second Rust binary supports seven Chromium browsers. Mechanism:
- Clone the victim's browser profile and launch it headless, leaving the victim's visible browser untouched.
- Open a WebSocket relay to the operator.
- Drive the headless instance via Chrome DevTools Protocol. The operator receives a roughly 3 fps screencast and full input: keyboard, mouse, scroll, navigation.
- Call
Network.getAllCookies, which returns plaintext values because the browser has already decrypted them into memory. - Inject an anti-detection script via
Page.addScriptToEvaluateOnNewDocumentthat patches browser fingerprinting APIs.
Why this matters more than the file collection. A conventional stealer produces a static dump that ages out. Rotate the password, invalidate the sessions, and the engagement ends. This module creates a hands-on-keyboard hidden browser session running the victim's live authenticated state. Jamf verified the control channel against a purpose-built test relay. [Capability] — proven functional in a lab, but not confirmed exploited in the wild.
2.4 Detection logic for ClickFix and AmnesiaStealer
Process-lineage detection is the strongest control.
Priority 1 — Terminal ancestry
Alert: Terminal.app / iTerm2 / kitty / WezTerm spawning
curl | base64 | gunzip | osascript | xattr
within N seconds of foreground browser activity.
Priority 2 — Native-tool abuse chains
Alert: curl piped directly to sh/zsh/bash
base64 -d immediately followed by chmod +x
xattr -c preceding execution
Priority 3 — Credential store access (highest fidelity)
Alert: `security unlock-keychain` invoked by a non-Apple-signed binary
Alert: `security delete-generic-password` targeting a
"* Safe Storage" entry ← near-zero FP
Alert: `dscl . -authonly` from an unsigned or ad-hoc-signed process
Priority 4 — Persistence
Alert: LaunchDaemon plist matching ^com\.apple\..*\.agent\.?[0-9]*$
that is not Apple-signed and not on the golden baseline.
Priority 5 — Stage 3 specific
Alert: Chromium-family binary launched with --headless AND
--user-data-dir pointing outside the standard profile path
Alert: Local listener on a CDP port (9222+) with outbound WebSocket
to a non-corporate endpoint
Preventive controls that actually move the needle:
- macOS 26.4+ paste protection. Apple shipped a mitigation that blocks pasting suspicious commands into Terminal with an explicit scam warning. This is the single highest-leverage control available and it is a version bump.
- User education with the right message: no legitimate software download, CAPTCHA, or verification step ever requires pasting a command into Terminal.
- Session-layer controls. Short session lifetimes, token binding where available, continuous session risk evaluation.
Part III — Convergence analysis
The two campaigns share no infrastructure, no tooling, and probably no operators. They are worth analyzing together because they exploit the same structural assumption from opposite ends.
The assumption: malicious code arrives as an inspectable artifact, at a controllable boundary, before it executes.
Every mainstream control inherits this assumption. Registry scanning, Gatekeeper, notarization, EDR file reputation, SBOM generation, dependency vulnerability scanning: all assume there is a thing to look at, and a moment at which to look at it.
TeamPCP poisoned the artifact upstream of the inspection point. ClickFix eliminates the artifact entirely.
Four shared consequences:
- The trust boundary moved and the controls did not follow. In the LiteLLM chain the boundary is build-time execution. In the ClickFix chain the boundary is the user's hands.
- Session material outranks credentials. Both campaigns target live session access, not static credentials. Incident response that stops at "rotate credentials" leaves the actual access intact.
- The response window is compressed below human reaction time. Three hours of PyPI availability produced 434,000 pipeline exposures because automated systems copy faster than humans revoke. Controls that require a human in the loop arrive after the harvesting is complete.
- Security tooling is now a first-class target. Trivy and Checkmarx KICS are tools organizations deploy to prevent exactly this. Assume your security tooling has the same access as your deployment tooling.
Part IV — Defensive architecture
Ordered by leverage, not by ease.
CI/CD
- Pin dependencies to cryptographic hashes, not tags or versions. Use
pip install --require-hashesoruv pip install --require-hashes. For GitHub Actions, pin to commit SHA, never to tag. - Impose a quarantine window on new releases. Let someone else's infrastructure absorb the first 72 hours.
- Egress-allowlist your runners. Build traffic goes to a small, stable set of registries. Anything else is an incident.
- Eliminate long-lived secrets from runner environments. OIDC federation with short-lived, narrowly scoped tokens.
- Enable cosign/Sigstore verification. Verify signatures at pull time rather than trusting the registry.
- Treat rotation as a transaction. Revoke first, cut over from a clean plane, verify no residual sessions.
Endpoint
- Ship macOS 26.4+, prioritizing non-engineering fleets where the paste protections have the best benefit-to-friction ratio.
- Deploy behavioral detection for bulk credential access rather than relying on signatures against payloads that were rewritten three times in one morning.
- Alert on .pth writes outside a known-good baseline. Cheap, durable, and directly covers the persistence primitive.
- Segment terminal-lineage rules by fleet. One rule with one threshold across the whole fleet will be turned off within a week.
Program
- Audit transitive depth. Use
pipdeptreeoruv pip tree. Enumerate what your AI tooling actually pulls in. - Treat LLM API keys as crown jewels. A proxy gateway concentrates keys for every provider it fronts.
- Add resource exhaustion to your supply chain indicator set. Unexplained CPU or OOM behavior immediately following a dependency update deserves a security review.
- Rehearse the session-invalidation path. Most IR playbooks have a credential rotation runbook and no session revocation runbook.
Appendix A — Indicators of compromise
TeamPCP / LiteLLM
| Indicator | Type | Notes |
|---|---|---|
litellm 1.82.7, 1.82.8 | Package version | PyPI, yanked ~3 hours after publication |
litellm_init.pth | Filename | Roughly 34,628 bytes in site-packages |
~/.config/sysmon/sysmon.py | File path | Layer 3 backdoor |
sysmon.service | systemd unit | "System Telemetry Service", Restart=always |
/tmp/pglog, /tmp/.pg_state | File paths | Stage-2 drop and dedup state |
tpcp.tar.gz | Filename | Exfiltration bundle |
models[.]litellm[.]cloud | Domain | Exfiltration endpoint |
checkmarx[.]zone | Domain | Stage-2 polling, /raw endpoint |
46.151.182.203 | IPv4 | litellm.cloud node, AS205759 |
83.142.209.11 | IPv4 | checkmarx.zone node, AS205759 |
AmnesiaStealer / ClickFix
| Indicator | Type | Notes |
|---|---|---|
github.aoitour[.]com | Domain | Counterfeit GitHub lure |
debug.allllowef[.]space/send/ | URL | AmnesiaStealer C2 |
4mn3s1a_2o26!xK | String | Config decryption key |
com.apple.ReportCrash.agent[0-9]* | Label | Impersonates Apple crash reporter |
~/.pwd | File path | Cleartext captured password |
mode: "php" | JS artifact | Fingerprint submission parameter |
file<word><word> | Pattern | Domain generation convention |
Appendix B — Intelligence gaps
- No public confirmation that TeamPCP successfully used LiteLLM-stolen credentials. Collection and exfiltration are demonstrated. Downstream exploitation specifically traceable to the LiteLLM harvest is not publicly confirmed.
- K8s lateral movement is capability-present, not confirmed-exploited. This materially affects breach scoping and notification obligations.
- The kill switch trigger reason is unknown. Analysis detection, harvest completion, and forensic hygiene are all consistent with observation.
- AmnesiaStealer stage 3 is lab-verified, not victim-verified.
- ClickFix campaign scale is undisclosed.
- CloudSEK's exposure methodology is not fully public.
- Bot suppression attribution is unresolved.
- TeamPCP attribution is moderate confidence at best, complicated by a geographically-triggered wiper component inconsistent with pure financial motivation.
Appendix C — Primary sources
LiteLLM / TeamPCP
- Trend Micro (ZDI Threat Hunting) — payload analysis, infrastructure, ACH attribution framework
- CloudSEK — exposure reconstruction, affected-organization list
- Palo Alto Unit 42 — Trivy compromise mechanics, actor overview
- Wiz Research — initial disclosure, environment audit guidance
- Kudelski Security — comparative analysis of two campaign variants
- SANS Institute — campaign synthesis, MITRE mapping
- Cloud Security Alliance Labs — TeamPCP actor profile
- SafeDep, Legit Security, Cycode, Endor Labs, Snyk, Aikido, StepSecurity — supporting technical reporting
- ramimac.me/teampcp — maintained campaign timeline and IOC aggregation
- LiteLLM security advisory,
docs.litellm.ai/blog/security-update-march-2026 - GitHub issue BerriAI/litellm#24512 — original disclosure thread
ClickFix / AmnesiaStealer
- Jamf Threat Labs — primary AmnesiaStealer analysis (Thijs Xhaflaire)
- Microsoft Threat Intelligence — fingerprinting gate analysis, detection coverage, IOCs
- Recorded Future Insikt Group — ClickFix cluster analysis and forecasting
- Netskope Threat Labs — AppleScript stealer and RAT variants
- Group-IB — ClickLock Stealer (overlapping password-capture behavior)
- Apple support documentation — macOS 26.4 Terminal paste mitigation
Prepared as an independent open-source analysis. No first-party telemetry, no vendor sponsorship. Corrections welcome, particularly on the detection calibration figures, which are reasoned estimates rather than measured false-positive rates.