問題

サイトにアクセスすると以下の画面が表示され

Drawボタンをクリックするとおみくじの結果が表示されます。「凶」でした

「Save Result」をクリックすると「View Saved Result」のリンクが表示されます

URLが「/result/ランダムな文字列.html」で先ほどのおみくじの結果が表示されます

脆弱性
/save エンドポイントにおけるパス・トラバーサル (ディレクトリ・トラバーサル)
この脆弱性は、server.jsのapp.post("/save") ルートに存在します。
type パラメータはリクエストボディ (c.req.text()) から直接取得され、
getResultContent 関数内でファイルパスを構築するために使用されています
app.post('/save', async c => {
const type = await c.req.text()
const content = await getResultContent(type)
getResultContent 関数では、${import.meta.dirname}/${type}がファイルの読み込み先パスになっており、typeパラメータによって自由に読み込むファイルを指定できます
async function getResultContent(type) {
return await readFile(`${import.meta.dirname}/${type}`, 'utf-8')
}
これにより、攻撃者はディレクトリトラバーサルシーケンス (例: ../, ../../) を含む type を指定し、意図された app ディレクトリ外の任意のファイルにアクセスして読み取ることができます。
脆弱性のPoC (Proof-of-Concept)
- リクエスト:
/saveに対してPOSTリクエストを送信し、機密ファイル (例:server.js自身) を読み取るためのパス・トラバーサル・シーケンスをボディに含めます。
POST /save HTTP/1.1
Host: localhost:3000
Content-Type: text/plain
Content-Length: 12
type=./server.js
- レスポンス: サーバーは、
{"location":"/result/RANDOM_FILENAME.html"}のようなlocationプロパティを含む JSON オブジェクトで応答します。 - アクセス: 返された URL (例:
http://localhost:3000/result/RANDOM_FILENAME.html) にアクセスします。指定されたファイル (server.jsまたは攻撃者が指定した任意のファイル) の内容が、生成された HTML ページの<pre>タグ内に表示されます。
修正方法
このパス・トラバーサルの脆弱性を修正するには、type パラメータに対して厳格なバリデーションを実装してください。期待される安全な値のみがファイルパスの構築に使用されるように、ホワイトリスト方式を推奨します。
/save エンドポイントを修正し、提供された type が許可された値 (例: ‘daikichi’ または ‘kyo’) のいずれかであることを明示的にチェックします。type がホワイトリストにない場合は、リクエストを拒否するか、安全なデフォルト値を返します。
app.post('/save', async c => {
const type = await c.req.text()
const allowedTypes = ['daikichi', 'kyo']; // 許可されるタイプを定義
if (!allowedTypes.includes(type)) {
return c.text('Invalid type', 400); // または適切に処理
}
const content = await getResultContent(type)
const filename = randomString()
await writeFile(`${import.meta.dirname}/public/result/${filename}.html`, html`
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Result</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/water.css@2/out/light.css">
</head>
<body>
<pre>${content}</pre>
<a href="/">Back to Top</a>
</body>
</html>
`)
return c.json({
location: `/result/${filename}.html`
})
})
この修正により、daikichi または kyo のみが getResultContent に渡されることが保証され、攻撃者がパスを操作して任意のファイルを読み取ることを防ぎます。
WriteUp
Firefoxのデベロッパーツールで検証します
「Save Result」をクリックするとネットワークタブで「save」にアクセスした記録が残ります
そこを右クリックして「編集して再送信」をクリックします

サーバへ送信するヘッダとボディを編集して再送できる画面に変わります
ここの要求ボディに以下の画面のように入れます

なぜflagファイルなのかは、Dockerfileファイルに以下のように記載されています。
RUN echo "TSGLIVE{REDACTED}" > /flag
これによりフラグは「/flag」にあることがわかります。そのため、ルートのディレクトリに移動できるように相対パス「../../../」を入れています
「」ボタンをクリックするとサーバにリクエストが再送されます
新しいsaveのアクセスが記録されるのでクリックして「要求タブ」を確認します。先ほど入力した内容が送信されています

「応答タブ」を確認します
おみくじの結果のロケーションが表示されます

実際そのロケーションにアクセスしてみるとフラグが表示されました



コメント