コース目次 / 第2章
セッション固定 — 掴まされた合言葉
攻撃者が用意したセッションIDを被害者に使わせ、ログイン後にそれを乗っ取る手口を再現します。原因(ログイン時に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章です。
この章はまだ完了していません。
保存できませんでした(プライベートブラウズ中かもしれません)。この端末に進捗は残りませんが、先へは進めます。