247CTF - Custom Protocol Writeup

We are working on our own custom command and control protocol. Can you identify any hidden features in the service? We also included a packet capture of some old sessions so you can learn how it works.

我们正在开发自己的自定义命令与控制协议。您能发现该服务中有哪些隐藏功能吗?我们还提供了一些旧会话的数据包捕获文件,以便您了解其工作原理。

PCAP Investigation with Scapy

The first step was to examine the provided packet capture (custom_protocol_log.pcap) to understand the communication pattern. I used scapy to extract and decode raw data from the TCP streams.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
from scapy.all import *
from scapy.layers.inet import TCP

PCAP_FILE = "custom_protocol_log.pcap"

def print_hex(pcap_path):
packets = rdpcap(pcap_path)
for pkt in packets:
if TCP in pkt and pkt.haslayer(Raw):
raw = pkt[Raw].load
try:
# Attempt to decode or print as hex
print(raw.hex())
except Exception as e:
print(f"[-] Error: {e}")

if __name__ == "__main__":
print_hex(PCAP_FILE)

Protocol Reversal

By analyzing the hex data from the traffic between 172.17.0.1 and 172.17.0.2, a clear pattern emerged:

1
2
3
b925afc1 00 31 00 30 00 323430323435313235
b925afc1 00 32 00 30 00 323032383630353038
b925afc1 00 33 00 32 00 33383232383038323633

The protocol structure appears to be: [Session ID] [Null] [Counter] [Null] [Command] [Null] [Checksum]

  • Session ID: A 4-byte identifier (e.g., b925afc1).
  • Counter: Increments with each request.
  • Command: Hex-encoded numbers (e.g., 30 for 0, 31 for 1, 32 for 2).
  • Checksum: A CRC32 calculation of the preceding bytes, where the result is converted to a decimal string and then hex-encoded.

Checksum Verification

Using Python to verify the CRC32 logic:

1
2
3
4
5
6
7
8
9
10
11
12
import zlib

def calc_custom_crc_hex(hex_data: str) -> str:
data_bytes = bytes.fromhex(hex_data)
crc_val = zlib.crc32(data_bytes)
# Convert CRC32 integer to decimal string, then hex-encode that string
return str(crc_val).encode().hex()

base_hex = "b925afc100310030"
print(f"Payload: {base_hex}")
print(f"Custom CRC Hex: {calc_custom_crc_hex(base_hex)}")
# Output: 323430323435313235 (Matches the PCAP!)

Solution

Exploit Script

I developed a brute-force script using pwntools to iterate through potential commands (0-14) and find the one that triggers the flag response.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/usr/bin/env python3
import zlib
from pwn import *

HOST = "35f9359cc67c7983.247ctf.com"
PORT = 50385
context.log_level = "info"

def calc_custom_crc_hex(hex_data: str) -> str:
data_bytes = bytes.fromhex(hex_data)
crc_val = zlib.crc32(data_bytes)
return str(crc_val).encode().hex()

def solve():
sep = "00"
counter = "31"
# Commands 0-14 in hex format
commands = [f"3{hex(i)[2:]}" for i in range(15)]

for cmd in commands:
try:
conn = remote(HOST, PORT, timeout=5)
# Receive Session ID
session_id = conn.recvline(drop=True).decode(errors="replace")
log.info(f"Session ID: {session_id} | Testing Command: {cmd}")

# Construct payload: [SessionID][00][Counter][00][Command][00][Checksum]
base_hex = f"{session_id}{sep}{counter}{sep}{cmd}"
crc_hex = calc_custom_crc_hex(base_hex)
payload = f"{base_hex}{sep}{crc_hex}"

conn.sendline(payload.encode())

raw_response = conn.recvall(timeout=3).decode(errors="replace")
try:
# Server returns hex-encoded response
decoded_response = bytes.fromhex(raw_response).decode(errors="replace")
if "247CTF" in decoded_response:
log.success(f"Flag found: {decoded_response}")
conn.close()
return
except ValueError:
log.warning(f"Failed to decode hex response: {raw_response}")
print("-" * 40)

if __name__ == "__main__":
solve()

Execution Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[*] Starting brute-force with commands: ['30', '31', '32', '33', '34', '35', '36', '37', '38', '39', '3a', '3b', '3c', '3d', '3e']
[+] Opening connection to 35f9359cc67c7983.247ctf.com on port 50385: Done
[*] Session ID: 9b2832a1
[+] Receiving all data: Done (54B)
[*] Closed connection to 35f9359cc67c7983.247ctf.com port 50385
[*] Response: (2\x001\x00notroot
\x003738085711
----------------------------------------
[*] Connection closed.
[+] Opening connection to 35f9359cc67c7983.247ctf.com on port 50385: Done
[*] Session ID: 3907e672
[+] Receiving all data: Done (152B)
[*] Closed connection to 35f9359cc67c7983.247ctf.com port 50385
[*] Response: 9\x07r\x001\x00uid=1000(notroot) gid=1000(notroot) groups=1000(notroot)
\x003051696603
----------------------------------------
[*] Connection closed.
[+] Opening connection to 35f9359cc67c7983.247ctf.com on port 50385: Done
[*] Session ID: edd0a465
[+] Receiving all data: Done (96B)
[*] Closed connection to 35f9359cc67c7983.247ctf.com port 50385
[*] Response: Фe\x001\x00Sun Mar 1 06:57:51 UTC 2026
\x003480231763
----------------------------------------
[*] Connection closed.
[+] Opening connection to 35f9359cc67c7983.247ctf.com on port 50385: Done
[*] Session ID: 705136bc
[+] Receiving all data: Done (446B)
[*] Closed connection to 35f9359cc67c7983.247ctf.com port 50385
[*] Response: pQ6\x001\x00 total used free shared buff/cache available
Mem: 7973384 1587756 3913000 2636 2472628 6135804
Swap: 2097148 0 2097148
\x002165499562
----------------------------------------
[*] Connection closed.
[+] Opening connection to 35f9359cc67c7983.247ctf.com on port 50385: Done
[*] Session ID: d0f8fe01
[+] Receiving all data: Done (118B)
[*] Closed connection to 35f9359cc67c7983.247ctf.com port 50385
[*] Response: \x01\x001\x00247CTF{e5df2a6497c8733e8dc4679d856591af}\x001655731160
[+] Flag found: \x01\x001\x00247CTF{e5df2a6497c8733e8dc4679d856591af}\x001655731160
[*] Connection closed.

[Process exited 0]

Flag

247CTF{e5df2a6497c8733e8dc4679d856591af}