kdnakt blog

hello there.

AmateursCTF 2023 Writeups

参加レポートはこちら。

kdnakt.hatenablog.com

[web/funny factorials]

以下のようなソースコードサイトが与えられる。

def filter_path(path):
    # print(path)
    path = path.replace("../", "")
    try:
        return filter_path(path)
    except RecursionError:
        # remove root / from path if it exists
        if path[0] == "/":
            path = path[1:]
        print(path)
        return path

@app.route('/')
def index():
    safe_theme = filter_path(request.args.get("theme", "themes/theme1.css"))
    f = open(safe_theme, "r")
    theme = f.read()
    f.close()
    return render_template('index.html', css=theme)

別で与えられたDockerfileで、サーバのルートディレクトリにflag.txtがあることがわかっているので、リクエストパラメータのthemeを書き換えてやれば良さそう。

filter_path()関数はひたすら自分を呼び出し、最後にRecursionErrorの処理で先頭のスラッシュを削除するので、スラッシュを2個つけて?theme=//flag.txtというリクエストを送ると、無事レスポンスのHTMLにフラグが埋め込まれていた。

[crypto/Compact XORs]

与えられるのはflegというファイルのみ。中身は以下のような感じ。

610c6115651072014317463d....

本CTFのフラグ形式はamateursCTF{...}という形。先頭が「610c6115...」となっており、0x61=97で小文字のaなので、1文字ずつ正解が混じっていそうと判断。

Pythonインタプリタを起動して、先頭の0x61と0x0cのXORを取ると以下のようになった。

>>> 0x61 ^ 0x0c
109

109は小文字のmなので、ama...と解読できそう。スクリプトを書くのが面倒だったので、ChatGPTに解かせることに。

自分:
Pythonで610c61156510のような16進数の文字列が与えられたとします。
この文字列を、61, 0c, 61, 15, 65, 10のように
2つずつの数字に分解し、
先頭から奇数番目の数字はそのままASCII文字列に変換、
偶数番目の数字は直前の数字と排他的論理和をとって
ASCII文字列にするようなコードを出力してください

ChatGPT:
このようなタスクはPythonの組み込み関数と、
特に`hex`と`ord`を使用することで解決できます。
以下のコードは、指定された仕様に従ってこのタスクを実行します。

```python
hex_str = "610c61156510"

# 2文字ごとに分解
chunks = [hex_str[i:i+2] for i in range(0, len(hex_str), 2)]

output = ""

for i in range(len(chunks)):
    # 奇数番目の数字はそのままASCII文字列に変換
    if i % 2 == 0:
        output += chr(int(chunks[i], 16))
    # 偶数番目の数字は直前の数字と排他的論理和をとってASCII文字列にする
    else:
        xor_res = int(chunks[i-1], 16) ^ int(chunks[i], 16)
        output += chr(xor_res)

print(output)
```

あとは、hex_str変数を実際の文字列に置き換えてコードを実行するとフラグが出力された。ChatGPT賢いなあ。

[misc/Censorship]

こんな感じでソースが与えられるので、サーバに接続してコードを入力してフラグを取得しに行く。

#!/usr/local/bin/python
from flag import flag

for _ in [flag]:
    while True:
        try:
            code = ascii(input("Give code: "))
            if "flag" in code or "e" in code or "t" in code or "\\" in code:
                raise ValueError("invalid input")
            exec(eval(code))
        except Exception as err:
            print(err)

ポイントになりそうなif文では、入力値にflagまたはeまたはtまたはバックスラッシュが入っているかをチェックしている。試しにサーバに接続してaと入力しエンターキーを押すと、以下のようになった。

Give code: a
name 'a' is not defined

exec(eval(code))で、入力された文字列をそのままPythonのコードとして実行してくれているらしい。flagは_という変数に格納されているので、ここにアクセスして頑張れば良さそう。直接print(_)というコードを実行しようとしたが、これはtが含まれているためinvalid inputの判定に。

なんとかして例外を発生させて、print(err)でそれを検知できればフラグを確認できそう。しばらく考えたのち、ゼロ除算のエラーを利用することを思いついた。フラグ文字列を1文字ずつ順番に取得し、想定されるアスキーコード値で引き算して、その結果が0になっているか確認すれば良い。フラグ形式はamateursCTF{...}なので、1文字目はaだろうとあたりをつけて、以下のように試してみると、ゼロ除算のエラーが表示され、1文字目がaであることが確認できた。

Give code: 1/(ord(_[0])-ord('a'))
division by zero

あとはこれをひたすら繰り返して、2番目の文字、3番目の文字...と進めていくとフラグが取得できる。小文字のeなどは、if文のチェックで弾かれてしまうので、直接アスキーコード値を入力して回避した。

きっともっと賢いやり方があるんだろうけど、頑張って1文字ずつ推測していく方法しか思いつかなかった...orz

[misc/Censorship Lite]

前作Censorshipの改良版。今回はif文のチェックが以下のように変更になっている。

if any([i in code for i in "\lite0123456789"]):
    raise ValueError("invalid input")

入力したコードを実行させて、例外を発生させて内容を確認できるところは前作と同じ。数値が直接使えなくなったので、フラグの何文字目かを指定するord(_[0])の部分の改善が必要。アスキーコード値を利用して、ord(_[ord('a')-ord('a')])のようにすると、目的の文字にアクセスできる。あとは力技で。

ちなみに、これの改良版のCensorship Lite++という問題もあった。コードは以下のように変更になっている。文字列にアクセスするための[]といった記号は使えるものの、ord()などの関数呼び出しが封じられたためフラグに辿り着けず。無念...。

#!/usr/local/bin/python
from flag import flag

for _ in [flag]:
    while True:
        try:
            code = ascii(input("Give code: "))
            if any([i in code for i in "lite0123456789 :< :( ): :{ }: :*\ ,-."]):
                print("invalid input")
                continue
            exec(eval(code))
        except Exception as err:
            print("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", err)

他の人のwriteupを読むと勉強になる。

github.com

[web/waiting-an-eternity]

ソースコードは無し。サイトを開くと、「just wait an eternity」とテキストが表示される。

開発者ツールを開いて、HTTPレスポンスを確認すると、以下のヘッダが設定されていた。

refresh: 1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000; url=/secret-site?secretcode=5770011ff65738feaf0c1d009caffb035651bb8a7e16799a433a301c0756003a

調べてみると、メタタグと同じで、指定時間待って指定されたURLにリダイレクトしてくれるヘッダっぽい。

developer.mozilla.org

そんなに待てないので、urlを直接入力して開くと「welcome. please wait another eternity.」というテキストが表示された。適当にリロードしてみると、表示が変わって、「you have not waited an eternity. you have only waited 15.026334047317505 seconds」と表示される。HTTPリクエストヘッダをみると「cookie: time=1690307411.834997」という意味ありげなクッキーが設定されていた。こいつをいじればフラグが取得できそう。

cookie: time=9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999

を設定してリクエストを送ったところ、「you have not waited an eternity. you have only waited -inf seconds」と表示された。マイナス無限大になってしまっているので、クッキーの値を「-9999...」に変更してリクエストを送ると、無事フラグが出力された。