コース目次 / 第2章

一本、設計する — 一つの穴を持つ最小ターゲット

一つの脆弱性クラスに絞った最小のやられアプリを、設計から実装まで一周します。パストラバーサルを題材に、攻撃→修正→再検証が成立する作りを確かめ、自分版の設計に進みます。

第2章 / 全5章目安 約13分この章のゴール: 一つの穴に絞った学習用ターゲットを、攻撃→修正→再検証込みで作れるようになる

設計ノートができたら、作ります。良い学習用ターゲットの条件は、豪華さではありません——一つの穴を、はっきり見せることです。

一穴・一題材・最小。穴は一つに絞る(複数あると、どれで攻撃が通ったか分からなくなる)。題材は身近な一つ(写真ビューア、メモ、掲示板…)。コードは動く最小。旗艦の 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章です。

この章はまだ完了していません。