메뉴
HN
Hacker News 12일 전

미니 샤이훌루드 공격 재개: 314개 npm 패키지 탈취

IMP
9/10
핵심 요약

2026년 5월 19일, npm 계정이 해킹당해 317개 패키지에 걸쳐 637개의 악의적인 버전이 단 22분 만에 자동으로 퍼블리싱되었습니다. 이 악성코드는 AWS 자격 증명, GitHub 토큰, SSH 키 등을 광범위하게 탈취할 뿐만 아니라, Claude Code 및 VS Code와 같은 AI 개발 도구를 하이재킹하여 지속적으로 감염을 유지하는 정교한 공격입니다. 특히 공개 GitHub 저장소를 C2(명령 및 제어) 채널로 위장하고 CI/CD 파이프라인에 침투하여 서명된 합법적인 아티팩트를 위조하므로 소프트웨어 공급망 전체에 심각한 위협을 줍니다.

번역된 본문

블로그로 돌아가기

미니 샤이훌루드 공격 재개: 317개 npm 패키지 침해

악성코드 SafeDep 팀 • 2026년 5월 19일 • 26분 읽기

목차 TL;DR

2026년 5월 19일, npm 계정 atool ( [email protected] )이 해킹당했습니다. 공격자는 22분 동안 자동화된 공격을 통해 317개의 패키지에 걸쳐 637개의 악의적인 버전을 퍼블리싱했습니다. 영향을 받은 패키지로는 size-sensor (월 420만 다운로드), echarts-for-react (380만), @antv/scale (220만), timeago.js (115만) 및 수백 개의 @antv 범위 패키지가 있습니다. 페이로드는 498KB의 난독화된 Bun 스크립트로, 3주 전 SAP 해킹에 사용된 미니 샤이훌루드(Mini Shai-Hulud) 툴킷과 동일합니다. 동일한 스캐너 아키텍처, 동일한 자격 증명 정규식 세트, 동일한 난독화 패턴을 사용합니다. 이 악성코드는 전체 AWS 체인(환경 변수, 구성 파일, EC2 IMDS, ECS 컨테이너 메타데이터, Secrets Manager), Kubernetes 서비스 계정 토큰, HashiCorp Vault, GitHub PAT, npm 토큰, SSH 키 등의 자격 증명을 수집합니다. 훔친 데이터는 User-Agent를 python-requests/2.31.0으로 위조한 다음, 탈취한 토큰으로 생성된 공개 GitHub 리포지토리에 Git 개체로 커밋하여 유출됩니다. CI 환경에서 페이로드는 GitHub Actions OIDC 토큰을 npm 퍼블리시 토큰으로 교환하고, 훔친 신원을 사용하여 Sigstore(Fulcio + Rekor)를 통해 아티팩트에 서명하며, .github/workflows/codeql.yml에 지속성을 주입합니다. 페이로드는 SessionStart 훅을 주입하여 Claude Code와 Codex를 하이재킹합니다. 이 훅은 로컬 및 접근 가능한 GitHub 리포지토리에 대한 커밋을 통해 모든 AI 세션에서 악성코드를 재실행합니다. VS Code 역시 동일한 효과를 얻기 위해 "runOn": "folderOpen" 속성이 포함된 tasks.json을 생성합니다. 지속적인 systemd 서비스 / macOS LaunchAgent( kitty-monitor )는 GitHub 데드드롭 C2 백도어를 설치합니다. 이는 Python 데몬으로, 매 시간마다 GitHub의 커밋 검색 API를 폴링하여 firedalazer 키워드가 포함된 커밋 메시지에서 RSA-PSS 서명된 명령을 찾은 다음, 서명된 URL에서 임의의 Python 코드를 다운로드하여 실행합니다. 별도의 gh-token-monitor 데몬은 훔친 GitHub 토큰을 60초 간격으로 폴링합니다. 또한 페이로드는 호스트 소켓을 통해 Docker 컨테이너 탈출을 시도하고 감염을 다른 로컬 Node.js 프로젝트로 전파합니다. 이 공격은 두 가지 실행 경로를 사용합니다. 각 손상된 버전은 사전 설치 훅( bun run index.js )을 추가합니다. 637개 버전 중 630개는 antvis/G2 GitHub 리포지토리의 가짜 커밋을 가리키는 optionalDependencies 항목도 주입합니다. 이들은 작성자가 위조된 고아 커밋(orphan commits)으로, 리포지토리의 브랜치 기록에서는 보이지 않으며, 대상 리포지토리에 대한 쓰기 권한 없이 GitHub의 포크 개체 공유를 악용하여 페이로드의 두 번째 복사본을 호스팅합니다. npm의 github: 종속성 해석은 SHA를 통해 콘텐츠를 가져와 실행합니다.

