JWTを偽造する|alg:noneと弱い署名鍵
多くのサービスでログイン状態を表すために使われているJWT(JSON Web Token)。その構造をデコードし、署名検証の2つの弱点(alg:noneと弱いシークレット)を突いて「自分は管理者だ」と主張するトークンを偽造します。
📋 目次
💭 前回の振り返り|「誰であるか」を証明する仕組みを狙う
第2話のIDORから、もう一歩深く
第2話はデータへのアクセス制御でしたが…
第2話のIDORは、サーバーが「このIDのデータを見てよいか」を確認しないという、データ側の防御の甘さを突くものでした。今回は一段深く、「あなたが誰であるか」「あなたが管理者かどうか」をサーバーに証明する仕組みそのものを狙います。その証明書の役割を担うのがJWTです。
この話の前提知識
JWTの中身はBase64で符号化され、署名にはハッシュ関数が使われます。前作の該当話を前提にします。
JWTはCookieの代わり、あるいはCookieと組み合わせて、ログイン状態やユーザーの権限情報を保持するために広く使われています。「自分は研修生(trainee)である」という情報を持ったトークンを、「自分は管理者(admin)である」という情報を持つトークンに書き換えることができれば、サーバーを欺いて管理者として振る舞えてしまいます。
🔑 JWTとは何か|3つのパーツでできたトークン
ヘッダー・ペイロード・署名、それぞれの役割
JWT(JSON Web Token)は、ピリオド(.)で区切られた3つのパーツからできています。それぞれがJSONをBase64url(通常のBase64の+と/を-と_に置き換えた、URLに入れても安全な形式)で符号化したものです。
JWTは「暗号化」ではなく「署名」
ペイロードはBase64urlで符号化されているだけで、暗号化はされていません。Consoleでatob()すれば誰でも中身(ユーザー名や役職など)を読めます。守られているのは「内容の秘匿性」ではなく「内容が改ざんされていないこと」だけです。この前提を理解しておくと、これから扱う脆弱性の意味がよく分かります。
下に表示するのは、あなたのトレーニーアカウントの「本物の」トークンです。3つのパーツがピリオドで連結されているのが分かります。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidHJhaW5lZSIsInJvbGUiOiJ0cmFpbmVlIn0.o6R5qfI5ScZvdHMSCC5UWD7S3KoC_wGwkLxBJ2qt-MI
🔓 alg:noneという脆弱性
「検証方法そのもの」をトークン側から指定できてしまう罠
JWTのヘッダーには、署名にどのアルゴリズムを使ったかを示すalgというフィールドがあります。仕様上、このフィールドには「署名なし」を意味するnoneという値も許可されています。問題は、一部の実装が「トークンのalgフィールドを読んで、それに応じた検証方法を選ぶ」という作りになっていたことです。攻撃者がalgをnoneに書き換えてしまえば、サーバーに「これは署名なしのトークンだから、署名検証はしなくていい」と思い込ませることができます。
| alg値 | 意味 | 安全性 |
|---|---|---|
HS256 | 共通鍵(シークレット)でHMAC-SHA256署名 | シークレットが強ければ安全 |
RS256 | 公開鍵暗号(RSA)で署名 | 秘密鍵が漏れない限り安全 |
none | 署名なし(仕様上は存在するが本番では使うべきでない) | ⚠ 検証する側が許可した時点で無意味になる |
過去に複数のJWTライブラリで実際に報告された設計ミス
「トークン側が指定したalgを無条件に信用する」という実装上の問題は、過去に複数の主要なJWT関連ライブラリで報告され、修正されてきた経緯があります。現在広く使われているライブラリは、デフォルトで許可するalgを明示的に指定する設計に改善されています。本シリーズで扱うのは、その「修正前の挙動」を学習用に再現したものです。
🧪 実践チャレンジ:JWTラボでトークンを偽造する
2つの弱点を順番に突いてadminになる
下の「JWTラボ」では、ヘッダー・ペイロード・シークレットを自由に編集して新しいトークンを作り、それを検証システムに送ってみることができます。検証システムは今回のチャレンジ専用に作った、学習用の脆弱なシミュレーターです。
ステージ1の手順(alg:noneバイパス)
① HEADERの"alg":"HS256"を"alg":"none"に書き換える → ② PAYLOADの"role":"trainee"を"role":"admin"に書き換える → ③ 「トークンを生成」を押す → ④ 「検証する」を押す → ⑤ 表示されたチェックポイントコードをステージ1に入力。
(まだ生成されていません)
🔓 alg:noneバイパス成功!
チェックポイントコード:BYPASS-ALGNONE
🎉 HS256での正規偽造に成功!
最終フラグ:CTF{JWT_F0RG3D_4DM1N}
ステージ1をクリアすると、ステージ2が解放されます。両方クリアした時点でスコアが確定します。
📊 ステージ進捗: 0/2|挑戦回数: 0回
JWTラボでalg:noneバイパスに成功すると表示されるチェックポイントコードを入力してください。
HEADERのalgをnoneに、PAYLOADのroleをadminに書き換えてから「トークンを生成」→「検証する」の順に押してください。SECRET欄は空のままでかまいません。
⚠ alg:noneの脆弱性は修正されました。今度はHS256の署名を正しく作る必要があります。元のトークンのシークレットは、下のワードリストのいずれかです。
Consoleに以下のコードを貼り付けると、一致するシークレットを自動で見つけられます(第12話の辞書攻撃と同じ発想です)。
(async function(){
var candidates = ["password123", "admin2024", "letmein!", "qwerty123", "changeme123", "s3cr3tkey", "jwt_secret", "tokentoken", "12345678", "welcome123", "trustme1", "backdoor99"];
var signingInput = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoidHJhaW5lZSIsInJvbGUiOiJ0cmFpbmVlIn0";
var targetSig = "o6R5qfI5ScZvdHMSCC5UWD7S3KoC_wGwkLxBJ2qt-MI";
for (var i=0;i
見つかったシークレットをJWTラボのSECRET欄に入力し、HEADERのalgをHS256に戻し、PAYLOADのroleをadminにしてから「トークンを生成」→「検証する」を実行してください。表示された最終フラグを入力してください。
HEADERを{"alg":"HS256","typ":"JWT"}、PAYLOADを{"user":"trainee","role":"admin"}、SECRETを見つけた単語にして「トークンを生成」→「検証する」を押してください。
🔗 なぜJWTの実装ミスは今も起こるのか
ライブラリの「デフォルト動作」が見落とされやすい
多くのJWTライブラリは現在、検証時に許可するalgを明示的に指定する設計になっています。しかし開発者がライブラリのドキュメントを読まずに古いサンプルコードをコピーしたり、許可するalgを指定せずに「とにかく動く」設定で使ってしまったりすると、同じ問題が再現することがあります。セキュリティの機能が「デフォルトで安全(secure by default)」になっているかどうかは、ライブラリ選定の重要な観点の1つです。
もう1つの定番攻撃:アルゴリズム混乱攻撃
RS256(公開鍵)で署名されたトークンを検証する仕組みに対し、攻撃者がalgをHS256に書き換え、公開されている公開鍵をそのまま共通鍵として使って署名を作る「アルゴリズム混乱攻撃(Algorithm Confusion Attack)」という手法も知られています。今回のalg:noneと同じく「トークン側の指定をサーバーが信用してしまう」ことが根本原因です。本シリーズでは扱いませんが、興味があれば調べてみてください。
次回は、JavaScriptそのものの仕組みを狙う「プロトタイプ汚染」に挑戦します。サーバー側の検証ロジックではなく、JavaScriptのオブジェクトがデータを継承する仕組み自体に潜む脆弱性です。
📝 まとめ+FAQ+次回予告
2つの弱点、2つのチェーン
第3話では、JWTの構造を理解し、alg:noneバイパスと弱いシークレットの総当たりという、2つの異なるアプローチで管理者権限を偽造する体験をしました。「検証する側がトークンの自己申告を信用してしまう」という根本原因は、第1話のCookie・第2話のIDORとも通じる、本シリーズ全体に流れるテーマです。
・JWTはheader.payload.signatureの3パーツ、暗号化ではなく署名(中身は誰でも読める)
・alg:noneは「署名検証をスキップする」ことを意味し、サーバーが許可すると無意味な検証になる
・シークレットが弱い場合、HMAC署名も辞書攻撃で総当たりされる可能性がある
・対策は、サーバー側で許可するalgを明示的に固定し、トークン側の自己申告を信用しないこと
・採点ルールは前2話と同じ(ヒント-15pt、両ステージ解決でスコア確定)
Q. JWTを使うサービス全部が、この方法で偽造できるのですか?
いいえ。現在主流のJWTライブラリは許可するalgを明示的に固定する設計になっており、alg:noneバイパスは通用しません。また強いランダムなシークレット(十分な長さの推測不可能な文字列)を使っていれば、辞書攻撃での総当たりも成功しません。今回のチャレンジは、学習用にあえて脆弱な実装を再現したものです。
Q. JWTのペイロードに重要な情報を入れても大丈夫ですか?
パスワードのような秘密情報は入れるべきではありません。ペイロードは暗号化されておらず、Base64urlを外せば誰でも読めます。JWTが守ってくれるのは「改ざんされていないこと」であり、「内容を見られないこと」ではない点に注意してください。
Q. シークレットを長くランダムにすれば、辞書攻撃は完全に防げますか?
十分な長さ(目安として32文字以上)のランダムな文字列であれば、現実的な時間で総当たりされる可能性は極めて低くなります。今回のチャレンジで使ったシークレットは、学習用にあえて短く推測しやすいものにしています。
Q. JWTラボの「検証する」は本物のサーバーに送信されますか?
送信されません。すべてこのページの中のJavaScriptだけで動く学習用シミュレーターです。実在するサービスやサーバーには一切アクセスしていません。
プロトタイプ汚染でadmin権限を奪う
JavaScriptのオブジェクトがプロパティを継承する仕組みに潜む「プロトタイプ汚染」を使い、本来は変更できないはずの設定を書き換えます。
📚 参考情報
- RFC 7519 — JSON Web Token (JWT)
- OWASP「JSON Web Token Cheat Sheet for Java」(alg:none・アルゴリズム混乱攻撃の解説を含む)


コメント