コース目次 / 第2章

セッション固定 — 掴まされた合言葉

攻撃者が用意したセッションIDを被害者に使わせ、ログイン後にそれを乗っ取る手口を再現します。原因(ログイン時にIDを再生成しない)を理解し、再生成で直して再検証します。

第2章 / 全6章目安 約11分この章のゴール: セッション固定を再現し、ログイン時のセッションID再生成で直せるようになる

前章は「セッションIDを当てる/盗む」話でした。この章は、もっと巧妙な——攻撃者が用意したセッションIDを、被害者に使わせる手口です。セッション固定(session fixation)と呼びます。

法と倫理ゲート

手を動かす前に

このコースは攻撃の技術に触れます。実習してよい相手は、次の3つだけです。

  • (a) 自分で作った練習用のやられ環境(このサイトが配る自作ターゲット)
  • (b) あなた自身の許可された環境(自分の Thousand SKY・secret-notes-ctf など)
  • (c) CTF 競技の中(規約の範囲で)

実在する第三者のシステムを許可なく触ることは、不正アクセス禁止法に触れます。 このゲートは、その線引きを毎回の入口で確かめるためのものです。詳しくは「セキュリティの法と倫理」へ。

手口の筋書き

1. 攻撃者が、あるサービスのセッションID X を1つ手に入れる(未ログインでいい)。
2. 攻撃者が、被害者のブラウザに sid=X を仕込む(細工したリンク、サブドメインの Cookie、XSS など)。
3. 被害者が、そのまま sid=X を持った状態でログインする。
4. もしサーバがログイン時にセッションIDを再生成しなければ、X が「被害者としてログイン済み」になる。
5. 攻撃者は、最初から知っている X を使って——被害者としてアクセスできる。

鍵は 4 です。ログイン前と後で同じセッションIDを使い回すと、「ログイン前に攻撃者が知っていたID」が、そのまま「ログイン後の被害者のID」になってしまう。

標的 — 再生成を切り替えられるログイン

下を fixation-demo.js として保存します。REGEN=1 で修正版(再生成する)、無しで脆弱版(既存の sid を使い回す)です。

// fixation-demo.js — セッション固定の再現。127.0.0.1:3810。REGEN=1 で修正版。
const http = require('node:http');
const crypto = require('node:crypto');

const sessions = new Map();
const REGEN = process.env.REGEN === '1';
const fresh = () => crypto.randomBytes(16).toString('hex');
const cookie = (s) => 'sid=' + s + '; HttpOnly; Path=/';

http.createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:3810');
  const sid = /(?:^|;\s*)sid=([^;]+)/.exec(req.headers.cookie || '')?.[1];
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');

  if (url.pathname === '/login') {
    let s = sid;
    // 【穴】既存の sid があれば、それをそのまま認証済みにする(=再生成しない)。
    if (!s) { s = fresh(); res.setHeader('Set-Cookie', cookie(s)); }
    // 修正版(REGEN=1): 既存の sid を無視して、必ず新しい sid を発行する。
    if (REGEN) { s = fresh(); res.setHeader('Set-Cookie', cookie(s)); }
    sessions.set(s, { auth: true, user: 'haruto' });
    res.end('ログインしました。sid=' + s);
    return;
  }
  if (url.pathname === '/me') {
    const x = sessions.get(sid);
    res.end(x && x.auth ? 'あなたは ' + x.user + ' さん(ログイン済み)' : '未ログイン');
    return;
  }
  res.end('ok');
}).listen(3810, '127.0.0.1', () => console.log('固定デモ: http://127.0.0.1:3810 (Ctrl+C で停止)'));

攻撃 — 固定したIDで、乗っ取る

脆弱版で起動します。

node fixation-demo.js

攻撃者は、自分の知っている値 attacker-known-123 を、被害者のログインに紛れ込ませます(手順3に相当)。被害者がその Cookie を持ったままログインした、という状況を curl で作ります。

# 被害者が sid=attacker-known-123 を持ったままログイン
curl -i -H "Cookie: sid=attacker-known-123" http://127.0.0.1:3810/login

レスポンスに新しい Set-Cookie は出ません。サーバは attacker-known-123 をそのまま認証済みにしました。ということは——攻撃者は、最初から知っているこの値で、被害者として入れます。

# 攻撃者が、最初から知っている値でアクセス
curl -H "Cookie: sid=attacker-known-123" http://127.0.0.1:3810/me
# => あなたは haruto さん(ログイン済み)

攻撃者が事前に知っていたセッションIDで、被害者のログイン状態を乗っ取れました。 パスワードは一切要らない。被害者に「攻撃者の知るIDで」ログインさせただけです。

修正 — ログイン時に、必ず再生成

直しは一言——ログインが成功した瞬間に、セッションIDを捨てて、新しく発行し直す。ログイン前のIDは、ここで無効になります。REGEN=1 で再起動します。

REGEN=1 node fixation-demo.js

同じ攻撃を、もう一度。

curl -i -H "Cookie: sid=attacker-known-123" http://127.0.0.1:3810/login   # 新しい sid を Set-Cookie で発行
curl -H "Cookie: sid=attacker-known-123" http://127.0.0.1:3810/me         # => 未ログイン

今度は、ログインのレスポンスでまったく新しい sid が発行され、攻撃者の知る attacker-known-123 は認証済みになりません。だから /me は「未ログイン」。ログイン前後でIDが変わるので、「ログイン前に知っていたID」は無価値になる——これがセッション固定の唯一にして確実な対策です。実績あるセッションライブラリは、ログイン時のID再生成(regenerate)を備えています。

持ち帰る一言

ログインしたら、合言葉を替える。 セッション固定は、ログイン前後でセッションIDを使い回すことで成立します。ログイン成功時に必ずIDを再生成すれば、攻撃者が事前に仕込んだIDは無価値になる。次は、セッションが正しくても起きる攻撃——CSRF です。

こうなっていればOK

卒業まであと3章です。

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