st98 の日記帳 - コピー

なにか変なことが書かれていたり、よくわからない部分があったりすればコメントでご指摘ください。匿名でもコメント可能です🙏 Please feel free to ask questions in English if that's more comfortable for you👍

UTCTF 2025 writeup

3/15 - 3/17という日程で開催された。BunkyoWesternsのマヌルネコとして参加して34位。48時間のCTFとは思えない簡単さだったり、エスパー問が多かったり、ネガティブなポイントが多かった。rating weightが67.21とされていた*1けれども、そこから想像できる品質からは程遠く感じた。


競技中に解いた問題

[Cryptography 106] Autokey Cipher (294 solves)

I know people say you can do a frequency analysis on autokey ciphers over long text, but the flag is short so it'll be fine.

lpqwma{rws_ywpqaauad_rrqfcfkq_wuey_ifwo_xlkvxawjh_pkbgrzf}

by jocelyn (@jocelyn3270 on discord)

Autokey Cipherというのは知らなかったけれども、古典暗号という感じの見た目をしている。ググると確かにそうらしい。Vigenere暗号の親戚というか、作ったのがVigenereさんということで兄弟のようなものだろうか。

"Autokey Cipher solver" 等で検索するとそれっぽいソルバが見つかる。Vigenere SolverとされているけれどもAutokey Cipherにも対応している。雑に投げると rwiliuvp というキーワードで次のように復号できると言われる。大体なんとかなってそう。フラグフォーマットが utflag{…} であることや、なんかfrequency analysisみたいなものが見えることを使えばよさそう。

utileg{why_foemuency_dnelysis_than_know_eekinnind_latters}

手作業で直す。rwllmuvp がキーワードだった。

utflag{why_frequency_analysis_when_know_beginning_letters}

Cryptoで4問があるうちの3問目がこれだった。

[Web 781] OTP (146 solves)

Find your One True Pairing on this new site I made! Whoever has the closest OTP to the "flag" will get their very own date!

This problem resets every 30 minutes.

By Sasha (@kyrili on discord)

(問題サーバのURL)

与えられたURLにアクセスすると、次のような画面が表示される。適当にユーザ名とsecretを入力して送信する。もうひとつユーザを登録して "Look up a specific pairing" でそれら2つのユーザ名を入力すると、なにやら数値が出てきた。secretの差分かなにかを数値化しているらしい。secretが近ければ小さな値に、大きく異なっていれば大きな値になる。まったく同じsecretであればその値は0になる。

問題名からは flag というユーザがsecretとしてフラグを持っていそうだと推測できる。1文字ずつブルートフォースして、もっとも差分が小さい文字を採用していくスクリプトを書く。

import re
import secrets
import string
import sys
import httpx

def register(u, p):
    return client.post('/index.php', data={
        'username': u,
        'password': p
    })

def diff(u):
    r = client.post('/index.php', data={
        'username1': u,
        'username2': 'flag'
    })
    return int(re.findall(r': (\d+)', r.text)[0])

def register_and_diff(p):
    u = secrets.token_hex(8)
    register(u, p)
    return diff(u)

table = string.digits + string.ascii_lowercase + string.ascii_uppercase + '_{}'
with httpx.Client(base_url='http://(省略)/') as client:
    known = 'utflag{On3'
    while True:
        mn, mc = 10000, '?'
        for c in table:
            while True:
                try:
                    r = register_and_diff(known + c)
                except KeyboardInterrupt:
                    sys.exit(0)
                except:
                    pass
                else:
                    break
            if r < mn:
                mn, mc = r, c

        known += mc
        print(known)

        if register_and_diff(known) == 0:
            break

実行するとフラグが得られた。

