Axios npm 패키지 공급망 공격 분석 — 2시간 54분 동안 벌어진 일
3월 마지막 주말, 개발자들을 긴장하게 만든 사건
지난 3월 31일 새벽, JavaScript 생태계에 작은 지진이 일어났습니다. 주간 다운로드가 1억 건에 달하는 axios 패키지가 공격을 받았다는 소식이었죠. 이런 류의 소식은 사실 매주 어딘가에서 뜹니다. 평소 같으면 '또 하나 떴구나' 하고 넘겼을 거예요. 그런데 이번엔 손이 멈췄습니다. 공격의 정교함, 그리고 배후 분석 결과를 보니 단순한 장난이 아니었거든요.
2시간 54분. 악성 버전이 npm에 올라가 있던 시간입니다. 숫자만 보면 짧습니다. 그런데 axios를 의존성으로 끌어다 쓰는 프로젝트가 전 세계에 얼마나 깔려 있는지를 떠올려보면 얘기가 달라져요. 이 시간 동안 돌아간 CI/CD 파이프라인이 얼마나 많았을지, 그게 다 잠재적 감염 경로였다는 뜻이니까요. 짧은 시간이 곧 작은 피해라는 등식은 공급망 공격에서는 성립하지 않습니다.

사건의 전개 과정
사전 준비 단계 (3월 30일)
공격자들은 하루 전부터 판을 깔았습니다. **[email protected]**라는 이름의 패키지를 미리 배포해둔 거죠. 이름만 보면 crypto-js를 흉내 낸 것 같은데, 안에는 악성 코드가 들어 있었습니다.
이런 사전 배치는 탐지를 우회하기 위한 전형적인 수법이에요. 새로 만든 패키지가 갑자기 axios 의존성에 추가되면 누구라도 의심합니다. 그런데 하루 전에 이미 올라와 있던 패키지라면? 타임라인상 '원래 있던 것'처럼 보이거든요. 침입 자체보다 침입의 흔적을 평범하게 위장하는 데 더 공을 들인 셈입니다.
본격적인 공격 (3월 31일 00:21 UTC)
공격의 핵심은 jasonsaayman 계정 탈취였습니다. axios의 핵심 관리자 계정이죠. 공격자들은 이 계정의 npm 장기 액세스 토큰을 확보했고, 계정 이메일까지 자신들의 Proton Mail 주소로 바꿔버렸습니다. 이메일을 바꿨다는 건 단순 침입이 아니라 계정 자체를 통째로 가져가려 했다는 신호예요.
여기서 흥미로운 지점이 하나 있습니다. 해당 계정은 OIDC Trusted Publishing이 활성화되어 있었는데도 공격이 통했다는 점이에요. OIDC가 걸려 있으면 토큰 없이도 신뢰 기반으로 게시되니까 더 안전할 것 같잖아요. 그런데 왜 뚫렸을까요?
문제는 워크플로우에서 NPM_TOKEN 환경 변수를 함께 들고 있었다는 데 있습니다. npm은 OIDC보다 NPM_TOKEN을 우선시해요. 즉 둘 다 존재하면 토큰이 이깁니다. 공격자가 탈취한 토큰 하나로 OIDC라는 보안 장치를 사실상 무력화한 거죠.
이게 운영하는 입장에서 제일 뼈아픈 부분입니다. 보안 기능을 켜는 것과 그 기능이 실제로 효력을 갖도록 구성하는 것은 다른 문제예요. OIDC를 도입했다는 만족감이 오히려 '우린 안전하다'는 착각을 만들고, 그 옆에 남겨둔 레거시 토큰 하나가 모든 걸 되돌립니다. 새 자물쇠를 달아놓고 옛날 열쇠를 현관 매트 밑에 그대로 둔 격이죠.
악성 버전 배포 전략
공격자들은 axios 1.14.1과 axios 0.30.4 두 버전을 배포했습니다. 그런데 이걸 GitHub Actions를 거치지 않고 npm CLI로 직접 올렸어요. 그 결과 GitHub 저장소에는 어떤 흔적도 남지 않았습니다. 소스를 보고 검증하던 사람들 눈에는 아무 일도 일어나지 않은 것처럼 보였다는 뜻이죠.
더 교묘한 건 태그 운영입니다. latest와 legacy 태그 양쪽에 모두 악성 버전을 지정해서, 신버전을 쓰든 구버전을 쓰든 npm install axios를 실행하면 악성 버전이 떨어지도록 만들었어요. package.json에는 plain-crypto-js만 슬쩍 추가된 형태라, 일반적인 diff 분석으로는 잡아내기 어려웠을 겁니다. 의존성 한 줄이 늘어난 걸 누가 매번 의심하면서 보겠어요.
악성 코드의 동작 방식
postinstall 훅을 통한 실행
패키지가 설치되면 plain-crypto-js의 postinstall 스크립트가 자동으로 실행됩니다. npm 생태계에서 postinstall은 오래된 공격 표면이에요. 설치만 해도 임의 코드가 돌아가니까요. 이때 난독화된 setup.js가 백그라운드에서 운영체제를 식별하고, 플랫폼에 맞는 백도어를 내려받아 실행하는 구조였습니다.
플랫폼별 감염 방식
| OS | 동작 방식 | 저장 경로 |
|---|---|---|
| Windows | PowerShell을 %PROGRAMDATA%\wt.exe로 복사 후 숨김 실행 |
%TEMP%\6202033.ps1 |
| macOS | curl로 Mach-O 바이너리 다운로드 후 백그라운드 실행 | /Library/Caches/com.apple.act.mond |
| Linux | Python 백도어 스크립트 다운로드 후 실행 | /tmp/ld.py |
세 플랫폼을 모두 커버한다는 것 자체가 손이 많이 가는 작업입니다. 일회성 장난이 아니라, 어떤 개발자가 걸려들든 잡겠다는 의도가 읽혀요. 모든 페이로드는 sfrclak[.]com:8000 서버와 통신하며 시스템 정보를 수집하고, 추가 명령을 받을 수 있는 형태였습니다. 단순 정보 탈취를 넘어 원격 제어 채널을 확보하려 한 거죠.
흔적 지우기
정말 치밀했던 건 anti-forensics 기능입니다. 드로퍼는 임무를 마치면 스스로를 삭제하고, 변조했던 package.json도 원본으로 되돌려놓습니다. 감염 이후 시점에 들어가서 포렌식을 돌려도 깨끗해 보이게 만들어 놓은 거예요.
이 지점에서 대응 난이도가 한 단계 올라갑니다. '뭔가 이상하다'는 의심이 들어 확인하러 들어갔을 때 현장이 이미 정리되어 있으면, 감염 여부를 파일 존재 여부로 판단하기가 어려워져요. 그래서 이런 사건은 사후 탐지보다 설치 시점의 로그, 네트워크 트래픽 같은 휘발성 증거가 더 중요해집니다.
배후 분석 - UNC1069
Google의 Threat Intelligence Group은 이번 공격을 UNC1069라는 북한 연계 위협 행위자의 소행으로 귀속했습니다. 근거는 다음과 같습니다.
- WAVESHAPER.V2 백도어가 과거 UNC1069가 쓰던 WAVESHAPER의 업데이트 버전
- C2 인프라가 이들의 과거 캠페인과 연결점을 보임
- 2018년부터 활동해온 금전 목적의 그룹으로, 주로 암호화폐와 AI 분야를 노림
개인적으로 마음에 걸리는 건 국가 차원의 행위자가 오픈소스 생태계를 정조준하고 있다는 사실입니다. 예전엔 이런 그룹들이 특정 기업이나 금융망을 직접 노렸어요. 그런데 npm 같은 중앙 레지스트리를 치면, 패키지 하나로 그 위에 얹힌 수만 개의 프로젝트를 한 번에 노출시킬 수 있습니다. 개별 회사를 한 곳씩 뚫는 것보다 훨씬 효율이 좋은 거죠.
공격자 입장에서 axios는 매력적인 표적입니다. 다운로드 1억 건짜리 패키지는 그 자체로 거대한 배포 인프라예요. 방어하는 쪽은 자기 코드만 지키면 됐던 시대가 지났다는 뜻이기도 하고요.

