コース目次 / 第2章
一本、設計する — 一つの穴を持つ最小ターゲット
一つの脆弱性クラスに絞った最小のやられアプリを、設計から実装まで一周します。パストラバーサルを題材に、攻撃→修正→再検証が成立する作りを確かめ、自分版の設計に進みます。
設計ノートができたら、作ります。良い学習用ターゲットの条件は、豪華さではありません——一つの穴を、はっきり見せることです。
一穴・一題材・最小。穴は一つに絞る(複数あると、どれで攻撃が通ったか分からなくなる)。題材は身近な一つ(写真ビューア、メモ、掲示板…)。コードは動く最小。旗艦の vuln-notes.js が「一穴・一エンドポイント」で作られていたのは、この原則です。豪華なアプリより、攻撃 → 修正 → 再検証がくっきり回るアプリが良い教材です。
法と倫理ゲート
手を動かす前に
このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。
- (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
- (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
- (c) CTF 競技の中(規約の範囲で)
実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。
題材にする穴 — パストラバーサル
旗艦で扱った3つとは別の穴を、あえて選びます。パストラバーサル——「../ を使って、公開してよい範囲の外のファイルにたどり着かれてしまう」穴です。写真ビューアを題材にします。
まず、次の準備をします。アプリ用のフォルダを作り、その中に:
photos/フォルダを作り、中に適当な画像haru1.jpgを1枚置く(公開してよい写真の想定)。secret.txtをフォルダ直下(photos の外)に作り、これは公開しないファイルとだけ書く(外に出てはいけないファイルの想定)。
そして下を photo-viewer.js として、同じフォルダに保存します。
// photo-viewer.js — 学習用に「わざと穴を空けた」写真ビューア。127.0.0.1 だけで動かす。
// 狙う穴: パストラバーサル(../ で、公開範囲の外のファイルにたどり着ける)。
const http = require('node:http');
const fs = require('node:fs');
const path = require('node:path');
// 公開してよい写真だけを置くフォルダ。
const PHOTOS_DIR = path.join(__dirname, 'photos');
const server = http.createServer((req, res) => {
const url = new URL(req.url, 'http://127.0.0.1:3100');
if (url.pathname === '/photo') {
const name = url.searchParams.get('name') || '';
// 【穴: パストラバーサル】受け取った name を、そのままパスに連結している。
// name に ../ が入っていると、PHOTOS_DIR の外へ抜けられる。
const file = path.join(PHOTOS_DIR, name);
fs.readFile(file, (err, data) => {
if (err) { res.writeHead(404); res.end('見つかりません'); return; }
res.writeHead(200);
res.end(data);
});
return;
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<p>はるとの写真ビューア</p><img src="/photo?name=haru1.jpg" width="200">');
});
server.listen(3100, '127.0.0.1', () => console.log('写真ビューア: http://127.0.0.1:3100 (Ctrl+C で停止)'));
起動します。
node photo-viewer.js
http://127.0.0.1:3100/ を開くと、写真が表示されます(/photo?name=haru1.jpg)。ここまでが正規の使い方です。
攻撃 — 公開範囲の外へ、抜ける
name は本来 haru1.jpg のような写真名を渡す欄です。では、../ を混ぜて、フォルダの外を指したら?
http://127.0.0.1:3100/photo?name=../secret.txt
これは公開しないファイル が表示されました。photos/ の中の写真しか出さないつもりだったのに、../ で一つ上のフォルダに抜けて、公開しないはずの secret.txt を読み出せた——これがパストラバーサルです。../ を重ねれば、さらに上のフォルダのファイルも狙えます。あなたが「作った」穴が、あなたの意図した攻撃で開きました。
修正 — 公開範囲の中だけを許す
原因は、name を検証せず path.join でつないでいること。直しは、sec-web-basics のオープンリダイレクトと同じ発想——「危ない ../ を消す」のではなく、連結後の実際のパスが、公開フォルダの中に収まっているかを確かめます。
const file = path.join(PHOTOS_DIR, name);
// 直したあと: 連結後の実パスが、PHOTOS_DIR の中に収まっているか確かめる。
const resolved = path.resolve(file);
if (!resolved.startsWith(PHOTOS_DIR + path.sep)) {
res.writeHead(403); res.end('その場所は見られません'); return;
}
fs.readFile(resolved, (err, data) => { /* 以下は同じ */ });
path.resolve は ../ を計算し切って、最終的な実パスにします。それが PHOTOS_DIR の中で始まっているかを見れば、外に抜ける経路はすべて弾けます。「.. という文字を探して消す」(ブラックリスト)より、「収まっているものだけ許す」(許可リスト)が確実——この原則は、どの穴でも同じでした。
再検証 — もう一度、../ を
サーバを再起動して(Ctrl+C → node photo-viewer.js)、さっきと同じ攻撃を繰り返します。
http://127.0.0.1:3100/photo?name=../secret.txt
今度は 403(その場所は見られません)。secret.txt は出てきません。念のため、正規の name=haru1.jpg はちゃんと表示されることも確かめます——公開すべき写真まで締め出していないか、の確認です。攻撃 → 修正 → 再検証が、あなたの自作ターゲットの上で一周しました。
自分版を、設計する
型は掴めました。ここからが本番です。設計ノートで狙うと決めた穴で、あなた版のターゲットを一本作ってください。
題材も穴も、あなたの選択です。保存型 XSS(投稿がそのまま他の閲覧者の画面で動く)、弱い認証(推測できるトークン)、コマンドの組み立て——なんでも構いません。大事なのは一穴・一題材・最小で、攻撃 → 修正 → 再検証がくっきり回ること。写経ではなく、あなたの言葉とコードで。
持ち帰る一言
一つの穴を、はっきり見せる。 一穴・一題材・最小で、攻撃 → 修正 → 再検証が回るターゲットを、自分の手で作る。作ると、他人のコードの穴も見えるようになる。次の章は、作ったこのやられを絶対に外に出さない——作り手の責任を、テンプレの必須項目にします。
こうなっていればOK
卒業まであと2章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。