Axios npm 공급망 공격 분석

Axios npm 패키지 공급망 공격 사례와 postinstall 기반 악성코드 실행 흐름 분석

Malware

최근 Axios npm 패키지 공급망 공격 사례를 보게 되어 정리해보려고 한다.

이번 공격은 Axios 소스코드 자체를 크게 수정한 것이 아니라, package.json에 악성 의존성을 추가하고 해당 패키지의 postinstall 스크립트를 이용해 악성코드를 실행하는 방식이다.

단순히 npm install axios를 실행하는 것만으로도 악성코드가 실행될 수 있다는 점에서 꽤 위험한 사례로 보인다.

본 글은 Datadog Security Labs, Elastic Security Labs, StepSecurity, Google GTIG 등 공개 분석 자료를 바탕으로 정리하였다. 실제 악성 샘플은 반드시 격리된 분석 환경에서만 다뤄야 한다.

공격 흐름

전체 공격 흐름을 간단히 보면 아래와 같다. 01_attack_flow.svg

1
2
3
4
5
6
7
8
9
npm install axios
    └─ axios@1.14.1 설치
        └─ package.json에 plain-crypto-js 의존성 추가
            └─ plain-crypto-js@4.2.1 설치
                └─ postinstall hook 실행
                    └─ setup.js 실행
                        ├─ macOS   → /Library/Caches/com.apple.act.mond 실행
                        ├─ Windows → wt.exe로 위장한 PowerShell RAT 실행
                        └─ Linux   → /tmp/ld.py Python RAT 실행

핵심은 Axios 코드가 애플리케이션에서 실행될 때 감염되는 것이 아니라, 패키지를 설치하는 과정에서 감염이 시작된다는 점이다.

npm 패키지는 설치 과정에서 postinstall 같은 lifecycle script를 실행할 수 있다. 이 기능 자체는 정상적인 용도로도 사용되지만, 공급망 공격에서는 사용자가 의식하지 못하는 코드 실행 지점이 될 수 있다.

package.json 변조

이번 공격에서 가장 눈에 띄는 부분은 Axios 원본 코드가 아니라 package.json이다.

악성 버전에서는 의존성에 plain-crypto-js가 추가되어 있었다.

1
2
3
4
5
6
 "dependencies": {
   "follow-redirects": "^1.15.6",
   "form-data": "^4.0.0",
   "proxy-from-env": "^1.1.0",
+  "plain-crypto-js": "^4.2.1"
 }

이 부분이 중요한 이유는 Axios 소스코드를 직접 수정하지 않아도 된다는 점이다.

일반적으로 유명 오픈소스 패키지의 소스코드 변경은 리뷰나 diff 분석 과정에서 눈에 띌 수 있다. 하지만 의존성 한 줄이 추가되는 형태라면 상대적으로 덜 의심받을 수 있다.

또한 plain-crypto-js는 Axios 코드에서 직접 import되지 않아도 설치된다. 그리고 이 패키지에 postinstall이 포함되어 있다면, 설치되는 순간 악성코드 실행이 가능하다.

즉, 공격자는 애플리케이션 실행 경로가 아니라 패키지 설치 경로를 공격면으로 사용한 것으로 볼 수 있다.

npm 레지스트리 메타데이터

공개 분석 자료를 보면 정상 버전과 악성 버전은 npm registry metadata에서도 차이가 확인된다.

정상 버전은 GitHub Actions OIDC 기반 trusted publishing을 통해 배포된 것으로 보인다.

1
2
3
4
5
6
7
8
9
{
  "_npmUser": {
    "name": "GitHub Actions",
    "email": "npm-oidc-no-reply@github.com",
    "trustedPublisher": {
      "id": "github"
    }
  }
}

반면 악성 버전은 maintainer 계정으로 직접 배포된 형태로 보인다.

1
2
3
4
5
6
{
  "_npmUser": {
    "name": "jasonsaayman",
    "email": "ifstap@proton.me"
  }
}

