こんにちは。 マネーフォワードの新卒Railsエンジニア、きなこ と申します。 マネーフォワードX という組織で、日々プロダクトの開発に勤しんでおります😊
突然ですが皆さんは JWT という技術をご存知でしょうか? 私は趣味でCTFというセキュリティコンテストに出場するのですが、最近ホットだと感じるのがJWTに関連する攻撃です。
今年の1月に初めてJWTを題材にした問題に遭遇し、その後JWTの出題頻度が強まっていると感じ、社内に向けてJWTにまつわる攻撃を通して学ぶための記事を書いたところ、たくさんの反応をいただきました。
今回の記事はその内容を社外向けにアレンジし、ハンズオンを通して実際にJWTを改竄し、受け取るAPIを攻撃することでJWT自体を学べるようにしたものです。
本記事はJWTに興味があるWeb開発者を想定していますが、そうでない方も楽しんでいただけるようにハンズオンを用意したので是非ご覧ください🙇♂️
JWTの定義
JWTはRFC7519で定義されています。 しかし今回はjwt.ioを提供しているAuth0によるJWTの定義が分かりやすいため、こちらを用いて説明します。
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
訳)
JSON Web Token(JWT)はRFC7519でオープンスタンダード化されている、JSONオブジェクトを用いて二者間での情報のやりとりを安全でコンパクトかつ自己完結型で行うための手段である。 この情報は電子的な署名によって認証され信頼される。JWTはHMACアルゴリズムを用いたsecretあるいはRSAやECDSAを用いた公開鍵、秘密鍵で署名される。
堅い文章ですが、ポイントは以下の3つです。
- JWTは情報を安全かつコンパクトにやりとりするための技術である
- JSON型のオブジェクトで扱う
- 署名のために特定のアルゴリズムを使用したsecretまたは公開鍵と秘密鍵を利用する
また、上記のAuth0の定義には書いていませんが、JWTはURL-safeです。
JWTの1番の特徴は、何と言っても署名検証により改竄が検知できる
ことです。
これは定義に書かれていた電子的な署名によって認証され信頼される
の部分関連しており、後述する仕組みでJWTは安全に情報をやりとりできるようになっています。
JWTの仕組み
このセクションではJWTの仕組みを説明していきます。
JWTは3つのパートに分かれており、それぞれbase64でエンコードされています。そして各々がピリオドで結合されており、実物は以下のようになっています。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTc4NDQ0NDEsImlhdCI6IjIwMjAtMDgtMTlUMjE6NDA6NDEuNTc2MDQzKzA5OjAwIiwidXNlciI6Imd1ZXN0In0.2UsQ3jsuwk17rJQGdBsUxArOEu_bW7HcHzqEb9OVsXo
こちらのJWTをjwt.ioでデコードすると以下のようになります。
3色でそれぞれ塗り分けられた部分について解説します。
赤の部分(Header)
ヘッダー(Header)の部分です。 ヘッダーには電子署名がどのような方法で行われるかの情報が格納されます。
赤の部分eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
をbase64でデコードすると、{"alg":"HS256","typ":"JWT"}
になります。
注目すべきは"alg":"HS256"
の部分です。
この部分は、署名に使っているアルゴリズム(alg)の情報です。
この例で言えば、HS256(HMAC using SHA-256 hash)を使っていることが分かります。
HMACの実装などの説明は本記事では省きますが、とりあえず電子署名と改竄検知のためにHS256というアルゴリズムを使っているんだな、くらいに思ってください。
紫の部分(Payload)
ペイロード(Payload)、つまり送りたい情報本体(Claim)が入る部分です。 同じくデータをbase64でエンコードしてピリオドで連結されています。
JWTの発行者は自分で定義した固有のclaimを追加することが可能です。
今回の例で言えば、"user": "guest"
は私が独自に定義したclaimです。
一方で、RFC7519にはRegistered Claim Namesとして予め決められているclaimがいくつか定められています。 今回は代表的なものだけを抜粋して説明いたします。 また、以下のRegistered Claim Namesはいずれもoptionalなので無くても構いません。
claim | detail |
---|---|
iss | Issuer JWTの発行者識別子 |
aud | Audience 誰を対象に発行されたか |
exp | expiration time 期限を示す |
iat | Issued at 発行された日時 |
攻撃者が改竄したいのはおそらくこのペイロードの部分です。
現在デコードしているJWTで言えば、ペイロードの"user": "guest"
を"user": "admin"
などに改竄して不正にadminになりすますことを目論むかもしれません。
他にも、JWTの有効期限を表す予約語expなどを改竄して不正に有効期限を延ばすなどの不正利用も考えられます。
青の部分(Signature)
署名(Signature)です。 この部分は通常、base64でエンコードしピリオド(.)で繋いだヘッダーとペイロードを署名アルゴリズム(ここではHS256)でハッシュ化したものです。
SignatureとJWTの関係を疑似コードで表すと、以下の様になります。
value = header + "." + payload signature = HS256(value, secret_key) JWT = header + "." + payload + "." + signature
こうすることで、署名検証時にはHeaderとPayloadをピリオドで連結したものを指定された署名アルゴリズムで処理したものとSignatureの部分にある文字列を比較することで、改竄されていないかどうかをチェックすることが可能になります。
攻撃を通して理解を深める
基本的な知識を紹介したところで、早速ハンズオンで実際にJWTを改竄して理解を深めていきましょう!
ハンズオンのリポジトリはこちらになります→https://github.com/KinakoExE/jwt-attack-hands-on
環境構築
動かすために必要なものは以下の通りです。
- Golang
- curlコマンド
- Python3
現在の環境に上記のいずれかが入ってない場合はインストールをお願いします🙇♂️ なお、ハンズオン中にJohn The Ripperというツールを使う箇所がありますが、使う前に説明いたしますので今は用意しなくても構いません。
また、答えを見ずに解きたい方は次からの紹介&解説を見る前に挑戦することをおすすめします。
JWT none Attack
JWTはalgの部分に署名検証に用いるアルゴリズムが入りますが、中には署名検証を行わないことを意味するnoneというものがあります。
RFCにはこう書かれています。
To support use cases in which the JWT content is secured by a means other than a signature and/or encryption contained within the JWT (such as a signature on a data structure containing the JWT), JWTs MAY also be created without a signature or encryption.
訳)
JWT内に含まれている署名や暗号化以外の方法でJWTの内容が保護される使用例をサポートするために、JWTは署名や暗号化なしでJWTを作成することもできます。
これを実現するのがnoneアルゴリズムです。
この仕組みを悪用し、デコードしたJWTのalgを"alg": "none"
に書き換えて再びエンコードしてトークンとして使用すると、noneを受け入れる環境では改竄したJWTが通ってしまいます。
それではハンズオンで実際に体験してみましょう。
jwt-attack-hands-onの中のnone-attackに移動し、go run main.go
を実行してください。
5555番ポートでアプリが立ち上がります。
用意したハンズオンでは、ルートの/
、JWTを得られる/token
、そしてadminしかアクセスできない/admin
の3つのエンドポイントが用意されています。
いずれのハンズオンでも、/admin
にadminユーザとしてアクセスすることがゴールになります。
/token
にアクセスして手に入れたトークンをjwt.ioでデコードしてみましょう。
署名アルゴリズムにはHS256が使われていることが分かります。 algをnoneに、そしてuserをadminに書き換えたいですね。
ここで一度、/token
で受け取った"alg":"HS256"
かつ"user":"guest"
の改竄していないJWTを/admin
に送信して何が起こるのかを確認しましょう。
受け取ったJWTのトークンを以下のcurlコマンドで送信してみます。
curl http://localhost:5555/admin -H "Authorization: Bearer <JWT>"
すると以下のように、"Hello, guest , but you are not admin!"
というメッセージが返ってくることが確認できます。
つまり、adminではないので追い出されてしまった構図です。
ここで同梱しているPython3のスクリプトjwt-none-attack.py
を使って改竄したJWTを作ってみましょう。
ファイルを開いたら、jwt = "<Please input your JWT>"
の部分に自分が受け取ったJWTを入力し、実行してください。
無事改竄したJWTが手に入ったら、再びjwt.ioでデコードしてみましょう。
無事に署名アルゴリズムをnoneに、userをadminに改竄したJWTができあがっていることが確認できます。
それでは改竄したJWTを使って/admin
へアクセスしてみましょう!
curl http://localhost:5555/admin -H "Authorization: Bearer <改竄したJWT>"
(成功時に何が出るかはお楽しみということでぼかしています)
none-attackまとめ
初歩的なJWTの改竄方法とそれを利用した攻撃であるJWT none attackを解説しました。
JWTをセッションCookieとしてセッション管理に使っている場合、ユーザの識別にusernameあるいはuser_idなどの値を入れて運用していると仮定します。 その際、サーバ側でnoneアルゴリズムを受け入れる設定にしているとやりたい放題されるので、noneは受け入れないようにしましょう。
とはいえ、現在のほとんどのJWTライブラリは明示的にnoneを許可しなければ基本受け入れないようになっているはずです。 (今回使用したjwt-goというライブラリではそうなっていました)
補足)
RFCによるとnoneアルゴリズムを使用する際はSignatureを省略するように定義されています。
しかし、今回のハンズオンで"alg":"none"
を指定してSignatureを省略しなくてもJWTが通るのは、私がハンズオンを円滑に進めるためにライブラリの一部を改変しているからです。
今回使用したのはjwt-goというライブラリですが、本来のjwt-goはちゃんとRFCの仕様に沿って実装されているのでご安心ください。
brute-force secret
では、"alg":"none"
をサーバ側で拒否することにしましょう。
これでひとまずJWT none attackは防げました。
それで本当に大丈夫なのでしょうか?🤔
brute-force-secretディレクトリでgo run main.go
でサーバを起動し、/token
でトークンを発行してください。
私が受け取ったトークンはjwt.ioでデコードするとこんな感じです。
署名検証アルゴズムはHS256なので、secretが署名に使われています。
今回は"alg":"none"
はサーバ側で拒否されるように設定したので、いくら頑張って改竄してもsecretを用いた検証で検知され、JWTは不正と見なされて受け入れられません。
JWT none attackが使えない=攻撃者はもうなす術無しでしょうか?
いいえ、そうとは限りません。
John The Ripperを用いたブルートフォース
ここでJohn the ripperに登場してもらいます。
John the ripperはオープンソースのパスワードクラックツールです。
John the Ripper is an Open Source password security auditing and password recovery tool available for many operating systems
クラックツールとはいえ、本来はパスワードor秘密鍵の総当たり攻撃などに対する耐性が十分かなどを検証するためのツールです。
というわけで早速先ほどのJWTをjohn the ripperにかけてみましょう。 まずはJohn The Ripperの環境構築を行います。
git clone https://github.com/magnumripper/JohnTheRipper // John the ripperの公式リポジトリからクローン cd JohnTheRipper/src ./configure make -s clean && make -sj4 // ビルド cd ../run vim jwt.txt // /tokenで発行されたJWTを記載したテキストファイルを作成 ./john jwt.txt // 実行
数秒待つと、以下のようにsecretが割れて黄色で表示されます。
正しく動作すれば、上の画像でぼかされた部分にsecretが載っているはずです。
あとはjwt.ioに戻り、画面右下のsecretを設定する部分に先ほど割れたsecretを設定してuserをadminに改竄してしまいましょう。
あとはnone-attackのときと同様に、Authorizationヘッダに改竄したJWTを付けて/admin
としてアクセスできるかどうか試してみてください!
brute-force secretまとめ
brute-force-secretのハンズオンを通して皆さんに伝えたかったのは、secretの鍵長が短いといとも簡単に破られるということです。
今回は演習として非常に短いもの(48-bit)をsecretに設定しましたが、時間をかければもう少し長い鍵長のsecretも割り出せそうです。
1文字が1バイト=8-bitなので、banana
なら48-bitということになります。
RFCによると、HS256を使う場合は256-bit以上の鍵長を使用しなければならない(must)とされています。 よって32文字以上かつ辞書攻撃を防ぐためにランダムな文字列をsecretとして設定するといいでしょう。
参考までに、Auth0はHS256のJWTなら512-bitのsecretを使用しているそうです。
まとめ
いかがだったでしょうか? 今回のJWTを改竄するハンズオンを通して、皆さんのJWTそのものについての理解が深まったのであれば幸いです。
本当はJWTにまつわる攻撃方法はまだ複数存在するのですが、今回の記事の主題は攻撃ではなく攻撃を通してJWTを学ぶことだったので、尺の都合もあり泣く泣く省略させていただきました(泣)
興味がある方はRS256 to HS256
やJWKS spoofing
などといったキーワードで検索してみてください。
最後に、本記事で取り扱った内容はJWTを使わないような開発においてはすぐ役立つといったものではありませんが、今後の開発の場面で「攻撃者ならどうするだろうか」という視点を持って開発したり、問題意識を高めたりすることでプロダクトを少しでもセキュアにできるかもしれません。
この記事がいつかそうした瞬間に貢献できれば幸いです!!
--
マネーフォワードでは、エンジニアを募集しています。 ご応募お待ちしています。
【サイトのご案内】 ■マネーフォワード採用サイト ■Wantedly ■京都開発拠点
【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』