Ossprey Security obtained and analyzed the complete source release of Miasma, a self-propagating multi-ecosystem credential-stealing worm published under the npm package name voicefromtheouterworld. The README self-attributes it to TeamPCP, but we have no independent evidence confirming that group authored or published it. We examined every source file in the release and corroborated our findings against contemporaneous incident reporting covering Red Hat's @redhat-cloud-services npm compromise, the Microsoft Azure GitHub organization takedown, and prior TanStack/Mistral AI campaign waves.
Miasma requires only a single GitHub PAT to operate. Given one token, it harvests credentials from the local filesystem, cloud provider APIs, process memory, and password managers; encrypts and exfiltrates everything; and propagates across every connected ecosystem: npm, PyPI, RubyGems, GitHub repos and Actions, SSH hosts, AWS EC2 via SSM, and JFrog Artifactory. It targets 13 AI coding tools - including Claude Code, Gemini CLI, and Cursor - injecting persistent execution hooks that fire whenever a developer opens a project or starts a session. It requires no dedicated infrastructure of its own: stolen PATs and GitHub's own commit search are sufficient for both exfiltration and command and control.
Open-source release caveat: The primary C2 transport (DomainSender, which POSTs to api.anthropic.com/v1/api) is explicitly commented out of the open-source release per the README: "Domain sending (DomainSender) is currently stripped from the sender chain in the open-source release. It can be easily added back - the code lives in src/sender/domain/." In the shipped OSS build, GitHub public repository commits are the only active exfiltration path. The configuration, code, and design intent for the api.anthropic.com C2 are fully present; only the sender chain hook is commented out. Operator/private builds are expected to have it re-enabled.
Who is Behind This?
The README bundled with this release credits 'TeamPCP' by name, but self-attribution in a public release is not reliable evidence of authorship - it could reflect a copycat actor claiming affiliation or deliberate misdirection. External vendor reporting from ReversingLabs, Wiz, Unit 42, and Tenable tracks the TeamPCP cluster under aliases including Replicating Marauder, TGR-CRI-1135, UNC6780, DeadCatx3, PCPcat, ShellForce, and CipherForce. We have tracked this cluster's earlier campaigns in depth, including the Trivy supply chain attack and the complete 83-day TeamPCP campaign timeline. The technical tradecraft - per-build self-extracting payloads, multi-ecosystem token abuse, and commit-based C2 - overlaps with previously documented Mini Shai-Hulud samples associated with that cluster, but code overlap alone does not confirm authorship. Our working assessment is that this worm was written by a group inspired by TeamPCP's methodology rather than TeamPCP itself. Attribution should not be confirmed without C2 infrastructure correlation or additional forensic evidence.
Technical Breakdown
Stage 0: Preflight and Anti-Analysis
We found a comprehensive preflight sequence in src/orchestrator/preflight.ts, src/utils/checkSandbox.ts, and src/utils/config.ts that runs before any credential collection begins.
Decoy detection checks multiple environment variables for known sandbox signatures and exits silently on a match: GITHUB_TOKEN starting with ghp_decoyGitHubToken, NPM_TOKEN starting with npm_F4k3NPMToken, ANTHROPIC_API_KEY starting with sk-ant-api03-fake, AWS_ACCESS_KEY_ID starting with AKIAFAKE, and GITHUB_REPOSITORY equal to actions-security-demo/compromised-packages. Additional exits trigger if the CWD starts with /tmp/npm-safe/ or /opt/hscan-supplychain-dynamic, if the hostname contains sandbox, or if ~/.aws/credentials contains known AWS documentation example keys. A Russian locale exit checks Intl.DateTimeFormat().resolvedOptions().locale, LC_ALL, LC_MESSAGES, LANGUAGE, and LANG, exiting immediately if any starts with ru.
EDR detection runs ps aux or tasklist against 17 process names and checks 4 installation paths. Covered products include CrowdStrike Falcon (falcon-sensor, falcond, csfalcon), SentinelOne, Microsoft Defender for Endpoint (mdatp, wdavdaemon), Carbon Black, Cylance, Trend Micro, Tanium, and osquery.
When not running inside CI, the worm forks as a background daemon and exits the parent - it detects 30+ CI systems including GitHub Actions, GitLab CI, CircleCI, Jenkins, Azure Pipelines, Buildkite, Vercel, and Cloudflare Pages. It then writes a PID lock file and exits if another instance is already running. SIGINT and SIGTERM handlers are replaced with no-ops, making the process immune to standard termination signals.
An undocumented OIDC fast-path (checkTargetRepo()) - not present in ARCHITECTURE.MD - checks whether the worm is running inside a GitHub Actions workflow where WORKFLOW_ID and REPO_ID_SUFFIX env vars match an expected pattern. If so, it immediately executes NPMOidcClient to publish trojanized packages using the runner's OIDC token and exits, bypassing the rest of the kill chain. This allows the worm to self-trigger directly inside OIDC-capable npm-publish runners.
The StepSecurity Harden-Runner bypass is layered across three paths: if the Docker socket is accessible, it kills any container whose name or image contains harden-runner, step-security, or stepsecurity. If passwordless sudo is available, it overwrites /etc/resolv.conf with public DNS servers or appends 127.0.0.1 stepsecurity.io (and related domains) to /etc/hosts. If no direct sudo but Docker socket access exists, it spawns a privileged Alpine container bind-mounted at /etc/sudoers.d/ to grant the runner passwordless sudo, then uses that sudo for the DNS operations.
Stage 1: Quick Harvest and Inline C2 Check-In
We found three credential providers running synchronously before the exfiltration sender chain is built, so stolen tokens can immediately seed the transport layer.
FileSystemService glob-scans 100+ hardcoded hotspot paths. Beyond cloud credentials and SSH keys, notable targets include Claude AI configuration files (~/.claude.json, ~/.claude/*, ~/.claude/projects/*, ~/.claude/mcp.json), shell history for bash, zsh, Python, MySQL, and PostgreSQL, Signal configuration, Slack cookies, Telegram Desktop data, Windows VPN configurations (NordVPN, ProtonVPN, CyberGhost, PIA, Windscribe, OpenVPN), and Monero wallet files. Binary files are base64-encoded before exfiltration; files over 10 MB are skipped. The provider is entirely skipped if hostile EDR is detected.
ShellService runs gh auth token to extract any GitHub CLI credential, captures the full process.env block - which in CI environments frequently contains injected tokens that never appear as files - and records hostname and username.
GitHubRunner reads /proc/<pid>/maps and /proc/<pid>/mem of any Runner.Worker process when running inside GitHub Actions, extracting secrets injected into runner memory that never touch disk or environment variables. An equivalent PowerShell-based dump runs on Windows Actions runners.
After the GitHub sender is initialized, an undocumented C2 check-in runs inline (src/c2/checkin.ts - not mentioned in ARCHITECTURE.MD). It searches GitHub commits for TheBeautifulSandsOfTime, verifies each matching commit's RSA-PSS-SHA256 signature against the embedded verify_key, and eval()s the decoded payload as JavaScript. This gives the operator arbitrary code execution on any victim that runs the worm after the signed commit is posted, with no file written to disk. This is distinct from the persistent GITHUB_MONITOR.py background process - that executes Python; this check-in executes JavaScript inline during the worm's main flow.
Stage 2: Exfiltration and C2
We traced all collected credential bundles through a single encryption pipeline in src/sender/base.ts: JSON-serialized results are gzip-compressed, encrypted with AES-256-GCM using a fresh random 32-byte key and 12-byte IV per envelope, and the AES key is wrapped with RSA-OAEP-SHA256 against the operator's 4096-bit enc_key.pub. The operator decrypts with the matching private key via scripts/decrypt.ts. Payloads over 30 MB are split into chunks with 5 retries and exponential backoff.
The sender chain attempts transports in priority order. The primary transport is DomainSender, which POSTs to https://api.anthropic.com/v1/api. The path /v1/api does not exist on the real Anthropic API; real API calls go to /v1/messages, /v1/complete, and similar paths. In CI/CD environments that allowlist AI API endpoints, this traffic would be indistinguishable from a legitimate API call at the network layer. This transport is stripped from the open-source release - the sender chain hook in src/orchestrator/senders.ts is commented out. Operator builds re-enable it trivially.
The fallback GitHubSender searches GitHub for commits containing AES-256-CBC-encrypted PATs disguised as fine-grained PAT strings (github_pat_11A + 92 additional characters) alongside the search string DontRevokeOrItGoesBoom. These are decrypted with the hardcoded TOKEN_AES_KEY, validated, and used to create public exfiltration repositories with Greek underworld-themed names using a fixed wordlist (stygian, tartarean, erebean, infernal, chthonic, acheronian, lethean, plutonian, abyssal, charonian, thanatic, funereal, nekyian, sepulchral, tenebrous, cimmerian combined with cerberus, charon, tartarus, erebus, asphodel, acheron, styx, lethe, cocytus, phlegethon, shade, eidolon, wraith, thanatos, hecate, persephone). Every created repo has the description "Hades * The End for the Damned". Encrypted blobs are committed to a results/ directory. This is the only active exfiltration path in the open-source build.
If the primary C2 domain fails its health check, the worm searches GitHub commits for messages matching firedalazer <url_base64>.<signature_base64>, verifies the signature against verify_key.pub with RSA-PSS-SHA256, and uses the decoded URL as the new C2 endpoint - allowing the operator to rotate C2 infrastructure without touching deployed binaries.
Stage 3: Deep Harvest
We found credential collection running through parallel cloud provider tracks simultaneously. All providers run via Promise.allSettled; token validation triggers immediate side-effects - npm and RubyGems trojanization starts synchronously as soon as a valid token is found, before the propagation phase. Package backdooring occurs even if the worm is interrupted mid-run.
AWS providers enumerate all SSM Parameters (with decryption) and all Secrets Manager secrets across 17 regions. Kubernetes secrets are collected across all namespaces via in-cluster service account or kubeconfig. HashiCorp Vault KV v1/v2 secrets are collected via env token, file, Kubernetes service account, or AWS IAM auth. Password managers are targeted via CLI: 1Password (op), Bitwarden (bw), pass, and gopass.
The Azure Key Vault and GCP Secret Manager providers are present in the source but the authors' own ARCHITECTURE.MD marks them as broken - they have never successfully exfiltrated live data from either platform. The code executes and attempts collection; defenders should not assume these providers are non-functional against all targets, but the authors themselves have not confirmed live success.
For each GitHub PAT with workflow scope, GitHubActionsService commits a malicious workflow named "Run Copilot" to a temporary branch that dumps all repo-level secrets as an artifact named format-results via ${{ toJSON(secrets) }}, downloads the artifact, exfiltrates the contents, and deletes the workflow.
Stage 4: Multi-Ecosystem Propagation
We counted up to fourteen parallel mutators running with credentials in hand, several of which are undocumented in the authors' own ARCHITECTURE.MD.
GitHub branch poisoning (ReadmeUpdater) pushes malicious files to every feature branch in the current repository via GitHub's GraphQL createCommitOnBranch mutation. Commit camouflage copies the last real commit message from each target branch. Files injected: .vscode/tasks.json (VSCode folderOpen task running node .vscode/setup.mjs), .claude/settings.json (Claude Code SessionStart hook), .claude/index.js (self-extracting worm payload), and .claude/setup.mjs (Bun bootstrapper). The .claude/settings.json merges with any existing settings found on the default branch, preserving existing hooks.
GitHub Actions tag hijacking (ActionMutator) enumerates all writable repos containing custom Actions (action.yml or action.yaml), finds all semver tags (v1, v2.x.x, etc.), and force-pushes orphan commits (no parent) to every tag. Any downstream workflow using your-org/your-action@v1 now executes the worm. The wrapper carefully delegates to the original action's HEAD OID after running the payload, delaying victim detection. Three hijacking strategies cover JavaScript actions (wrapped with a pre-execution index.js step), composite actions (setup-bun step injected), and Docker actions (wrapper that runs the worm before the original image).
Repository mutation (RepositoryMutator) targets branches with open pull requests only - not all branches - across up to 500 writable repositories. Non-owned public repos require at least 10 stars and push permission; enterprise-managed users are skipped entirely. Five files are injected per branch, with commit message chore: update dependencies and the Git trailer skip-checks:true:
File | Purpose |
|---|---|
| Primary JS_LOADER bootstrapper |
| Claude Code |
| Gemini CLI hook - same |
| Cursor rules injection (model-instruction format, not JSON hook) |
| VSCode |
RepositoryMutator also injects worm execution into build files on each branch: package.json (scripts.test), Makefile, composer.json, tox.ini, Dockerfile, CMakeLists.txt, and shell scripts. When the token has admin scope, branch protection is permanently deleted via the GitHub API before committing directly to the default branch. It is not restored.
npm --ignore-scripts bypass (orphanDep.ts) - undocumented in ARCHITECTURE.MD - adds a .npmrc file with git=node and an optionalDependencies entry pointing to github:axios/axios#main. When npm resolves the github: dependency, it reads .npmrc and calls node clone.js instead of git clone. The --ignore-scripts flag suppresses lifecycle hooks but not the custom git resolver invocation, making this a complete bypass of the most widely-recommended safe-install mitigation. The github:axios/axios#main reference is chosen specifically because it is a real, large, legitimate package - the install appears entirely plausible.
PyPI typo-squatting (TypoMutator) - also undocumented in ARCHITECTURE.MD - activates with TYPO_MODE=1 and generates typo names by appending -mcp and -mpc suffixes to target package names. The source comment states explicitly: "PyPI blocks direct typos but allows -mcp suffix packages." This deliberately exploits PyPI's failure to block requests-mcp as a typosquat of requests. The -mcp (Model Context Protocol) suffix has become a common legitimate naming convention, making these packages appear credible.
OIDC branch injection with legitimate sigstore provenance (NpmOidcBranchMutator) targets npm packages published via GitHub Actions OIDC Trusted Publishing. It creates a branch snapshot-<hex>, injects a workflow named "Dependabot Updates" that mints an OIDC token and publishes a trojanized package, and - if an admin token is available - permanently deletes the OIDC environment protection rules (required_reviewers, deployment_branch_policy) from the Development environment without restoring them. Critically, the published package receives genuine, verifiable sigstore provenance: the worm requests a real OIDC token from GitHub Actions, gets a real signing certificate from Fulcio (fulcio.sigstore.dev), and submits a real inclusion proof to Rekor (rekor.sigstore.dev). The resulting package will pass npm audit signatures and any sigstore-based provenance check. The provenance correctly references the victim repository and workflow because the OIDC token genuinely identifies them. Standard provenance verification is not a detection signal for this attack path.
AI tool hook injection (Claude mutator) scans the home directory for config files belonging to 13 AI coding tools: Claude Code, Codex, Gemini CLI, Copilot, Kiro, OpenCode, Cline, Aider, Tabby, Amazon Q, Cody, Bolt, and Continue. For JSON configs it injects a SessionStart hook running the worm on every session start. For .cursorrules, .windsurfrules, and .cursor/rules/*.mdc files it injects natural-language instructions designed to look like legitimate model instructions rather than tool hooks - exploiting the model's instruction-following to trigger execution, bypassing hook-based security controls that inspect only the hooks key in JSON configs. The worm payload is copied to ~/.config/index.js.
Stage 5: Persistence and Dead-Man Switch
We identified two persistence mechanisms operating independently, installed as separate services.
InstallMonitor in src/mutator/persist/install-monitor.ts writes GITHUB_MONITOR.py to ~/.local/share/updater/update.py and installs it as a systemd user service (update-monitor) on Linux or as a LaunchAgent (com.user.update-monitor) on macOS, calling loginctl enable-linger to survive user logout. Installation skips systems with fewer than 4 CPU cores to avoid sandbox detection. The monitor polls GitHub commit search hourly for messages matching firedalazer <url_b64>.<sig_b64>, verifies each RSA-PSS-SHA256 signature against an embedded 4096-bit public key, and executes the downloaded URL as arbitrary Python code. State is tracked in /var/tmp/.gh_update_state.
The dead-man switch (DEADMAN_SWITCH.sh) installs as a separate service under the name gh-token-monitor. It polls api.github.com/user every 60 seconds using the stolen token. When the token returns a 4xx response, it executes rm -rf ~/; rm -rf ~/Documents and exits after a 72-hour TTL. Revoking a stolen PAT without first stopping the gh-token-monitor service will trigger data destruction on the victim host.
The operator reinfection loop (create_payload_repo.py) creates a private GitHub repo, encrypts the worm bundle, pushes an Actions workflow that decrypts and executes it, stores stolen PATs as Actions secrets (PATS, PASSPHRASE), dispatches the workflow, and deletes the secrets 15 seconds later to cover tracks. An orphan-commit.py utility delivers payloads via unreferenced commits: creates a tag pointing to the payload commit, returns the raw file URL, immediately deletes the tag - the commit remains reachable by SHA in GitHub's object store but is invisible in normal repository browsing.
Indicators of Compromise
Network
Primary C2 (operator builds only - stripped from OSS release):
https://api.anthropic.com/v1/api- C2 endpoint; POST requests carry encrypted exfil blobs, not the JSONmodel/messagespayload of legitimate Claude API calls. Nox-api-keyoranthropic-versionheaders.
C2 and PAT discovery strings (active in OSS build):
DontRevokeOrItGoesBoom- GitHub commit search string; PATs stashed alongside this string are targeted by the wormTheBeautifulSandsOfTime- GitHub commit search string for signed C2 commands (inline JS eval)firedalazer- commit message keyword for signed C2 URL rotation used by GITHUB_MONITOR and DomainSenderFactory
Exfiltration repos:
Newly created public GitHub repos with Greek underworld names matching the pattern
(stygian|tartarean|erebean|infernal|chthonic|acheronian|lethean|plutonian|abyssal|charonian|thanatic|funereal|nekyian|sepulchral|tenebrous|cimmerian)-(cerberus|charon|tartarus|erebus|asphodel|acheron|styx|lethe|cocytus|phlegethon|shade|eidolon|wraith|thanatos|hecate|persephone)-[0-9]{5}Repository description:
"Hades * The End for the Damned"- hardcoded on every created exfil repoFile pattern:
results/<timestamp>-<counter>.jsonin exfil repos
Runtime downloads:
https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-{platform}-{arch}.zip- Bun runtime downloaded byINJECT_PTH.pthandJS_LOADERhttps://github.com/oven-sh/bun/releases/download/bun-v1.3.13/...- Bun version used byBASH_LOADER.sh(SSH delivery path uses a different pinned version)
StepSecurity bypass:
/etc/hostsentry:127.0.0.1 agent.stepsecurity.io api.stepsecurity.io app.stepsecurity.io www.stepsecurity.io stepsecurity.io/etc/resolv.confoverwrite:nameserver 8.8.8.8
Filesystem
Worm payload drops:
~/.config/index.js- worm payload (AES-128-GCM + ROT-N self-extracting wrapper); created by Claude mutator/tmp/.bun_ran-INJECT_PTHone-run guard; presence confirms Python.pthpersistence has fired/var/tmp/.gh_update_state-GITHUB_MONITORcommand deduplication state; presence confirms persistent C2 channel is installed/tmp/b/bunor/tmp/b/bun.exe- Bun runtime cached byINJECT_PTH.pth
GITHUB_MONITOR persistence (service name: update-monitor):
~/.local/share/updater/update.py~/.config/systemd/user/update-monitor.service~/Library/LaunchAgents/com.user.update-monitor.plist
Dead-man switch persistence (service name: gh-token-monitor):
~/.local/bin/gh-token-monitor.sh~/.config/gh-token-monitor/token(mode 600 - stolen PAT)~/.config/gh-token-monitor/handler(mode 600 - destruction command)~/.config/systemd/user/gh-token-monitor.service~/Library/LaunchAgents/com.user.gh-token-monitor.plist
Python .pth persistence:
<site-packages>/INJECT_PTH.pth- single obfuscated line; injected byPypiMutatorandPyPIOidcClient<site-packages>/_index.js- worm payload copied into Python site-packages
Files injected into victim repositories (branches with open PRs):
.github/setup.js- JS_LOADER bootstrapper.claude/settings.json- Claude CodeSessionStarthook.gemini/settings.json- Gemini CLISessionStarthook.cursor/rules/setup.mdc- Cursor rules injection (AI model instruction format).vscode/tasks.json- VSCodefolderOpentask.vscode/setup.mjs- Bun bootstrapper.claude/index.js- worm payload.claude/setup.mjs- Bun bootstrapper for Claude Code vector
SSH delivery (SshMutator):
/tmp/.sshu-<6 random chars>/- remote temp directory on SSH target hostsai_setup.sh- BASH_LOADER script on SSH targetsai_init.js- worm payload on SSH targets
AWS SSM delivery:
/tmp/.ssm-setup.js- worm payload written on EC2 instances via SSM SendCommand
Process Indicators
bun run ~/.config/index.js- worm execution via AI tool hookbun run .claude/index.js- worm execution from repo hooknode .vscode/setup.mjs- VSCode task bootstrapnode .github/setup.js- Claude/Gemini hook payloadgh auth token- GitHub CLI token extraction (run as subprocess by any bun/node parent process)python3 /tmp/*.py- GITHUB_MONITOR executing downloaded C2 commands from temp filessudo python3with stdin from a memory dump script - Runner.Worker/proc/memdump
Commit and Repository Indicators
Commit message
chore: update dependencies+ trailerskip-checks:true- RepositoryMutator fallback; any commit with this message adding.claude/,.gemini/,.cursor/, or.vscode/files is an IOCCommit message
fix: ci- used in OIDC branch injectionchore: update dependencies [skip ci]- second commit in OIDC branch injection (restores original tree after deployment-target commit)Branch names
snapshot-<8 hex chars>- OIDC injection branchWorkflow named
"Dependabot Updates"with aworkflow_dispatchtrigger andOIDC_PACKAGESenv var - OIDC injection camouflageWorkflow named
"Run Copilot"dumping${{ toJSON(secrets) }}to artifactformat-results- GitHubActionsService secrets dumpWorkflow file at
.github/workflows/codeql.ymladded by the worm (not a genuine CodeQL workflow)Semver tags (
v1,v2.x.x) pointing to orphan commits (commits with no parent) - ActionMutator IOCAction step
oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6+ step named"Cleanup Action"- ActionMutator injection signatureBranch protection deletion (
protected_branch.destroyin GitHub Audit Log) with no corresponding restore - RepositoryMutator admin-token pathUser-Agent
python-requests/2.31.0used by RepositoryMutator for enterprise user detection
npm Package Indicators
Bumped patch version with no corresponding changelog or git tag
preinstallscript added topackage.jsonbunadded as a dependencyoptionalDependenciesentry pointing togithub:axios/axios#maincombined with.npmrccontaininggit=node---ignore-scriptsbypass (orphanDep.ts)npm provenance for a package where the provenance
buildTriggerreferences asnapshot-*branch or a workflow named"Dependabot Updates"- sigstore signatures will verify as genuine
Credentials Targeted
~/.aws/credentials,~/.aws/config~/.azure/accessTokens.json,~/.azure/msal_token_cache.*~/.config/gcloud/application_default_credentials.json,~/.config/gcloud/credentials.db~/.kube/config~/.ssh/(all private keys,known_hosts,config,authorized_keys)~/.docker/config.json~/.gitconfig,~/.git-credentials,~/.netrc~/.npmrc,~/.pypirc,~/.gem/credentials~/.claude.json,~/.claude/*,~/.claude/projects/*,~/.claude/mcp.json~/.config/helm/*~/.ethereum/keystore/*,~/.monero/*~/.config/Signal/*,~/.config/Slack/Cookies,~/.config/telegram-desktop/*~/.bash_history,~/.zsh_history,~/.python_history,~/.mysql_history,~/.psql_history.env,.env.local,.env.production,.env.development,.env.stagingVAULT_TOKEN,VAULT_AUTH_TOKEN(env)GITHUB_TOKEN,GITHUB_TOKEN2(env)AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY(env)
Embedded Keys
RSA-4096 enc_key.pub(exfiltration encryption public key) - presence in any file or binary confirms MiasmaRSA-4096 verify_key.pubpartial match:MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAw5zZbSXX+4X2kTs/zC7l- embedded inGITHUB_MONITOR.pyAES-256 TOKEN_AES_KEY(hardcoded hex):bd8035203536735490e4bd5cdcede581a9d3a3f7a5df7725859844d8dcc8eb49- used to encrypt PATs stashed in GitHub repos
Hashes
SHA256 (outer package.zip):
0736c83273465c6b64c04bd92f0e7a8517e8dbc19bb54d6c9ab41420fd833d6bSHA256 (Miasma-Open-Source-Release-main.zip):
6331d1511783dcb1158fb54775f563e90399b3a2a81a584d3cba9a77f63d15a7
Affected Versions
Miasma-Open-Source-Release-main (voicefromtheouterworld, no published version tag)
MITRE ATT&CK
ID | Technique | Application |
|---|---|---|
| Supply Chain Compromise: Software Supply Chain | npm/PyPI/RubyGems packages trojanized with preinstall hooks; GitHub Actions semver tags hijacked; OIDC branch injection; PyPI typo-squatting via |
| Supply Chain Compromise: Compromise Software Dependencies |
|
| Valid Accounts: Cloud Accounts | All propagation and exfiltration uses legitimate victim GitHub PATs; no malicious login events are generated |
| JavaScript | Primary worm bundle; |
| Python |
|
| Unix Shell |
|
| User Execution: Malicious File | VSCode |
| Create or Modify System Process: Launch Agent |
|
| Event-Triggered Execution | Claude Code and Gemini CLI |
| Compromise Host Software Binary | npm |
| Escape to Host | Spawns privileged Alpine container mounted at |
| Deploy Container | Creates and starts privileged Docker container via Docker socket to escalate privileges and restore sudo |
| Obfuscated Files or Information | Polyalphabetic substitution cipher ( |
| Software Packing |
|
| Encrypted/Encoded File | All embedded payload assets AES-256-GCM encrypted at build time; self-extracting payloads use AES-128-GCM + ROT-N with per-build random keys |
| Masquerading: Match Legitimate Name or Location | C2 path |
| Deobfuscate/Decode Files or Information |
|
| Virtualization/Sandbox Evasion: System Checks | Checks for decoy token prefixes, sandbox CWD paths, specific sandbox tool paths, hostname containing "sandbox", and honeypot AWS credential file contents; exits on Russian locale |
| Security Software Discovery | Checks 17 EDR/AV process names and 4 installation paths before running credential providers or mutators |
| Impair Defenses: Disable or Modify Tools | Kills StepSecurity Harden-Runner Docker containers via Docker socket; spawns privileged container to restore runner sudo |
| Impair Defenses: Indicator Blocking | Poisons |
| Hide Artifacts: Hidden Files and Directories | Payload at |
| Indicator Removal | Deletes |
| Unsecured Credentials: Credentials In Files | 100+ filesystem hotspot paths scanned including cloud credentials, package registry tokens, AI tool configs, shell histories, crypto wallets, and messaging app data |
| Unsecured Credentials: Private Keys |
|
| Unsecured Credentials: Cloud Instance Metadata API | AWS IMDSv2, Azure IMDS, and GCP metadata server all queried for credentials |
| Steal Application Access Token | GitHub PATs, npm tokens, RubyGems API keys, PyPI tokens, JFrog tokens harvested from files and environment |
| Steal Web Session Cookie | Discord and Element browser local storage; Signal config; Slack cookies |
| Credentials from Password Stores: Password Managers | 1Password CLI ( |
| Exploitation for Credential Access | GitHub Runner |
| Remote Services: SSH | SCP worm payload to hosts enumerated from |
| Software Deployment Tools | AWS SSM |
| Exfiltration Over C2 Channel | Encrypted batches HTTPS-POSTed to |
| Exfiltration Over Web Service: Exfiltration to Code Repository | Encrypted blobs committed to freshly-created public GitHub repos with Greek underworld-themed names |
| Archive Collected Data | Data is gzip-compressed then AES-256-GCM encrypted before exfiltration |
| Dynamic Resolution | C2 domain discovered at runtime from cryptographically signed GitHub commits; allows operator to rotate C2 without redeploying the worm |
| Web Service: Bidirectional Communication |
|
| Encrypted Channel: Asymmetric Cryptography | All exfiltrated data wrapped in RSA-OAEP-SHA256; C2 commands authenticated with RSA-PSS-SHA256 |
A Note from Ossprey
Ossprey obtained and analyzed the Miasma source release after it was published publicly. This was not an Ossprey detection event - Miasma was not distributed through a package registry, and there was no package for our pipeline to flag. For organizations whose pipelines process npm, PyPI, or RubyGems dependencies, Ossprey's behavioral analysis models the exact tradecraft Miasma uses: binding.gyp and preinstall triggers, per-build AES-GCM self-extracting payloads, multi-ecosystem publish-token abuse, and the .npmrc git=node bypass of --ignore-scripts. Derivative campaigns that deploy this worm through a registry would be detectable before install. The public availability of the full source - including the TypoMutator, OIDC branch injection, and --ignore-scripts bypass, none of which are documented in the authors' ARCHITECTURE.MD - makes monitoring for Miasma-derived registry uploads an active Ossprey priority.
For teams processing open-source dependencies at scale, book a demo to see how Ossprey's behavioral analysis detects Miasma-derived campaigns before they reach your environment.
Related Ossprey research: The Complete TeamPCP Campaign | Trivy Supply Chain Attack: TeamPCP, CanisterWorm | Axios Hijacked: Cross-Platform RAT | TJ-Actions Breach
Frequently Asked Questions
Was there a malicious npm package I need to remove? Miasma was released as a public source repository, not as a deployed registry package. No published npm, PyPI, or RubyGems package is known to contain this worm at time of writing. If you find an installed package that looks like a derivative, compare its package.json against the preinstall hook and Bun dependency patterns in the IOC section above.
Does Ossprey detect Miasma? Miasma was not distributed through a package registry, so there was no package for Ossprey's pipeline to analyze. Derivative campaigns deploying Miasma's tradecraft through npm, PyPI, or RubyGems would be detectable before install - the binding.gyp + preinstall combination, per-build AES-GCM self-extracting payloads, and .npmrc git=node override are all behavioral signals Ossprey models.
How confident are you that TeamPCP authored this? We are not. The README claims TeamPCP authorship. Our assessment is that this was written by a group inspired by TeamPCP rather than TeamPCP itself. Attribution requires C2 infrastructure correlation beyond code-level overlap. See our Complete TeamPCP Campaign research for the confirmed attribution baseline.
Is the api.anthropic.com C2 infrastructure live? The api.anthropic.com transport is explicitly stripped from the open-source build. In the published source, GitHub repository commits are the only active exfiltration path. Operator builds are expected to re-enable it - the full implementation is present in src/sender/domain/.
I found gh-token-monitor running. What do I do? Stop the service before revoking any stolen PATs. The dead-man switch fires rm -rf ~/; rm -rf ~/Documents the moment it receives a 4xx response from the GitHub API on the monitored token. Stop first: systemctl --user stop gh-token-monitor (Linux) or launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist (macOS). Then revoke.