정상 릴리즈는 GitHub Actions OIDC 기반으로 배포되었지만, 악성 버전은 trusted publisher 정보가 없는 형태로 보인다. 따라서 공격자는 Axios GitHub 저장소를 직접 수정했다기보다는 npm 배포 권한 또는 장기 보존 npm token을 탈취해 악성 버전을 배포한 것으로 추정된다.

이런 경우에는 소스 저장소만 보는 것으로는 부족하고, npm registry metadata, GitHub tag, changelog, tarball diff를 함께 봐야 한다.

plain-crypto-js와 postinstall

03_postinstall_mechanism.svg

악성 의존성으로 추가된 plain-crypto-js@4.2.1의 핵심은 postinstall 스크립트이다.

1
2
3
4
5
6
7
{
  "name": "plain-crypto-js",
  "version": "4.2.1",
  "scripts": {
    "postinstall": "node setup.js"
  }
}

postinstall은 npm 패키지 설치가 끝난 뒤 자동으로 실행된다.

그래서 사용자가 직접 plain-crypto-js를 실행하지 않아도, Axios를 설치하는 과정에서 간접 의존성으로 설치되고 setup.js가 실행될 수 있다.

정리하면 아래와 같다.

1
2
3
4
5
사용자: npm install axios 실행
npm: axios 의존성 설치
npm: plain-crypto-js 설치
npm: plain-crypto-js의 postinstall 실행
결과: setup.js 실행

이런 흐름 때문에 npm 공급망 공격에서는 단순히 소스코드만 보는 것이 아니라, package.json의 lifecycle script를 반드시 확인해야 한다.

setup.js 드로퍼 분석

공개 분석에서는 setup.js를 SILKBELL로 언급하고 있다. 여기서는 편하게 setup.js 드로퍼라고 부르겠다.

확인된 해시는 아래와 같다.

1
SHA256: e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09

난독화 방식

setup.js는 문자열을 그대로 노출하지 않기 위해 간단한 난독화를 사용한다.

대략적인 구조는 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _trans_1(str, key) {
    const digits = key.split("").map(Number);
    let result = "";
    for (let i = 0; i < str.length; i++) {
        const d = digits[7 * i * i % 10] || 0;
        result += String.fromCharCode(str.charCodeAt(i) ^ d ^ 333);
    }
    return result;
}

function _trans_2(encoded, key) {
    const reversed = encoded.split("").reverse().join("").replaceAll("_", "=");
    const decoded = Buffer.from(reversed, "base64").toString("utf8");
    return _trans_1(decoded, key);
}

예를 들어 아래 값들은 복호화 후 fs, os로 변환된다.

1
2
__gvEvKx  → fs
__gvELKx  → os

이를 통해 require("fs"), require("os"), child_process 같은 민감 모듈명을 정적 분석에서 숨기려고 한 것으로 보인다.

다만 난독화 자체는 복잡하지 않다. 오히려 OrDeR_7077 같은 고정 문자열은 탐지 시그니처로 활용할 수 있어 보인다.

OS별 페이로드 다운로드

난독화를 해제하면 setup.js는 OS를 확인한 뒤 각 플랫폼에 맞는 페이로드를 다운로드하고 실행한다.

1
2
3
macOS   → curl + osascript
Windows → VBScript + PowerShell
Linux   → curl + python3

C2 URL은 아래와 같이 사용된 것으로 보인다.

1
http://sfrclak.com:8000/6202033

각 OS별 특징은 다음과 같다.

OS 실행 방식 저장/실행 경로 특징
macOS osascript로 AppleScript 실행 /Library/Caches/com.apple.act.mond Apple daemon처럼 보이는 이름 사용
Windows VBScript로 PowerShell 실행 %PROGRAMDATA%\wt.exe, %TEMP%\6202033.ps1 powershell.exe를 wt.exe로 복사해 위장
Linux Python RAT 다운로드 후 실행 /tmp/ld.py 단순 임시 파일명 사용

