247CTF - The Impossible User

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.