---
title: "npm 317개 패키지 감염, Mini Shai-Hulud가 다시 터짐"
published: 2026-05-19T05:04:49.000Z
canonical: https://jeff.news/article/2848
---
# npm 317개 패키지 감염, Mini Shai-Hulud가 다시 터짐

atool npm 계정이 탈취되어 2026년 5월 19일 22분 동안 317개 패키지에 637개의 악성 버전이 배포됐다. 악성 페이로드는 Mini Shai-Hulud 계열로, npm·깃허브·AWS·쿠버네티스·볼트·패스워드 매니저·AI 코딩 에이전트까지 훑고 CI에서 OIDC와 Sigstore까지 악용한다.

## 22분 만에 npm 패키지 317개가 털림

- npm 계정 atool이 2026년 5월 19일 탈취됐고, 공격자는 22분 동안 악성 버전 637개를 자동 배포함
  - 영향 패키지는 317개로 집계됨
  - size-sensor는 월 420만 다운로드, echarts-for-react는 월 380만 다운로드, @antv/scale은 월 220만 다운로드, timeago.js는 월 115만 다운로드 규모임
  - atool 계정은 원래 547개 패키지를 관리하던 계정이라, 계정 하나가 털렸을 때 폭발 반경이 말 그대로 수백 개 패키지로 번짐

- 이 공격은 기존 Mini Shai-Hulud 계열과 거의 같은 구조를 재사용함
  - 페이로드는 498KB짜리 난독화된 Bun 스크립트임
  - 3주 전 SAP 관련 침해에서 쓰인 Mini Shai-Hulud 툴킷과 스캐너 구조, 자격증명 정규식, 난독화 패턴이 맞아떨어짐
  - 환경 변수 이름, 파일 경로, C2 주소 같은 민감 문자열은 base64와 XOR 기반 디코더로 숨겨져 있음

> [!WARNING]
> semver 범위를 쓰는 프로젝트는 “latest 태그가 안전하면 괜찮겠지”가 통하지 않음. 예를 들어 echarts-for-react의 latest가 3.0.6이어도, package.json에 ^3.0.6이 있으면 클린 설치 때 악성 3.2.7로 해석될 수 있음.

- 실행 경로도 하나가 아니라 둘임
  - 모든 감염 버전은 package.json에 preinstall 훅으로 bun run index.js를 추가함
  - 637개 중 630개는 optionalDependencies에 @antv/setup을 추가하고, github:antvis/G2#커밋SHA 형태로 두 번째 페이로드를 가져오게 함
  - optional dependency는 실패해도 npm 설치가 계속되기 때문에, prepare 스크립트가 먼저 실행된 뒤 조용히 실패한 것처럼 보일 수 있음

## 훔치는 범위가 개발자 환경 전체임

- 페이로드는 80개 이상의 환경 변수를 읽고 파일 시스템까지 스캔함
  - npm 토큰, GitHub 개인 접근 토큰(PAT), AWS 키, GCP 서비스 계정, Azure 자격증명, 데이터베이스 연결 문자열, Stripe 키, Slack 토큰, SSH 키, Docker 인증 정보가 대상임
  - 쿠버네티스 서비스 계정 토큰, HashiCorp Vault 토큰도 훑음
  - 로컬 패스워드 매니저인 1Password, Bitwarden, pass, gopass까지 CLI가 있으면 털려고 시도함

- AWS 쪽은 단순 환경 변수만 보는 수준이 아님
  - AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN 같은 기본 환경 변수를 확인함
  - .aws 디렉터리와 설정 파일을 읽음
  - EC2 인스턴스 메타데이터 서비스(IMDSv2)인 169.254.169.254를 호출함
  - ECS 컨테이너 메타데이터 주소 169.254.170.2도 확인함
  - Secrets Manager의 ListSecrets와 GetSecretValue까지 시도함

- CI 환경 탐지도 꽤 넓음
  - GitHub Actions, Jenkins, GitLab CI, CircleCI, Travis, Buildkite, Drone, TeamCity, AppVeyor, Bitbucket Pipelines, Azure DevOps, CodeBuild, Netlify, Vercel 등 20개 넘는 플랫폼 환경 변수를 확인함
  - GitHub Actions에서는 워크플로 실행 기록, 아티팩트, 시크릿 이름, OIDC 토큰 교환까지 추가로 수행함
  - 러너 프로세스 메모리를 훑어 환경 변수에 안 보이는 비밀값까지 찾는 코드도 포함됨

## 빼낸 데이터는 GitHub와 가짜 OpenTelemetry로 나감

- 탈취 데이터는 두 경로로 동시에 빠져나감
  - 하나는 GitHub API를 써서 공격자가 만든 공개 저장소에 Git 객체로 커밋하는 방식임
  - 다른 하나는 hxxps://t.m-kosche[.]com/api/public/otel/v1/traces로 HTTPS POST를 보내는 방식임
  - 후자는 OpenTelemetry 트레이스 수집 엔드포인트처럼 보이게 만든 경로라 네트워크 로그에서 정상 관측성 트래픽처럼 보일 수 있음

