1. What Is It?#
bind_tcp_agent is a Mythic C2 agent that acts as a P2P bridge between Mythic and remote agents (Poseidon, Apollo) already listening on TCP ports. Unlike traditional C2 agents, it generates no binary payload — it is a “virtual callback” that dynamically links to existing agents via TCP and relays C2 traffic.
In simple terms: you deploy this inside Mythic, use the link command to connect it to a Poseidon or Apollo agent listening on a TCP port, and it becomes a transparent relay for tasking, file transfers, interactive shells, reverse port forwards, and SOCKS proxying — all through a single outbound TCP connection.
Key Characteristics#
- Outbound-only connections — bind_tcp_agent connects to agents, not the other way around.
- P2P-aware — Supports multi-hop chains:
Mythic → bind_tcp_agent → Poseidon A → Poseidon B. - SOCKS/HTTP proxy support — Connections can route through SOCKS4, SOCKS5, or HTTP proxies.
- AES-256-CBC + HMAC-SHA256 — Communication can be plaintext, AESPSK-encrypted, or EKE (RSA key exchange → session key).
2. The Problem It Solves#
Mythic C2 has two categories of C2 profiles: egress (agents that phone home to Mythic directly, e.g., HTTP, DNS, SMB) and P2P (agents that route through other agents, e.g., TCP, SMB). Poseidon and Apollo both support TCP as a P2P profile — they listen on a TCP port and wait for a parent to connect and relay tasking.
The challenge: Mythic itself has no built-in egress profile that speaks the TCP P2P protocol. If you deploy a Poseidon agent with a TCP P2P profile inside a target network, Mythic has no way to reach it — Poseidon is listening for an incoming TCP connection, but there’s nothing on Mythic’s side that can initiate one.
bind_tcp_agent solves this by acting as the missing link:
Mythic ←→ bind_tcp_agent (Docker) ←TCP→ Poseidon (target) ←TCP→ Poseidon (target)
(egress) (P2P bridge) (P2P listener) (P2P listener)It lives inside Mythic’s container network, speaks Mythic RPC natively, and initiates outbound TCP connections to remote agents wherever they are.
3. The Link Command — End-to-End Flow#
The link command is the heart of the agent. Here is the complete sequence from operator command to active polling:
Phase 0 — Virtual Callback Creation#
When the operator builds a bind_tcp_agent payload in Mythic:
builder.pycreates a virtual callback viaSendMythicRPCCallbackCreate()- An egress edge is registered (
self → self) so Mythic treats it as a routable callback builder.pyreturnsb""— no binary payload is generated.
Phase 1 — Link Command#
link 192.168.1.100 18888
link.pyparses arguments (IP, port, optional proxy settings)- Calls
BindtcpRPC.Connect()to establish a TCP socket.
Phase 2 — TCP Connection#
The TcpConnection class (connection.py) handles connection management:
- Direct TCP or SOCKS4/5/HTTP proxy (via PySocks)
- TCP_NODELAY, SO_KEEPALIVE (KEEPIDLE=30s, KEEPINTVL=10s, KEEPCNT=3)
- 30-second connect timeout
- Deadline-based
_recv_exact()for resilience against partial reads
Phase 3 — Agent Checkin#
Once connected, ReadAndForwardCheckin() (checkin.py) reads the agent’s first message:
- Receive raw bytes from TCP socket
- Base64 decode the message
- Extract UUID (first 36 bytes) — identifies the agent
- Detect encryption mode by trying to parse the body:
- Try
json.loads()→ if it works, message is plaintext - Try AES-256-CBC decrypt with session key → if it works, EKE mode
- Try AES-256-CBC decrypt with AESPSK → if it works, AESPSK mode
- Try
- Dispatch by action:
"checkin"→ Create callback in Mythic, register P2P edge, start polling"staging_rsa"→ RSA key exchange (EKE), then recurse for encrypted checkin"post_response"/"get_tasking"→ Agent is reconnecting with existing callback
Phase 4 — P2P Edge Registration#
After successful checkin, the P2P edge bind_tcp_agent → remote_agent is registered in Mythic, enabling task routing through the graph.

