JSON Web Tokens(JWT)の解析とWebサイトのセキュリティ設計


はじめに

株式会社日立ソリューションズ セキュリティプロフェッショナルセンタの青山桃子です。
弊社、(株)日立ソリューションズでは、グループ会社の(株)日立ソリューションズ・クリエイトと合同で、社内および一部の日立グループ会社向けに、毎年セキュリティコンテストを開催しています。
セキュリティコンテストとは、与えられたデータやソフトウェア、情報システムなどに関するセキュリティの課題を解決することで得られる得点を競うものです。
このような競技は、CTF(Capture The Flag)と呼ばれています。

競技形式ではありますが、弊社ではセキュリティの技術習得、意識啓発など、人材育成の目的で開催しています。
そして、今年も10~11月に開催されたコンテストの中で、セキュリティに関する様々な解析技術を取り扱いました。
本記事では、その中の一つをピックアップしてご紹介したいと思います。

JSON Web Tokens(JWT)とは

JWTとは、RFC 7519に書かれているオープンなWebトークンの規格です。
2者間でパラメータを送受信することが可能で、トークン認証などに使用できます。
署名することが可能で、改ざん防止や作成者の確認が可能です。

JWTを題材としたセキュリティ解析問題

この問題では、Webサイトにアクセスし、そのWebサイトのセキュリティの問題点を検出し、それを利用して攻撃を成功させるとフラグ(答えとなる文字列)を入手することができます。
問題となったのは以下のようなWebサイトです。

ユーザ名とパスワードを登録すると、その組み合わせでログインできるという、典型的な認証を行うサイトのようです。
ただし、通常のログインの他に「JWT Login」が可能となっており、ここにJWTの認証トークンを入力することでもログインできるようです。
JWTの認証トークンは、ユーザ名とパスワードでログインした後の画面に表示されます。

JWTでログインした場合の画面は以下のようになります。

さて、このWebサイトにどのようなセキュリティの問題があるのでしょうか。

JWTの構造

まずは、JWTの構造を確認します。
今回使用されたJWTは以下のようなものです。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IkFkbWluIiwiaWF0IjoxNjA3MzE1OTEzLCJleHAiOjE2MDczMTU5NzN9.5ISvTba5Co0KsbGBL5yh_8rRp5yH-JiX9kgSSIz_phI

JWTは「.」で区切られた以下の3つの部分に分かれていて、それぞれBase64エンコードされています。

  • ヘッダ
    • 署名アルゴリズムなどをJSONで記述したもの
    • 例:{"typ":"JWT","alg":"HS256"}
  • ペイロード
    • サブジェクト、発行日時などの情報をJSONで記述したもの
    • 例:{"username":"Admin","iat":1607315913,"exp":1607315973}
  • 署名

ヘッダとペイロードの中身は、暗号化されている訳ではないので、Base64デコードすれば、JSONで記述されている内容を読むことができます。
署名部分は、署名者しか知らない鍵データを使用して署名するため、同じ署名は署名者にしか作ることができません。
つまり、通常は署名の偽造はできないので、もし、ヘッダとペイロードの内容を改ざんした場合は署名の検証エラーとなります。

Webサイトの観察

通常、このような認証画面に問題がある場合、認証がバイパス可能だったり、SQLインジェクションなどで情報が窃取可能だったりする可能性が考えられます。
Webサイトの動作を観察すると、古いトークンを使って「JWT Login」をした場合、エラー画面に飛ばされることが分かりました。

エラー画面にはサーバ上で動作しているソースコードの一部が表示されていました。
デバッグ用に、エラーが発生した箇所の近くのソースコードが表示されるようになっていたのです。
Flaskを使ってpythonで書かれたWebサイトのようです。

まずは表示されているソースコードのうち、以下に注目します。

ここから、次のことが分かりました。

  • exp(有効期限)はiat(JWTの発行時刻)+60の値である
  • 「Admin」というユーザ名でログインするとフラグが得られる
  • 署名の鍵となるデータはmsgという変数に格納されている
  • PyJWTを使用してJWTを生成している

「鍵=msg変数に格納されている文字列」が分かれば、自分が作成した任意のデータに署名し、JWTを作り出すことができそうです。
このmsgという変数の中身は、エラー画面には書かれていませんでした。
しかし、ソースコードをよく見ると、msgはflashという変数に格納されています。

flashという変数は前後の関数を見ると、Webサイトに表示されるメッセージに使用されているようです。
条件文から、「JWT Login」で正しくログインした後に表示されるメッセージであると推測できます。
つまり、以下のメッセージです。(ユーザ名「test」でログインした場合の例)

msg % usernameという構文で記述しているため、
msgはJWT Login success. Welcome %s.という文字列であると推測できます。

あとは、判明した情報を元に、「Admin」というユーザ名でログインできるJWTを作成します。
PyJWTを用いて以下のようにJWTを作成します。

import jwt

data = {"username":"Admin","iat":1607315913,"exp":1607315973}
out = jwt.encode(data,'JWT Login success. Welcome %s.', algorithm='HS256')
print(out.decode())

ユーザ名「Admin」でログインすることに成功し、フラグが表示されました。

Webサイトのセキュリティ設計における注意ポイント

今回の問題から学ぶべきことは以下の2点です。

  • エラーメッセージなどの不要な情報をユーザに表示しない
  • 暗号化や署名に使用する鍵(データ)をユーザに表示しない

本問では、Flaskがデバッグモードになっているため、エラーメッセージが表示されていました。
Flaskに限らず、PHPやSQLのエラーなどがユーザに表示されるような状態になっていると、エラーの出力から内部構造を把握できる可能性があります。

また、JWTの署名の鍵(msg変数に格納されている文字列)が、ユーザ側へ表示されるメッセージとなっていました。
鍵とするデータはユーザ側から隠し、きちんと管理する必要があります。
表示していないつもりでも、前述のエラーメッセージでうっかり表示されている場合もあるので注意が必要です。
(今回は表示されていませんでしたが・・・。)

おわりに

ご紹介したのはコンテストのための問題なので、攻撃が成功してもフラグが表示されただけでしたが、実際の環境でこのようなことが起こった場合、攻撃者に管理者権限を奪われてシステムを乗っ取られることになりかねません。

ソフトウェア側にセキュリティの問題が無くても、使い方が間違っていると問題が発生する場合があります。
正しく使って安全な情報システムを作りましょう。
メリークリスマス!

※本記事は、JWTおよび記事内で使用しているソフトウェアのセキュリティ上の問題を指摘するものではございません。

参考URL

RFC7519
JWT.io