この記事で書くこと
- JAuth(Web Exploitation)のwriteup
- JWTって何なのか
JWTってよく聞く割りに理解が浅かったので、自分用の整理として残します。
本記事で扱う内容はセキュリティ業務や娯楽目的のハッキング(CTF)において役立つ知識としてまとめるもので、許可を得ずに第三者のサイトに利用(悪用)することは不正アクセス行為の禁止等に関する法律に抵触する可能性があるため、絶対にお控えください。
JAuth - 問題概要
picoCTFのWeb Exploitationカテゴリ、難易度Mediumの問題です。
インスタンスを起動すると、シンプルなログイン画面にアクセスできます。
テストユーザーの情報が与えられるのでログインしてみます。
username:test
Password:Test123!
/private
ページに遷移し、ログインできました。しかしflag
を入手するにはadmin権限でのログインが必要なようです。
Burp Suiteでログイン時の動作を見てみましたが特に有益な情報は得られず、そういえば見てなかったなと調査ツールでCookieを見ると、なんか入っていました。
JWTとは
Cookieの中に入ってたこの文字列は JWT(Json Web Token) と呼ばれ、ユーザーの認証情報などを含むデータを安全にやり取りするためのトークン形式です。平たく言うと、署名付きのJsonデータをBase64でエンコードしたものです。
JWTはサーバーが発行し、Cookie等を利用してクライアント側に保存されます。Base64エンコードがされているだけで誰でも読み取ることができるため、パスワードや機密情報は持たせてはいけません。
クライアント側で認証情報を持たせると改ざんが容易なのでは?と思いますが、JWTではHMACによる署名を使い改ざんを防いでいます。(後ほど解説)
JWTを読み解く
Cookieの中の文字列をよく見るとeyJ*******.eyJ*******.*******
という形式となっており、.
で区切られた3セクションで構成されています。それぞれヘッダー、ペイロード、署名です。
CyberChefでBase64デコードしてみます。
Jsonが復元できました。
ただし署名部分はハッシュ形式なので謎の文字列になっていますね。
// デコードしたJson文字列
{
// ヘッダー
"typ":"JWT",
"alg":"HS256"
}
{
// ペイロード
"auth":1752318647015,
"agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36",
"role":"user",
"iat":1752318647
}
// 署名
y©©â8XÁRµöÔ°ÄmÑ=f2ÞªVR7v
ヘッダー
ヘッダーにalg
とあるのは署名に用いられるアルゴリズムです。(HS256 = HMAC-SHA256)
HMACでは、サーバーだけが知っている秘密鍵を使って、トークンの中身(ヘッダーとペイロード)から署名を作ります。そのためクライアント側で中身を書き換えても正しい署名は作れないので、サーバー側で無効と判断されます。
// サーバーがクライアントにJWTを渡す
{ "typ":"JWT", "alg":"HS256" }.{ "user":"john" }.<署名A>
// クライアント側でペイロードを改変してサーバーに送信
{ "typ":"JWT", "alg":"HS256" }{ "user":"john_the_ripper" }.<署名A>
// => サーバーが自身の秘密鍵を使って再計算したHMACが署名Aと異なるため、改ざんを検知できる
これがクライアント側で認証情報を持つことができる理由です。
「ヘッダーとペイロードはBase64エンコードなだけなので誰でも読める、しかし署名があるので改ざんはできない」のがJWT。
ペイロード
ペイロードはクレームと呼ばれるKey:Valueセットで構成されます。開発者が任意に指定できるものと既に規定されている予約クレームが存在するようです。(例えばiat
はJWTの発行日時と決まっており、UNIX時間の値が入ります)
ちなみに予約クレームに異なる意味の値をセットするとどうなるのか調べてみると以下のような問題が。
- JWTライブラリとの互換性喪失
例えばexp
(JWTの有効期限)に異なる意味の値を入れることで、トークンが期限切れと誤認識されてしまったり、誤ったユーザーを認証してしまう可能性がある - 可読性・保守性の低下
他の開発者が見た時に混乱を招き、予期せぬバグの温床となる
予約クレームは守りましょう。
今回の場合だとrole
というクレームが気になります。このrole
をadmin
にして送信できればadmin権限を持った状態で/private
ページにアクセスでき、flagが手に入りそうです。
解法
role
をadmin
に変更したものをBase64でエンコードし、Cookieにセットしてアクセスしてみますが、前述の通り署名があるため上手くいきません。
しかし調べてみると、alg=none攻撃なるものがあることが分かりました。
alg=none攻撃とは
勘の良い方はすぐピンと来るかもしれませんが、ヘッダーのalg
の値のnone
に変更することで、署名による検証がされなくなるという仕様を悪用した手法です。繰り返しますが、JWTの仕様です。
jwt.ioという便利なサイトがあるので、改変したヘッダー+ペイロードからJWTを作ってもらいます。
念のためもう一度test
アカウントでログインし、最新のタイムスタンプを持ったJWTを取得します。
CyberChefでデコードしてJsonを復元したら、jwt.ioでヘッダーのalg
をnone
に、ペイロードのrole
をadmin
に書き換え、新たなJWTを生成。
JWTに署名セクションがなく.で終わっていることが確認できます。
あとは調査ツールからCookieの値に新JWTをセットし、/private
へアクセスするとflagが表示されました。
なぜこの攻撃が成功するのか
そもそもalg
をnone
にするだけで署名検証しなくなるってザルすぎない?と思ったので調べてみたら、4年前に語られていた話題でした。(以下の記事が大変参考になります)
JWTを作成する際に利用した署名アルゴリズムをサーバー側で記憶しておき、検証の際にはヘッダーの署名アルゴリズムが作成時のアルゴリズムと同一であることを検証すればこのような問題は起きません。
ではなぜ発生するのかと言われれば、RFCにalg=none
の仕様が規定されている以上、結局は実装次第ということらしいです。
RFCというのは「何ができるか」を定める技術的仕様書に過ぎないため、危険な挙動でも「仕様上あり得る」ことは記述されます。
ただしRFCを見ると、alg=none
を使うことはUnsecuredであることが明記されていました。
実際のユースケースにおいて
現在主流のJWTライブラリにおいてはnone
指定はデフォルトで無効化されていることがほとんどらしいです。当然かもしれませんが。
ただし一部の独自の実装によっては発生する可能性は0ではないということで、alg=none
を見かけたら危険という意識は持っておこうと思います。
まとめ
picoCTFの問題を軸にJWTについて解説してきましたが、色々と疑問に思ったことを調べていったらかなり勉強になりました。
便利なものは用法を誤ることで脆弱になる、という良い例だったのではないでしょうか。
以上です。
参考