POST body에는 아래와 같은 문자열이 사용된다.

1
2
3
packages.npm.org/product0  # macOS
packages.npm.org/product1  # Windows
packages.npm.org/product2  # Linux

npm 관련 트래픽처럼 보이도록 위장하려는 의도로 보인다.

설치 후 흔적 제거

이 공격에서 흥미로운 점은 설치 후 흔적을 지운다는 것이다.

1
2
3
fs.unlink(__filename, () => {});
fs.unlink("package.json", () => {});
fs.rename("package.md", "package.json", () => {});

동작을 정리하면 아래와 같다.

1
2
3
1. setup.js 자기 삭제
2. postinstall이 포함된 package.json 삭제
3. 정상처럼 보이는 package.md를 package.json으로 교체

그래서 설치가 끝난 뒤 node_modules/plain-crypto-js를 확인하면 setup.js와 악성 package.json이 사라져 있을 수 있다.

이 경우 node_modules만 보고 안전하다고 판단하면 놓칠 수 있다. 오히려 lockfile과 설치 로그가 더 중요한 증거가 된다.

1
git log -p -- package-lock.json | grep plain-crypto-js

OS별 RAT 동작

05_os_payloads.svg

Stage 2 페이로드는 OS별로 구현체가 다르지만, 전체적인 목적은 비슷하다.

  • 시스템 정보 수집
  • 프로세스 목록 수집
  • 디렉터리 목록 수집
  • C2 통신
  • 명령 실행
  • 추가 페이로드 실행

Linux

Linux에서는 /tmp/ld.py 경로에 Python RAT이 저장되는 것으로 보인다.

C2 통신은 대략 아래와 같은 구조이다.

1
2
3
4
5
6
시스템 정보 수집
→ JSON 생성
→ Base64 인코딩
→ data= 파라미터로 HTTP POST 전송
→ C2 응답 수신
→ 명령 처리

특징적으로 아래 User-Agent를 사용한다.

1
mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)

Linux나 macOS 환경에서 Windows XP / IE8 User-Agent가 보인다면 충분히 의심해볼 수 있다.

Windows

Windows에서는 PowerShell 기반 RAT이 사용된다.

특히 powershell.exe%PROGRAMDATA%\wt.exe로 복사하여 Windows Terminal처럼 보이게 하려는 시도가 있다.

또한 Run key 등록을 통해 persistence를 구성한다.

1
2
3
HKCU:\Software\Microsoft\Windows\CurrentVersion\Run
Name: WindowsUpdateHelper
Value: %PROGRAMDATA%\wt.exe -w hidden -ep bypass -file <script path>

따라서 Windows 환경에서는 파일 아티팩트뿐 아니라 Run key도 확인해야 한다.

macOS

macOS에서는 C++ 기반 RAT이 사용된 것으로 보인다.

주요 특징은 아래와 같다.

1
2
3
4
5
- Universal Binary 형태
- Intel Mac / Apple Silicon 모두 대응
- /Library/Caches/com.apple.act.mond 경로 사용
- libcurl 기반 C2 통신
- 시스템 정보와 프로세스 목록 수집

/Library/Caches/com.apple.act.mond라는 이름은 Apple 시스템 데몬처럼 보이도록 위장한 것으로 보인다.

탐지 포인트? Atifact?

개발 환경이나 CI 서버에서 아래 흔적을 확인해볼 수 있다.

구분
악성 의존성 plain-crypto-js
의심 Axios 버전 axios@1.14.1, axios@0.30.4
C2 도메인 sfrclak.com
C2 포트 8000
C2 IP 142.11.206.73
User-Agent mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
macOS 파일 /Library/Caches/com.apple.act.mond
Linux 파일 /tmp/ld.py
Windows 파일 %PROGRAMDATA%\wt.exe, %TEMP%\6202033.ps1
Windows Run key WindowsUpdateHelper

