Skip to main content
  1. Articles/

bind_tcp_agent: a Mythic P2P bridge for TCP-based C2

·7 mins·
Table of Contents

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:

  1. builder.py creates a virtual callback via SendMythicRPCCallbackCreate()
  2. An egress edge is registered (self → self) so Mythic treats it as a routable callback
  3. builder.py returns b"" — no binary payload is generated.

Phase 1 — Link Command#

link 192.168.1.100 18888
Link command
  1. link.py parses arguments (IP, port, optional proxy settings)
  2. 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:

  1. Receive raw bytes from TCP socket
  2. Base64 decode the message
  3. Extract UUID (first 36 bytes) — identifies the agent
  4. 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
  5. 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.

Edge registration

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:

  1. Agent sends staging_rsa with its RSA public key (PEM-encoded, PKCS#1 or SPKI format)
  2. bind_tcp_agent generates a random 32-byte session key
  3. Session key is encrypted with RSA-OAEP (SHA-1) and sent back
  4. Agent decrypts with its private key
  5. 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:

  1. Sleep — waits poll_interval ± jitter seconds (skipped during file transfers for chunk pacing)
  2. Get tasking from Mythic — calls GetTasksFromMythic() to fetch pending tasks and delegates
  3. Assemble message — merges Mythic tasks + queued delegate responses + pending rpfwd data into one JSON message, encrypts if needed
  4. Send to agent — writes it over TCP with the callback UUID prefix
  5. 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
P2P delegates

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 RPC

P2P 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_agent

This 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 SDK
  • pycryptodome — AES-256-CBC encryption
  • cryptography — RSA-OAEP, X.509, ASN.1 parsing
  • PySocks — SOCKS4/5 and HTTP proxy support
  • asn1crypto — PKCS#1 RSAPublicKey parsing

Supported Agents
#

  • Poseidon (Linux, macOS) — TCP profile
  • Apollo (C#, Windows) — TCP profile

Usage
#

CommandDescription
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/

Related