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

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

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