Skip to main content

Overview

The C2 framework uses TLS certificates for two purposes:
  1. Encryption: Protect beacon traffic from network interception
  2. Certificate Pinning: Prevent man-in-the-middle attacks by validating the server’s certificate fingerprint
The agent pins the server certificate at build time. If you regenerate the certificate, you must rebuild the agent executable with the new certificate or it will refuse to connect.

Certificate Requirements

The TLS certificate must meet the following requirements:
RequirementValueReason
Key size4096 bits RSAAdequate security for lab environment
Validity365 daysLong enough for extended testing
Subject CNc2.lab.internalMatches the DNS name in the agent config
SAN extensionDNS:c2.lab.internal, IP:192.168.100.10Allows connection by hostname or IP
Self-signedYesNo trusted CA required for lab

Generating Certificates

1

Create certificate directory

cd /home/c2server/c2-framework
mkdir -p certs
2

Generate self-signed certificate

openssl req -x509 -newkey rsa:4096 \
  -keyout certs/server.key -out certs/server.crt \
  -days 365 -nodes \
  -subj "/CN=c2.lab.internal" \
  -addext "subjectAltName=DNS:c2.lab.internal,IP:192.168.100.10"
Parameters explained:
  • -x509: Generate a self-signed certificate (not a CSR)
  • -newkey rsa:4096: Create a new 4096-bit RSA private key
  • -keyout: Path to save the private key
  • -out: Path to save the certificate
  • -days 365: Certificate valid for 1 year
  • -nodes: Do not encrypt the private key (“no DES”)
  • -subj: Certificate subject (Common Name)
  • -addext: Subject Alternative Name extension
3

Set file permissions

The private key must be readable by the C2 server process:
chmod 644 certs/server.crt
chmod 640 certs/server.key
For Docker deployment (UID 1000):
chown c2server:c2server certs/server.{crt,key}
For bare-metal deployment with Nginx:
sudo chown root:www-data certs/server.key
4

Verify certificate details

Inspect the generated certificate:
openssl x509 -in certs/server.crt -text -noout
Confirm the following fields:
Subject: CN = c2.lab.internal
X509v3 Subject Alternative Name:
    DNS:c2.lab.internal, IP Address:192.168.100.10
Validity
    Not Before: ...
    Not After : ... (365 days from generation)

Certificate Pinning in the Agent

The agent validates the server certificate by comparing its SHA-256 fingerprint to a pinned value embedded at build time.

Extract Certificate Fingerprint

Generate the SHA-256 fingerprint of the server certificate:
openssl x509 -in certs/server.crt -noout -fingerprint -sha256
Example output:
SHA256 Fingerprint=A1:B2:C3:D4:E5:F6:...

Pin Certificate in Agent

The agent’s TLS wrapper (transport/tls_wrapper.py) validates the server certificate against the pinned fingerprint:
# transport/tls_wrapper.py:23
PINNED_CERT_SHA256 = "A1:B2:C3:D4:E5:F6:..."

def verify_cert_pinning(cert_der: bytes) -> bool:
    """Verify server cert matches pinned fingerprint."""
    actual = hashlib.sha256(cert_der).hexdigest().upper()
    expected = PINNED_CERT_SHA256.replace(":", "")
    return actual == expected
If you regenerate the server certificate, you must update PINNED_CERT_SHA256 in transport/tls_wrapper.py and rebuild the agent executable.

Certificate Deployment

Docker Compose

The certificate is mounted into both containers via volume mounts:
# docker-compose.yml
services:
  c2-server:
    volumes:
      - ./certs:/app/certs:ro  # Server reads from /app/certs/

  nginx:
    volumes:
      - ./certs:/etc/nginx/certs:ro  # Nginx reads from /etc/nginx/certs/
Nginx configuration references the mounted paths:
# redirector/nginx_docker.conf
ssl_certificate     /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;

Bare-Metal