$ python3 s.py
utflag{On3_
utflag{On3_s
utflag{On3_sT
utflag{On3_sT3
utflag{On3_sT3P
utflag{On3_sT3P_
utflag{On3_sT3P_4
utflag{On3_sT3P_4t
utflag{On3_sT3P_4t_
utflag{On3_sT3P_4t_4
utflag{On3_sT3P_4t_4_
utflag{On3_sT3P_4t_4_t
utflag{On3_sT3P_4t_4_t1
utflag{On3_sT3P_4t_4_t1m
utflag{On3_sT3P_4t_4_t1m3
utflag{On3_sT3P_4t_4_t1m3}
utflag{On3_sT3P_4t_4_t1m3}

Webではないだろう。以降同じauthorの問題が何度も出てくるが、いずれもとても面白いとは言い難く、むしろ理不尽だったりエスパー要素が強かったりしてつらかった。問題のレビューをちゃんとしてほしい。

[Misc 886] Down the Rabbit Hole (104 solves)

Join our Discord and find the flag. https://discord.gg/RDDNTV7F62

Note: The initial scope for this challenge is just the Discord server itself, and not any persons or individuals. Unofficial content is not in scope.

By Sasha (@kyrili on discord)

問題文の招待リンクはUTCTFのDiscordサーバに入れるもので、なにかこの問題のために特別に用意されたようなものではなかった。変なbotはいないし、というか大量にユーザもメッセージもあるサーバだから情報の収集がやりづらくて仕方がない。この問題専用のDiscordサーバを作るような思いやりを見せてほしい。

rabbit holeが多すぎるだろとブチギレている*2と、rand0mさんがどこからか次のような不思議なURLとパスワードを見つけてきてくれた。どこかのチャンネルにあったらしい。

Planning doc: https://docs.google.com/document/d/1cgFhoHKLEbbJlu1SX4gCfFI4CGEEoEisiFq1CW-TKUo/
Admin password: Bdm@9D/]J^7@9[D(

黒塗りになっている箇所がある。

が、よく見ると各行の最後の1文字だけスペースでない文字になっている*3

2ページ目の最後に白文字で WPPVY-9YgdHlRZjIWlYWnyST4lqZiILaA_tpGt3bqVU と書かれている。

最初にある https://utctf.live というリンクも、よく見るとGistへのリンクになっている。そんなこと、あるか?*4

リンク先のGistにはなにもないように見える。

が、変更履歴を見ると https://mega.nz/file/HHgR1RRL というMEGAへのリンクがある。

MEGAのリンクにアクセスすると「復号キーを入力してください」と言われる。白文字で書かれていたパスワードを入力すると、rabbit.zip をダウンロードできた。パスワードがかかっているが、admin passwordとされていた Bdm@9D/]J^7@9[D( がそれだった*5

中にはうさぎの画像と .git ディレクトリが入っていた。うさぎの画像が過去のコミットで書き換えられていることがわかるので、git reset --hard なりなんなりで。別のうさぎの画像が出てきた*6

$ git log -p
commit 3063138d285ed27944490858fcbca3b3715012c4 (HEAD -> master)
Author: Sasha Huang <huang@cs.utexas.edu>
Date:   Mon Feb 24 17:48:37 2025 -0600

    oops

diff --git a/image.jpg b/image.jpg
index 5c6f3ba..8fa736d 100644
Binary files a/image.jpg and b/image.jpg differ

commit 584451911960d0363c8fa6e5abcbc5d22a090086
Author: Sasha Huang <huang@cs.utexas.edu>
Date:   Mon Feb 24 17:48:10 2025 -0600

    initial commit

diff --git a/image.jpg b/image.jpg
new file mode 100644
index 0000000..5c6f3ba
Binary files /dev/null and b/image.jpg differ

いい加減に頭もguessing思考に切り替わっていて、画像ということはどうせSteghideでなにか仕込んでいるのだろうと考える。Stegseekrockyou.txt の組み合わせでは不発だった。では残っている不思議な文字列がないか。あった、黒塗りになっていた文字たちだ。ということで、Coq\IP1o7hr#yyW7 がパスワードだった。なんとか解けた*7

utflag{f0ll0w1ng_th3_wh1t3_r4bb1t_:3}

ストーリー性もなにも感じない、純粋なエスパー問だった。なにもかもが意味不明だ。Discordサーバで感想を見ているとこれが一番面白いと感じている人が何人か見えて、正直なところどこにそんな面白さを感じているのかが理解できない。

[OSINT 754] Three Words 1 (153 solves)

The three words I would use to describe this location are...

Flag format: utflag{word1.word2.word3}

Note: For the OSINT challenges, find the location where the photo was taken.

By Sasha (@kyrili on discord)

添付ファイル: image.png

次の画像が与えられる。とりあえず大まかな場所を掴みたいが、左下に …HBHackerman といった文字情報が見えること、また牛柄のバナーが見えることぐらいしか使えそうな情報がない。まずこのCTFの主催と後者のロゴからUT Austinであることがわかる。とはいえ、さすがはアメリカという感じでキャンパスがデカく総当たりはやってられない。

「UT Austin "hackerman"」で検索してみると、Norman HackermanというUT Austinに関連する人物が出てくる。ということは、この人物にちなんで命名された建物なのだろう。もう少し詳しく検索結果を見ていると、Norman Hackerman Buildingという建物があるとわかる。

GoogleマップでNormal Hackerman Buildingと検索すると、同じデザインの看板が見つかる。しかしながら、周りの風景がぜんぜん違う。この周辺はストリートビューがほとんどなく、衛星写真等からそれっぽい場所を探すしかない。

ちなみに、フラグフォーマットは3つの単語ということだけれども、これはwhat3wordsのことだろう。これは精度が3m四方であるわけだけれども、厳しすぎないか。rand0mさんが通してくれた。ありがとうrand0mさん…

utflag{online.animate.quietly}

OSINT問は全部で3問あったけれども、いずれもwhat3wordsで答えろという形式だった。精度が厳しすぎるし、まったく本質的ではない部分で引っかかることになるからやめてほしい。

[Forensics 669] Forgotten Footprints (175 solves)

I didn't want anyone to find the flag, so I hid it away. Unfortunately, I seem to have misplaced it.

https://drive.google.com/file/d/1L75zJ1ha1-myAM3vL_C6lpT8VJe1XQHB/view?usp=sharing

by Caleb (@eden.caleb.a on discord)

Googleドライブからイメージファイルをダウンロードし、strings コマンドを実行する。なにかhexが出てきた。

_BHRfS_M
7574666c61677b64336c337433645f6275375f6e30745f67306e335f34657665727d…

これをデコードするとフラグが得られた。

utflag{d3l3t3d_bu7_n0t_g0n3_4ever}

[Forensics 930] Finally, an un-strings-able problem (76 solves)

I inherited this really cursed disk image recently. All the files seem to be corrupted and I can't even read some of them. What the heck is going on?

https://cdn.utctf.live/disk.img

By Sasha (@kyrili on discord)

イメージファイルをダウンロードする。ext4らしい。

$ file disk.img
disk.img: Linux rev 1.0 ext4 filesystem data, UUID=981eb2d5-0400-4c7d-986e-e9c3860666d3 (extents) (64bit) (large files) (huge files)

マウントして含まれるファイルの一覧を見てみると、パーミッションがなんだかすごいことになっている。また、更新日時でソートすると1分刻みになっていることがわかる。

なるほど、パーミッションに1ビットずつフラグを埋め込んでいるのだなとエスパーする。デコードするJSのスクリプトを書く。

const s = `--wxr-x-w- 1 root root  1025 Mar 12 10:23 PtRcxoHyWhhS6z9q.txt
-rwx-w---x 1 root root  1025 Mar 12 10:24 y7Dsjz7CmkvpTA1H.txt
-r--rw--wx 1 root root  1025 Mar 12 10:25 R6qgmnljCORHERFH.txt
--wx---rw- 1 root root  1025 Mar 12 10:26 1Pe4S76zWpxA8mgI.txt
----r-xr-- 1 root root  1025 Mar 12 10:27 u70m5b8l2T3vnZHZ.txt
-rwx-wxrw- 1 root root  1025 Mar 12 10:28 x9f1QlloTkYd5sfU.txt
-rw--wx--x 1 root root  1025 Mar 12 10:29 eiUFiXPDk6oee8sL.txt
-r-xrwx--- 1 root root  1025 Mar 12 10:30 kY4FOlfPt3qGR17K.txt
--wxr----- 1 root root  1025 Mar 12 10:31 cDnYAOBE4mvBnh0C.txt
--wx--xr-x 1 root root  1025 Mar 12 10:32 Uljik5BaQPOeMjfc.txt
-rw--w--wx 1 root root  1025 Mar 12 10:33 OOVH70vCevC3FSZq.txt
-r-x---r-x 1 root root  1025 Mar 12 10:34 HsxVbSgKw3d7tvFi.txt
-rwxr-xr-- 1 root root  1025 Mar 12 10:35 fGMkb1gANtjLb5Qz.txt
-rw---xr-- 1 root root  1025 Mar 12 10:36 6ReSGoJsRnFqhg7r.txt
----rwx--x 1 root root  1025 Mar 12 10:37 uAkL5PqfK2I7K4PE.txt
----rw--wx 1 root root  1025 Mar 12 10:38 lysJisb6wMlPG9WX.txt
--wx-wxr-- 1 root root  1025 Mar 12 10:39 LuVzAMVXkpJYAxRM.txt
-rwx--xr-- 1 root root  1025 Mar 12 10:40 GP4decBqHC6UL66s.txt
-rw---x-wx 1 root root  1025 Mar 12 10:41 S5dyoQrp6a8grHOD.txt
----rw-r-x 1 root root  1025 Mar 12 10:42 kcVtPjZM85b4B3V6.txt
-rwxr--rw- 1 root root  1025 Mar 12 10:43 t7WeFKhvlS3e1Yet.txt
-r---wx-wx 1 root root  1025 Mar 12 10:44 pU2aTHrPpCjthwwi.txt
-r---wx-w- 1 root root  1025 Mar 12 10:45 Z0VC73hFMVt2AcO9.txt
---xr-xr-- 1 root root  1025 Mar 12 10:46 bG0d9OBnfwVP2NS8.txt
--wxrw--w- 1 root root  1025 Mar 12 10:47 Vp7XQiTt4ad9IDfB.txt
-rwx--xr-- 1 root root  1025 Mar 12 10:48 GeDYq3hoIx7oijhO.txt
-rw---x-wx 1 root root  1025 Mar 12 10:49 5UhgLeVLJWuEnc4W.txt
-r--rw-r-x 1 root root  1025 Mar 12 10:50 B7HIyq5CKwGavwaW.txt
-rwxr--rwx 1 root root  1025 Mar 12 10:51 3zkhN7A0Vqe0HgNC.txt
--w---xr-- 1 root root  1025 Mar 12 10:52 KEY19YZmg8L92D1H.txt
-rw-rw---x 1 root root  1025 Mar 12 10:53 HN5vsOJkU4004pEl.txt
-r-xrwxr-x 1 root root  1025 Mar 12 10:54 W5xw54rLYvyj7qRM.txt`;
const t = s.split('\n').map(x => x.split(' ')[0].slice(1).replaceAll('-', '0').replaceAll(/[^0]/g, '1'));
console.log(String.fromCharCode(...t.join('').match(/.{8}/g).map(x => parseInt(x, 2))));

実行するとフラグが得られた。

utflag{3xp3rt_f0r3ns1c_4n4lys1s_:3c}

意味がわからない。

競技終了後に解いた問題

[Web 981] Chat (39 solves)

A chat server with some 'interesting' features! I wonder how many of them are secure...

(Send /help to get started.)

By Alex (.alex. on discord)

与えられたURLにアクセスするとチャットアプリが表示される。

適当なユーザ名でログインする。general, log, mod-info の3つのチャンネルがあるらしいが、general 以外はadminでなければ表示できないと言われる。問題文の指示通り /help を実行すると、次のように利用可能なコマンドの一覧が表示される。

Help:
/help               display this message
/msg [text]         send a message on the current channel
/nick [name]        change your username

/list               list available channels
/join [channel]     switch to a different channel
/channel            show info about the current channel
/users              list users in the current channel
/user [id]          show info about the given user

/create [channel]   create a new channel
/delete [channel]   delete a channel
/set [prop] [value] configure channels or users

/announce [msg]     send a message to all channels
/kick [id]          kick a user
/ban [id]           ban a user

/login [password]   log in as a moderator or admin.  (CTF note: do not brute force.)

/users でユーザの一覧を確認すると、0000000000000 (admin)0000moderator (moderator) という大変あやしいユーザがいるとわかる。

/set で何が設定できるのか確かめたく、/set hoge と適当に引数を与えてやると、Unknown property. Available property groups: channel, user と言われた。/set の引数に userchannel を加えてやると、次のプロパティが設定可能であるとわかる。

> /set user hoge
Available user properties: .name, .style
> /set channel hoge
Available channel properties: .description, .slowmode, .hidden, .immutable, .owner, .admin-only, .mode

/user を実行すると自身の情報が確認できる。

User '2v3mnegldnloq':
- name: poyopo
- privileges: Privileges(CHANNEL_CREATE | CHANNEL_DELETE | MESSAGE_SEND | CHANNEL_MODIFY)
- created: 2025-03-17T13:19:25Z[Etc/Unknown]
- banned: N/A
- style: "& .username { color: var(--palette-3); &::before, &::after { color: var(--fg) } }"

privileges という大変気になるプロパティもあり、権限昇格したくなるのだけれども、残念ながら /set user.privileges hoge のようなことをしようとするとそんなプロパティは知らないと怒られてしまう。

チャンネルの .mode とはなんだろうと /set channel.mode hoge と入力してみたところ、次のようなエラーが表示される。デフォルトのモードである normal のほかにも log があるらしい。

Invalid channel mode. Valid modes: 'normal', 'log'

ならばと /set channel.mode log と入力してみたところ、チャンネルが hidden かつ admin-only でなければならないと怒られる。

Log channels must be hidden and admin-only.

hidden の方は /set channel.hidden true を実行すると設定でき、チャンネル一覧からは見えなくなる。ただ、admin-only の方は /set channel.admin-only true で設定できるのはよいのだけれども、そうすると一般ユーザは入れなくなってしまう。うーむ。


競技時間内には解けず。競技終了後にDiscordサーバで共有されていた解法を見てみると、チャンネルについて admin-only をtrueにした上で modelog にし、そしてまた admin-only をfalseにする、という流れを一気にやればよいということだった。log モードではそのコマンドで発行されたすべてのコマンドが、つまり /login も含めて記録されるらしい。

admin-only がtrueになった時点で一旦切断されてしまうけれども、ちゃんとその後の /set channel.admin-only false も実行してくれるので、一般ユーザでももう一度そのチャンネルに入れるようになる。なるほど。ということで、次のコマンドたちを一気に送信してみる。

/create nekochan
/join nekochan

/set channel.hidden true
/set channel.admin-only true
/set channel.mode log
/set channel.admin-only false

nekochan チャンネルに入ると、次のように 0000moderator が実行したコマンドが記録されていた。パスワードも含まれている。

/log 0000moderator general /join 0000moderator moderator
/log 0000moderator general /login unbroken-sandpit-scant-unmixable

/login unbroken-sandpit-scant-unmixable でモデレータになることができた。mod-info チャンネルに入って /channel でチャンネルの情報を確認すると、次のようにフラグが得られた。

Channel 'mod-info':
- description: Congradulations on becoming a moderator!  The flag is utflag{32c6FLiaX5in9MhkPNDeYBUY}.
- slowmode: 0.05
- hidden: false
- immutable: true
- owner: 0000000000000
- current users: 1
- admin-only: false
- mod-only: true
- mode: normal

解法を見ると、たしかにWeb問として面白かったけれども、ブラックボックスである必要はなかったのではないか。説明不足なのもちょっとつらい。

*1:rating weightは長年大きな問題を起こさずCTFを続けてきたで賞のような側面があるけれども

*2:どうしてなんで期待の戸締まり忘れるの?

*3: (そんなのないよ)ありえない

*4:それがありえるかも

*5:はじめがかんじん つーんだつーんだ

*6:なんで? なんで? ふたりいる? (うそ!)

*7:チリ積モですよ あきらめなければ