- GitHub 기반 유출은 꽤 교묘하게 설계됨
  - 훔친 GitHub 토큰으로 GET /user와 /user/orgs를 호출해 유효성과 권한을 확인함
  - repo 또는 public_repo 권한이 있으면 공개 저장소를 새로 만들고 데이터를 커밋함
  - 저장소 이름은 Dune 세계관 단어 조합인 {단어1}-{단어2}-{0-999} 패턴임
  - 설명 문자열은 뒤집으면 “Shai-Hulud: Here We Go Again”이 되도록 숨겨져 있음

- HTTPS 유출은 암호화까지 붙어 있음
  - JSON 페이로드를 gzip으로 압축한 뒤 AES-256-GCM으로 암호화함
  - AES 키는 하드코딩된 RSA 공개키로 감싸서 전송함
  - 즉 중간에서 트래픽을 잡아도 내용 확인이 쉽지 않음

## CI/CD와 서명 체인까지 악용함

- GitHub Actions 환경에서는 OIDC를 이용해 npm publish 토큰을 얻으려 함
  - ACTIONS_ID_TOKEN_REQUEST_TOKEN과 ACTIONS_ID_TOKEN_REQUEST_URL을 읽어 OIDC 토큰을 가져옴
  - 이 신원을 npm 토큰 교환 엔드포인트에 넣어 publish 권한을 얻는 흐름임
  - 장기 토큰을 없애려고 도입한 OIDC가, CI가 털린 순간 공격자에게 임시 발급기처럼 쓰일 수 있다는 얘기임

- Sigstore 서명도 공격 경로에 포함됨
  - Fulcio에서 인증서를 받고 Rekor 투명성 로그에 제출하는 구현이 들어 있음
  - in-toto Statement와 SLSA provenance 포맷도 언급됨
  - CI 신원이 이미 털린 상태라면, 공격자가 만든 아티팩트가 합법적인 CI에서 나온 것처럼 서명될 수 있음

> [!IMPORTANT]
> 서명 검증은 여전히 필요하지만, “서명됐으니 안전함”은 아님. 빌드 주체의 신원 자체가 탈취되면 Sigstore 체인도 공격자의 정상 포장지가 될 수 있음.

## AI 코딩 도구도 직접 노림

- 이번 페이로드는 Claude Code, Codex, VS Code 설정을 감염 대상으로 삼음
  - .claude/settings.json에 SessionStart 훅을 넣어 세션이 시작될 때 node .claude/setup.mjs가 실행되게 함
  - .vscode/tasks.json에는 runOn: folderOpen 작업을 넣어 폴더를 여는 순간 실행되게 함
  - setup.mjs는 Bun v1.3.14를 내려받아 페이로드를 다시 실행하는 부트스트래퍼 역할을 함

- 로컬 전파도 있음
  - 감염된 npm install이 한 번 실행되면 ~/.claude/package/index.js와 ~/.codex/package/index.js에 페이로드를 복사함
  - 파일 시스템에서 settings.json을 찾아 Claude Code나 Codex 설정에 훅을 주입함
  - 결과적으로 한 프로젝트에서 감염돼도 같은 머신의 다른 AI 코딩 워크스페이스로 번질 수 있음

## GitHub 의존성 경로가 진짜 골때림

- 공격자는 antvis/G2 저장소의 특정 커밋 SHA를 npm github: dependency로 참조함
  - 이 커밋들은 브랜치에 붙지 않은 orphan commit이고, 작성자 정보는 실제 maintainer처럼 위조됨
  - Git 작성자 이름과 이메일은 쉽게 위조할 수 있고, GitHub UI는 noreply 이메일 매칭만으로 계정처럼 보여줄 수 있음
  - 커밋에는 GPG 서명이 없었음

- 더 무서운 건 antvis/G2에 쓰기 권한이 없어도 이런 호스팅이 가능하다는 점임
  - GitHub는 원본 저장소와 포크 사이에 Git 객체 저장소를 공유함
  - 공격자는 포크에 악성 orphan commit을 만들고, 원본 저장소 네임스페이스의 SHA로 참조되게 만들 수 있음
  - 포크를 지워도 객체가 바로 사라지지 않으면 npm이 github:antvis/G2#SHA로 해당 내용을 가져올 수 있음
  - 이 방식은 인기 저장소를 몰래 페이로드 호스트처럼 쓰는 셈임

## 바로 해야 할 점검

- lockfile부터 확인해야 함
  - package-lock.json, pnpm-lock.yaml, yarn.lock에서 2026년 5월 19일에 배포된 감염 패키지 버전이 들어왔는지 봐야 함
  - semver 범위 때문에 클린 설치에서 악성 버전이 들어왔을 수 있음