Phase 5 — Background Polling#
A daemon polling thread is started for the connection, running for its entire lifetime.
4. Encryption Modes#
bind_tcp_agent supports three encryption modes, matching Poseidon’s capabilities.
4.1 Plaintext#
The agent’s message is raw JSON. Used for testing or when encryption is handled at a higher layer.
Wire: base64( UUID(36) + JSON(body) )4.2 AESPSK (Pre-Shared Key)#
Every message is encrypted with AES-256-CBC + HMAC-SHA256.
Wire: base64( UUID(36) + IV(16) + ciphertext + HMAC(32) )The key is looked up from Mythic via GetPayloadEncryptionKey() if not already cached.
4.3 EKE (Encrypted Key Exchange)#
EKE uses RSA-OAEP to exchange a temporary AES-256 session key. The flow matches Poseidon’s Go implementation exactly:
- Agent sends
staging_rsawith its RSA public key (PEM-encoded, PKCS#1 or SPKI format) - bind_tcp_agent generates a random 32-byte session key
- Session key is encrypted with RSA-OAEP (SHA-1) and sent back
- Agent decrypts with its private key
- All subsequent messages use AES-256-CBC + HMAC-SHA256 with the session key
5. The Polling Loop#
Each linked connection gets its own background polling thread. This is where the core work happens. It repeats until disconnected or intentionally unlinked:
- Sleep — waits poll_interval ± jitter seconds (skipped during file transfers for chunk pacing)
- Get tasking from Mythic — calls GetTasksFromMythic() to fetch pending tasks and delegates
- Assemble message — merges Mythic tasks + queued delegate responses + pending rpfwd data into one JSON message, encrypts if needed
- Send to agent — writes it over TCP with the callback UUID prefix
- Read responses — loops reading agent replies (up to 20 normally, 200 during file transfers):
6. P2P Delegates — Multi-Hop C2#
bind_tcp_agent supports arbitrary chaining through intermediate agents. A delegate is a message destined for a child agent, packaged inside the parent’s C2 message.
Outbound (Mythic → Leaf Agent)#
Mythic → bind_tcp_agent → Agent A → Agent B
7. Reconnection Handling#
When a polling thread encounters a read or send error, it enters the reconnection loop:
1. Exponential backoff: 2s, 4s, 8s, 16s, 32s
2. Max 5 retries, then give up
3. Close old socket → Reconnect TCP → Read checkin
4. If same callback UUID → Resume polling
5. If NEW callback UUID (fresh EKE) → Old thread exits
(new polling thread already started in checkin handler)The new-UUID check prevents dual-thread-on-same-socket races: if the remote agent did a fresh EKE key exchange, it gets a new callback UUID and a new polling thread. The old thread detects the mismatch and exits.
State persistence via Mythic’s agentstorage API (save_to_storage / load_from_storage) ensures that keys, callback mappings, and delegate queues survive container restarts:
# State restored on startup:
- Payload keys (AESPSK)
- Session keys (EKE)
- Callback routing mappings
- Temp UUID mappings (for in-progress EKE)
- Delegate response queues
- Connection metadata (for relink)8. Wire Protocol#
TCP Framing (Chunked)#
Messages are split into 30KB chunks with a 12-byte header:
┌──────────┬────────────────┬────────────┬──────────────────┐
│ SIZE (4B)│ TOTAL_CHUNKS │ CHUNK_NUM │ CHUNK_DATA │
│ big-end │ (4B) │ (4B) │ (size - 8 bytes) │
└──────────┴────────────────┴────────────┴──────────────────┘The receiver collects chunks by number and reassembles in order.
Message Format (After Base64 Decode)#
┌────────────────┬──────────────────────────────┬────────────┐
│ UUID (36 bytes)│ IV (16B) [if encrypted] │ HMAC-SHA256│
│ │ + Ciphertext / Plaintext │ (32B) │
│ │ JSON body │ │
└────────────────┴──────────────────────────────┴────────────┘Encryption Decision Tree#
Incoming Message
├── Try json.loads() on body → Success? → Plaintext, process directly
└── Body is encrypted:
├── Session key exists? → aes_decrypt(session_key)
├── Payload key cached? → aes_decrypt(payload_key)
└── Neither? → GetPayloadEncryptionKey() from Mythic RPCP2P Delegate Wrapping#
Delegates are base64-encoded messages wrapped inside the parent’s C2 message:
{
"action": "get_tasking",
"delegates": [
{
"uuid": "<connection_uuid>",
"message": "<base64( child_uuid(36) + encrypted_body )>",
"c2_profile": "tcp"
}
]
}9. Building and Deploying#
Requirements#
- Mythic C2 3.4+ (Docker-based deployment)
- Python 3.11 container (built from source in Docker)
Build#
# From the Mythic server:
sudo /opt/Mythic/mythic-cli install folder -f /path/to/bind_tcp_agentThis installs the payload type, C2 profile, and translator into Mythic’s container network.
Dependencies#
All dependencies are installed in the multi-stage Docker build:
mythic-container==0.6.9— Mythic Python SDKpycryptodome— AES-256-CBC encryptioncryptography— RSA-OAEP, X.509, ASN.1 parsingPySocks— SOCKS4/5 and HTTP proxy supportasn1crypto— PKCS#1 RSAPublicKey parsing
Supported Agents#
- Poseidon (Linux, macOS) — TCP profile
- Apollo (C#, Windows) — TCP profile
Usage#
| Command | Description |
|---|---|
link <ip> <port> [proxy_host proxy_port [proxy_type]] | Connect to remote agent |
sleep <callback_uuid> <interval> [jitter] | Set polling interval |
unlink <target_uuid> | Disconnect and clean up |
relink <agent_callback_uuid> | Reconnect from stored metadata |
10. Conclusion#
bind_tcp_agent fills a specific gap in the Mythic ecosystem: the ability to bridge Mythic’s egress network with TCP-based P2P agents through an outbound connection.
The project is open source and available for the Mythic community. Contributions and bug reports are welcome: https://github.com/olegsenko/bind_tcp_agent/