lockfile에서는 아래와 같이 확인해볼 수 있다.

1
2
grep -r "plain-crypto-js\|\"axios\": \"1\.14\.1\"\|\"axios\": \"0\.30\.4\"" \
  package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null

macOS/Linux에서는 아래 파일도 확인해볼 수 있다.

1
2
[ -f "/Library/Caches/com.apple.act.mond" ] && echo "macOS RAT artifact found"
[ -f "/tmp/ld.py" ] && echo "Linux RAT artifact found"

Windows에서는 아래 항목을 확인해볼 수 있다.

1
2
3
Test-Path "$env:PROGRAMDATA\wt.exe"
Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" |
  Select-Object WindowsUpdateHelper

다만 파일이 없다고 해서 무조건 안전하다고 보기는 어렵다. 설치 후 자기 삭제나 실패한 실행, 로그 삭제 가능성이 있기 때문에 lockfile, CI 로그, 네트워크 로그를 함께 봐야 한다.

대응 방안

이번 사례에서 생각해볼 수 있는 대응 방안은 아래와 같다.

구분 대응
패키지 발행 npm trusted publishing / OIDC 사용
토큰 관리 장기 보존 npm token 제거 또는 최소 권한 적용
의존성 관리 lockfile 커밋, version pinning, caret 범위 최소화
설치 스크립트 CI에서 --ignore-scripts 적용 검토
변경 검토 신규 의존성 추가, maintainer 변경, postinstall 추가 모니터링
영향 확인 개발자 장비와 CI runner의 설치 로그/네트워크 로그 확인
사고 대응 npm/GitHub/cloud credential 회전

특히 공급망 공격에서는 개발자 PC뿐 아니라 CI/CD 서버도 같이 봐야 한다. CI에서 악성 패키지가 설치되었다면 빌드 환경의 토큰이나 배포 권한이 함께 노출되었을 가능성도 있기 때문이다.

영향도

개인적으로 이 공격은 단순히 특정 패키지 하나의 문제라기보다, npm 생태계의 구조적인 공격면을 잘 보여주는 사례로 보인다.

특히 아래 조건이 겹치면 영향이 커질 수 있다.

  • 유명 패키지에 악성 버전이 배포됨
  • semver 범위 지정으로 자동 업데이트 가능
  • CI에서 lifecycle script가 허용됨
  • lockfile 없이 의존성을 매번 새로 해석함
  • 개발자 장비나 CI runner에 민감한 credential이 존재함

즉, 실제 피해 여부는 악성 버전을 설치했는지뿐 아니라, 설치된 환경에 어떤 권한이 있었는지에 따라 달라질 수 있다.

결론

이번 Axios npm 공급망 공격은 유명 패키지라고 해서 무조건 안전하다고 보기 어렵다는 것을 보여주는 사례이다.

특히 소스코드가 직접 변경되지 않더라도, package.json의 의존성 하나만으로 설치 과정에서 악성코드가 실행될 수 있다는 점이 위험해 보인다.

정리하면 중요한 부분은 아래와 같다.

1
2
3
4
5
1. 소스코드 diff만 보면 놓칠 수 있다.
2. package.json과 lockfile 변경을 반드시 확인해야 한다.
3. postinstall 같은 lifecycle script는 강력한 코드 실행 지점이다.
4. npm token 같은 장기 credential은 공급망 공격의 핵심 타깃이 될 수 있다.
5. 개발자 PC와 CI/CD 환경은 모두 영향 범위에 포함해야 한다.

공급망 공격을 막기 위해서는 단순히 취약점 스캐너만 돌리는 것보다, 패키지가 어떻게 발행되었는지, 어떤 의존성이 추가되었는지, 설치 시점에 어떤 스크립트가 실행되는지를 함께 확인해야 할 것으로 보인다.

참고

  • Datadog Security Labs
  • Elastic Security Labs
  • StepSecurity
  • Google GTIG
  • N3mes1s GitHub Gist

Comments