For bare-metal deployment, Nginx reads certificates from the repository:
# redirector/nginx_example.conf
ssl_certificate     /home/c2server/c2-framework/certs/server.crt;
ssl_certificate_key /home/c2server/c2-framework/certs/server.key;
Ensure Nginx has read permissions:
sudo chmod 644 /home/c2server/c2-framework/certs/server.crt
sudo chmod 640 /home/c2server/c2-framework/certs/server.key
sudo chown root:www-data /home/c2server/c2-framework/certs/server.key

Testing TLS Connection

Verify TLS Handshake

Test TLS connection without certificate validation:
openssl s_client -connect c2.lab.internal:443 -servername c2.lab.internal
Expected output:
Certificate chain
 0 s:CN = c2.lab.internal
   i:CN = c2.lab.internal
   a:PKEY: rsaEncryption, 4096 (bit); sigalg: RSA-SHA256
   v:NotBefore: ...; NotAfter: ...

Verify Certificate from Agent VM

From the Windows VM, test TLS connection:
$cert = [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
Invoke-WebRequest -Uri https://c2.lab.internal/beacon -Method POST

Verify Certificate Pinning

The agent will log a certificate pinning failure if the fingerprint doesn’t match:
{"level": "error", "message": "cert pinning failed", "expected": "A1B2C3...", "actual": "D4E5F6..."}
Check agent logs (logs/agent.log) for this error after starting the agent.

Certificate Rotation

1

Generate new certificate

cd /home/c2server/c2-framework
mv certs/server.crt certs/server.crt.old
mv certs/server.key certs/server.key.old

openssl req -x509 -newkey rsa:4096 \
  -keyout certs/server.key -out certs/server.crt \
  -days 365 -nodes \
  -subj "/CN=c2.lab.internal" \
  -addext "subjectAltName=DNS:c2.lab.internal,IP:192.168.100.10"
2

Extract new fingerprint

openssl x509 -in certs/server.crt -noout -fingerprint -sha256
Copy the fingerprint output (e.g., A1:B2:C3:...).
3

Update agent pinned certificate

Edit transport/tls_wrapper.py and update the pinned fingerprint:
PINNED_CERT_SHA256 = "A1:B2:C3:D4:E5:F6:..."  # New fingerprint
4

Rebuild agent executable

Rebuild the agent with the new pinned certificate:
cd /home/c2server/c2-framework
python -m agent.builder.builder
Deploy the new executable to the Windows VM.
5

Restart C2 server

For Docker deployment:
docker compose restart
For bare-metal deployment:
sudo systemctl restart nginx
# Restart server process (depends on your process manager)
Critical: Existing agents with the old pinned certificate will fail to connect after rotating the certificate. You must deploy the rebuilt agent to all victim machines.

Git and Certificate Security

Committed to Git

  • certs/server.crtCommitted to the repository
    • Required for agent build process (certificate pinning)
    • Not sensitive (public key)

Excluded from Git

  • certs/server.keyNever committed (listed in .gitignore)
    • Contains the private key
    • Compromising this allows impersonation of the C2 server
Verify .gitignore includes:
certs/server.key

Troubleshooting

SymptomCauseFix
Permission denied reading keyWrong file permissionsRun chmod 640 certs/server.key
certificate verify failed from agentPinned fingerprint mismatchUpdate PINNED_CERT_SHA256 in tls_wrapper.py
No such file or directory: server.crtMissing certificateRegenerate certificate with openssl
unable to load Private KeyEncrypted private keyRegenerate with -nodes flag
Wrong version number from agentAgent using HTTP instead of HTTPSVerify agent config uses https://
SSL handshake failedTLS version mismatchEnable TLS 1.2+ in nginx config

Production Considerations

This guide describes self-signed certificates for lab use only. In production:
  • Use certificates signed by a trusted CA (Let’s Encrypt, DigiCert, etc.)
  • Implement certificate rotation with a 90-day validity period
  • Store private keys in a hardware security module (HSM) or secrets manager
  • Use domain fronting or CDN to hide the real C2 infrastructure
  • Never reuse certificates across multiple C2 servers

Next Steps

Build docs developers (and LLMs) love