- 감염 버전이 설치됐다면 비밀값 회전은 넓게 잡아야 함
  - npm 토큰, GitHub PAT, AWS 키, SSH 키, 클라우드 자격증명, 데이터베이스 비밀번호, Vault 토큰, 쿠버네티스 서비스 계정 토큰을 회전해야 함
  - 1Password, Bitwarden, pass, gopass 같은 로컬 패스워드 매니저 CLI가 열린 상태였거나 접근 가능했다면 그쪽도 점검 대상임

- 흔적 확인 포인트도 많음
  - t.m-kosche[.]com으로 나간 트래픽을 DNS나 네트워크 로그에서 확인함
  - GitHub 계정 아래 Dune 단어 조합 이름의 이상한 공개 저장소가 생겼는지 확인함
  - .github/workflows/codeql.yml에 Run Copilot 워크플로가 생겼는지 봄
  - .claude/settings.json, .vscode/tasks.json, .claude/setup.mjs, .vscode/setup.mjs가 이상하게 바뀌었는지 확인함
  - systemd user service의 kitty-monitor, macOS LaunchAgent의 com.user.kitty-monitor, ~/.local/share/kitty/cat.py, ~/.local/bin/gh-token-monitor.sh도 확인해야 함

- 방어는 “설치 전 차단” 쪽으로 가야 함
  - lockfile을 커밋하고, 갑자기 올라온 새 버전으로 자동 해석되지 않게 관리해야 함
  - preinstall 훅 추가, 패키지 크기 급증, maintainer 변화 같은 이상 징후를 CI 전에 잡는 도구가 필요함
  - Docker socket 노출, EC2 메타데이터 접근, GitHub Actions OIDC 권한 범위도 다시 봐야 함

---

## 기술 맥락

- 이번 공격의 핵심은 npm 설치 단계가 사실상 코드 실행 단계라는 점이에요. preinstall 같은 라이프사이클 스크립트는 의존성을 받기 전에 실행되기 때문에, 패키지를 신뢰하면 그 패키지의 설치 스크립트도 같이 신뢰하는 구조가 돼요.

- semver 범위가 위험을 키운 이유도 명확해요. latest 태그가 안 바뀌어도 ^3.0.6 같은 범위는 더 높은 호환 버전을 자동 선택할 수 있거든요. 그래서 공격자가 짧은 시간에 악성 버전을 올리면, 깨끗한 CI 설치나 새 개발자 환경에서 바로 밟을 수 있어요.

- OIDC와 Sigstore가 언급되는 대목은 특히 중요해요. 이 기술들은 장기 토큰을 줄이고 출처를 검증하기 위해 쓰이지만, CI 런타임 자체가 감염되면 그 신뢰 체인이 공격자 손에 들어가요. 그래서 “비밀값을 없앴다”가 아니라 “누가 어떤 조건에서 임시 권한을 받을 수 있나”까지 봐야 해요.

- GitHub orphan commit을 npm 의존성으로 쓰는 방식은 저장소 권한 모델의 빈틈을 찌른 거예요. 원본 저장소 브랜치가 바뀌지 않아도 SHA로 객체를 가져올 수 있으면, npm 입장에서는 정상 GitHub 의존성처럼 처리해요.

- AI 코딩 도구 설정을 노린 것도 새로운 흐름이에요. 개발자가 터미널에서 직접 실행하지 않아도, 에이전트 세션 시작이나 VS Code 폴더 열기 같은 자동화 지점이 실행 표면이 되거든요. 앞으로는 .claude, .codex, .vscode 같은 개발 도구 설정도 소스 코드만큼 리뷰 대상이 돼야 해요.

## 핵심 포인트

- 637개 악성 버전이 22분 동안 자동 배포됐고, size-sensor는 월 420만 다운로드, echarts-for-react는 월 380만 다운로드 규모
- 악성 스크립트는 498KB 난독화 Bun 번들이며 preinstall 훅과 GitHub 의존성 경로를 동시에 사용
- 탈취 대상은 npm 토큰, GitHub PAT, AWS 키, 쿠버네티스 토큰, Vault 토큰, SSH 키, 1Password·Bitwarden·pass·gopass 저장소까지 포함
- GitHub 공개 저장소에 훔친 데이터를 Git 객체로 커밋하고, 동시에 OpenTelemetry 트레이스처럼 위장한 HTTPS 엔드포인트로 전송
- Claude Code, Codex, VS Code 설정을 감염시켜 AI 세션 시작이나 폴더 열기 때 악성코드를 다시 실행하게 만듦
- GitHub Actions OIDC를 npm publish 토큰으로 바꾸고 Sigstore 서명까지 만들어 정상 출처처럼 보이게 할 수 있음

## 인사이트

이 사건은 npm 패키지 몇 개가 뚫린 수준이 아니라 개발자 로컬, CI, 클라우드, 코드 서명, AI 에이전트 설정까지 한 번에 노리는 공급망 공격이다. 특히 semver 범위와 GitHub SHA 의존성, OIDC 신뢰 체인이 공격자의 자동화에 너무 잘 맞물렸다는 게 진짜 무섭다.
