C2 Wire Protocol
June 27, 2026 ยท View on GitHub
The beacon talks to the C2 over plain TCP using a tiny
Gopher-style line protocol. Every request is a single ASCII line
the client sends; every response is a single line followed by
\r\n.
Three request shapes are recognised:
GET command
<uri>/<client_id>\r\n
<uri> is the c2.uri value from config.json (default
/pleasesubscribe/v1/users/). <client_id> is the
c2.client_id value (default linux).
The C2 responds with:
<base64(iv || AES-256-CFB(command))>\r\n
If no command is queued, the response decrypts to a single NUL byte. The beacon treats this as "nothing to do, sleep and poll again".
The C2 pops the queued command on first read, so a beacon that crashes after reading the GET but before POSTing the result will lose the command. Operators should treat this as best-effort.
POST result
/report/<base64(iv || AES-256-CFB(json))>\r\n
The encrypted JSON has the keys: client, pid, hostname,
ips, user, command, output, plus optional
discovered_ips, result_portscan, result_pwd.
The C2 responds with OK\r\n on success, ERROR\r\n on any
decode/parse failure. On success the row is appended to
sessions/logs/<client>.log in CSV form.
GET BOF
/bof/<name>.x64.o\r\n
The C2 looks the file up in sessions/uploads/<basename(name)>.
On hit, the response is the file's content base64-encoded and
terminated with \r\n. On miss, the response is the literal
BOF_NOT_FOUND\r\n.
The beacon then loads the file with RunELF() and calls the
BOF's go(args, alen) entry point.
Encryption
AES-256-CFB, 16-byte IV prepended to the ciphertext. Both the
beacon and the C2 agree on the key from config.json. The
Python side uses cryptography.hazmat.primitives.ciphers with
modes.CFB(iv); the C side uses the hand-rolled CFB in
include/aes_cfb.c (extracted from the original beacon so it
is unit-testable).
CFB is unauthenticated. An attacker on the path can flip bits
in the encrypted command and the beacon will execute whatever
comes out. This is a known limitation; if you need integrity,
front the C2 with TLS (the network.verify_tls flag enables
CA verification on the beacon) and treat CFB as defense in
depth.
Path traversal
/bof/../../etc/passwd is sanitised: the C2 applies
os.path.basename() before joining with upload_dir, so the
request resolves to /sessions/uploads/passwd (which does not
exist) and returns BOF_NOT_FOUND. The test
test_path_traversal_in_bof_name in tests/test_c2_server.py
pins this behaviour.