🦙

Writeup for Alpaca Rangers 2

に公開

問題

Daily AlpacaHack 2026/05/02 Alpaca Rangers 2という問題です.

試したこと

URLにアクセスしてみました.

リンクを押すと,アルパカレンジャーの画像が表示されます.次に,配布されたファイルを確認してみます.

app.py
app.py
from flask import Flask, request, make_response

app = Flask(__name__)

INDEX = """
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>Alpaca Rangers</title>
</head>
<body>
    <h1>Alpaca Rangers!</h1>
    <p>Here's our members:</p>
    <ul class="sample-list">
        <li><a href="/member?img=red.png">Red</a></li>
        <li><a href="/member?img=blue.png">Blue</a></li>
        <li><a href="/member?img=yellow.png">Yellow</a></li>
        <li><a href="/member?img=green.png">Green</a></li>
        <li><a href="/member?img=pink.png">Pink</a></li>
    </ul>
</body>
</html>

""".strip()

@app.get("/")
def index():
    return INDEX

notfound = open('./images/notfound.png', "rb").read()

@app.get("/member")
def member():
    path = request.args.get("img", "")
    if len(path) == 0:
        img = notfound
    else:
        path = path.replace("../", "") # Prevents directory traversal
        path = "./images/" + path
        try:
            img = open(path, "rb").read()
        except:
            img = notfound

    response = make_response(img)
    response.headers.set('Content-Type', 'image/png')

    return response

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=3000)
INDEX = """
<!DOCTYPE html>
... (HTMLの中身) ...
"""
@app.get("/")
def index():
    return INDEX

トップページにアクセスしたときの処理です.INDEX 変数に格納されたHTMLをそのまま返しています.HTMLの中には <a href="/member?img=red.png">Red</a> のように,/member というURLに img パラメータを付けてアクセスさせるリンクが並んでいます.

notfound = open('./images/notfound.png', "rb").read()

画像が見つからなかったときに表示する Not Found画像 を,あらかじめ読み込んで変数 notfound に保存しています.

@app.get("/member")
def member():
    path = request.args.get("img", "")

/member にアクセスしたときの処理です.URLの ?img=... の部分から値(...)を取り出し,変数 path に入れます.パラメータがなければ空文字 "" になります.

if len(path) == 0:
    img = notfound

もし img パラメータが空っぽだったら,事前に読み込んでおいた notfound 画像をセットします.

else:
    path = path.replace("../", "") # Prevents directory traversal

文字列の中に ../ があったら,それを ""(空文字)に置き換えて消去しています.しかし,この書き方には致命的な欠陥があります.後述します.

    path = "./images/" + path

指定したファイル名の前に ./images/ をくっつけます.red.png を入力したら,./images/red.png になります.

    try:
        img = open(path, "rb").read()
    except:
        img = notfound

出来上がったパスのファイルを読み込もうと試みます.もしファイルが存在しなかったりエラーが起きた場合は,except に飛び,notfound 画像をセットします.

    response = make_response(img)
    response.headers.set('Content-Type', 'image/png')
    return response

読み込んだ画像データをブラウザに返すためのレスポンスを作ります.その際,これはPNG画像ですよ!とヘッダーに明記して,ブラウザに送り返します.

Pythonの replace() は,見つけた文字列を一度だけ置き換えます.つまり,入れ子にされると突破されてしまいます.もし,攻撃者が img パラメータに以下のように入力したらどうなるでしょうか?

入力 -> ....//
replace("../", "") が発動します.....// の真ん中にある ../ だけが消去されます.外側に残っていた文字がくっつきます.その結果,../ が復活します!これを応用して,?img=....//....//....//flag.txt のように入力すると,replace を通過した後に ./images/../../../flag.txt となり,サーバー内の秘密のFLAGを読み込めてしまう仕組みです.FLAGは,画面には表示されず,開発者ツールなどでHTTPのレスポンスを確認するとFLAGがあります.

FLAG

Alpaca{Alp4ca_rang3rs_5aves_the_w0rld!}

Discussion