영향: 시맨틱 버전 범위(예: echarts-for-react의 경우 ^3.0.6)를 사용하는 프로젝트는 자동으로 손상된 버전을 해석합니다. 자격 증명 수집은 npm 토큰, GitHub PAT, AWS 키(EC2 메타데이터 및 ECS 컨테이너 자격 증명을 포함한 전체 자격 증명 체인), GCP 서비스 계정, Azure 자격 증명, 데이터베이스 연결 문자열, Stripe 키, Slack 토큰, SSH 키, Docker 인증, Kubernetes 서비스 계정 토큰 및 HashiCorp Vault 토큰을 대상으로 합니다. 유출된 데이터는 python-requests/2.31.0 User-Agent로 위장하여 GitHub API를 C2 채널로 사용해, 훔친 토큰 계정으로 생성된 공개 GitHub 리포지토리에 커밋됩니다. CI에서 npm OIDC 토큰 교환을 통해 공격자는 파이프라인의 자체 ID를 사용하여 퍼블리시 토큰을 얻을 수 있습니다. 훔친 OIDC 토큰을 사용한 Sigstore 서명은 위조된 출처(provenance)를 가진 합법적으로 서명된 아티팩트를 생성합니다. Docker 소켓 액세스는 호스트 파일 시스템 바인드 마운트를 통해 권한이 있는 컨테이너 탈출을 가능하게 합니다. .github/workflows/codeql.yml 주입을 통한 CI/CD 지속성("Run Copilot"으로 명명됨)은 GitHub Actions 아티팩트로 toJSON(secrets)을 덤프한 다음 워크플로우 실행을 삭제하고 브랜치를 재설정하여 자체적으로 정리합니다. AI 에이전트 하이재킹: Claude Code SessionStart 훅, Codex 훅, VS Code의 "runOn": "folderOpen" 작업은 모두 페이로드를 재실행하는 Bun 부트스트래퍼를 트리거합니다. 지속적인 systemd 사용자 서비스