즉시 해야 할 일들
혹시 3월 31일 새벽에 axios를 설치했거나 그 시간대에 CI/CD가 돌았다면, 아래 순서로 점검해보는 걸 권합니다.
1. 버전 확인 및 업데이트
npm list axios
npm audit
axios 1.14.1이나 0.30.4가 잡히면 즉시 안전한 버전(1.7.8 등)으로 교체해야 합니다. 락파일까지 같이 확인하세요. package.json만 바꾸고 락파일을 안 갈면 그대로 악성 버전이 다시 깔립니다.
2. 자격증명 전면 교체
해당 시간대(3월 31일 00:21–03:15 UTC)에 걸쳐 있는 환경이라면, 모든 시크릿과 API 키를 폐기하고 새로 발급받으세요. 백도어가 이미 수집했다는 전제로 움직이는 게 안전합니다. '아마 안 털렸을 것'이라는 가정은 운영에서 제일 위험한 가정이에요.
3. 시스템 점검
다음 파일들이 남아 있는지 확인합니다.
- Windows:
%PROGRAMDATA%\wt.exe - macOS:
/Library/Caches/com.apple.act.mond - Linux:
/tmp/ld.py
다만 앞서 말한 anti-forensics 때문에 파일이 없다고 안심할 수는 없어요. 파일이 발견되면 감염 확정이지만, 발견되지 않았다고 깨끗하다는 보장은 안 됩니다.
4. 전이 의존성 감사
axios를 직접 import하지 않더라도, 다른 패키지가 간접적으로 끌어왔을 수 있습니다. 직접 의존성만 보면 놓쳐요. 전체 의존성 트리를 펼쳐서 점검하세요.
5. npm 계정 보안 강화
패키지 관리자라면 장기 액세스 토큰을 폐기하고, OIDC를 쓴다면 워크플로우에서 NPM_TOKEN을 아예 제거해야 합니다. 이번 사건의 핵심이 바로 이 둘의 공존이었으니까요. MFA는 기본이고요.

마치며
이번 사건에서 제일 오래 곱씹게 된 건 공격 기법 자체가 아니었습니다. OIDC라는 진보된 보안 기능을 켜두고도, 그 옆에 남겨둔 토큰 하나 때문에 전부 우회당했다는 구조였어요. 보안 기능을 추가하는 것과 보안 수준이 올라가는 것은 같은 말이 아니라는 걸, 이만큼 선명하게 보여주는 사례도 드물죠.
2시간 54분. 패치를 공급하는 주체가 아니라, 패키지를 게시하는 신뢰의 사슬에서 가장 약한 칸이 끊긴 시간이었습니다. 공급망 공격은 결국 가장 약한 고리를 찾는 게임이에요. 그리고 그 고리는 대개 우리가 '이건 켜놨으니 됐다'고 안심한 바로 그 옆에 있습니다.