원문 보기
원문 보기 (영어)
Back to Blog Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised Malware SafeDep Team • May 19, 2026 • 26 min read Table of Contents TL;DR The npm account atool ( [email&#160;protected] ) was compromised on May 19, 2026. The attacker published 637 malicious versions across 317 packages in a 22-minute automated burst. Affected packages include size-sensor (4.2M downloads/month), echarts-for-react (3.8M), @antv/scale (2.2M), timeago.js (1.15M), and hundreds of @antv scoped packages. The payload is a 498KB obfuscated Bun script that matches the Mini Shai-Hulud toolkit used in the SAP compromise three weeks earlier: same scanner architecture, same credential regex set, same obfuscation pattern. It harvests credentials across the full AWS chain (env vars, config files, EC2 IMDS, ECS container metadata, Secrets Manager), Kubernetes service account tokens, HashiCorp Vault, GitHub PATs, npm tokens, SSH keys, and more. Stolen data is exfiltrated by committing it as Git objects to public GitHub repositories created under the compromised token, with the User-Agent forged as python-requests/2.31.0 . In CI environments, the payload exchanges GitHub Actions OIDC tokens for npm publish tokens, signs artifacts via Sigstore (Fulcio + Rekor) using the stolen identity, and injects persistence into .github/workflows/codeql.yml . The payload hijacks Claude Code and Codex by injecting SessionStart hooks that re-execute the malware on every AI session, both locally and via commits to accessible GitHub repositories. VS Code gets a tasks.json with "runOn": "folderOpen" for the same effect. A persistent systemd service / macOS LaunchAgent ( kitty-monitor ) installs a GitHub dead-drop C2 backdoor: a Python daemon that polls GitHub’s commit search API hourly for RSA-PSS signed commands in commit messages containing the keyword firedalazer , then downloads and executes arbitrary Python from the signed URL. A separate gh-token-monitor daemon polls stolen GitHub tokens at 60-second intervals. The payload also attempts Docker container escape via the host socket and propagates infection to other local Node.js projects. The attack uses two execution paths. Each compromised version adds a preinstall hook ( bun run index.js ). 630 of 637 versions also inject an optionalDependencies entry pointing to imposter commits in the antvis/G2 GitHub repository. These are orphan commits with forged authorship, invisible in the repo’s branch history, exploiting GitHub’s fork object sharing to host a second copy of the payload without any write access to the target repository. npm’s github: dependency resolution fetches and executes the content by SHA. Impact: Projects using semver ranges (e.g., ^3.0.6 for echarts-for-react ) auto-resolve to compromised versions Credential harvesting targets npm tokens, GitHub PATs, AWS keys (full credential chain including EC2 metadata and ECS container credentials), GCP service accounts, Azure credentials, database connection strings, Stripe keys, Slack tokens, SSH keys, Docker auth, Kubernetes service account tokens, and HashiCorp Vault tokens Exfiltrated data is committed to public GitHub repositories created under the stolen token’s account, using the GitHub API as a C2 channel disguised with a python-requests/2.31.0 User-Agent npm OIDC token exchange in CI allows the attacker to obtain publish tokens using the pipeline’s own identity Sigstore signing with stolen OIDC tokens creates legitimately-signed artifacts with forged provenance Docker socket access enables privileged container escape with host filesystem bind mounts CI/CD persistence via .github/workflows/codeql.yml injection (named “Run Copilot”) that dumps toJSON(secrets) as a GitHub Actions artifact, then self-cleans by deleting the workflow run and resetting the branch AI agent hijacking: Claude Code SessionStart hooks, Codex hooks, and VS Code "runOn": "folderOpen" tasks, all triggering a Bun bootstrapper that re-executes the payload Persistent systemd user services and macOS LaunchAgents: kitty-monitor runs a GitHub dead-drop C2 backdoor that accepts RSA-signed remote commands via GitHub commit search; gh-token-monitor polls stolen tokens at 60-second intervals Local project infection copies payload files and hooks into other Node.js projects on the same machine Redundant payload delivery via GitHub imposter commits survives even if preinstall hooks are blocked Indicators of Compromise (IoC): Any package published by atool ( [email&#160;protected] ) on 2026-05-19 between 01:44 and 02:06 UTC preinstall script: bun run index.js Payload SHA256: a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c Imposter commits in antvis/G2 (orphan, forged author, message: “New Package”): 1916faa365f2788b6e193514872d51a242876569 (626 versions) 7cb42f57561c321ecb09b4552802ae0ac55b3a7a (2 versions) dc3d62a2181beb9f326952a2d212900c94f2e13d (1 version, garbage collected) Optional dependency: @antv/setup: github:antvis/G2#<commit-sha> Exfiltration repositories matching the Dune-themed naming pattern {word1}-{word2}-{number} where word1 is one of: sardaukar , mentat , fremen , atreides , harkonnen , gesserit , prescient , fedaykin , tleilaxu , siridar , kanly , sayyadina , ghola , powindah , prana , kralizec ; word2 is one of: sandworm , ornithopter , heighliner , stillsuit , lasgun , sietch , melange , thumper , navigator , fedaykin , futar , phibian , slig , cogitor , laza , ghola ; number is 0-999. Description: “Shai-Hulud: Here We Go Again” (reversed in source) HTTP requests to 169.254.169.254 (EC2 metadata) and 169.254.170.2 (ECS container metadata) Branches named chore/add-codeql-static-analysis in repositories accessible to compromised tokens .github/workflows/codeql.yml with workflow name Run Copilot that dumps toJSON(secrets) to format-results.txt .claude/settings.json containing SessionStart hooks running node .claude/setup.mjs .vscode/tasks.json with "runOn": "folderOpen" tasks calling .claude/setup.mjs .claude/setup.mjs or .vscode/setup.mjs (Bun bootstrapper, downloads bun v1.3.14 from GitHub) Systemd user service kitty-monitor.service or LaunchAgent com.user.kitty-monitor.plist gh-token-monitor daemon at ~/.local/bin/gh-token-monitor.sh Files at ~/.local/share/kitty/cat.py (GitHub dead-drop C2 backdoor) State file /var/tmp/.gh_update_state (C2 execution tracking) GitHub commits containing the keyword firedalazer (C2 command trigger) RSA-PSS signed commands in commit messages: firedalazer <base64_url>.<base64_signature> If you are auditing lockfiles or reinstalling on affected machines, Package Manager Guard (pmg) is an open-source install proxy that evaluates packages against threat intelligence before preinstall scripts run. Its dependency cooldown can refuse versions published inside a configurable window, which helps against bursts like the May 19 wave where semver ranges were still resolving to freshly published malicious releases. Analysis Account Compromise and Blast Radius The atool npm account maintains 547 packages. The attacker published 637 malicious versions across 314 of those packages in two automated waves, both on May 19, 2026: Wave Time (UTC) Versions published Pattern First 01:39 - 01:56 ~317 versions Initial burst with 4 early test publishes at 01:39-01:49 Second 02:05 - 02:06 ~314 versions Second version bump across same packages Most packages (309) received exactly 2 malicious versions, one per wave. Four packages ( size-sensor , echarts-for-react , jest-canvas-mock , jest-date-mock ) received 3 versions, suggesting they were used for early testing before the bulk publish. A sample of the highest-impact affected packages: The attacker did not move the latest dist-tag on most packages. For echarts-for-react , latest still points to 3.0.6 . This provides no protection: npm’s semver resolution picks the highest version matching a range, regardless of the latest tag. Any project with "echarts-for-react": "^3.0.6" in its package.json resolves to 3.2.7 (malicious) on the