セッションIDと認証チケット

以前の日記で、ASP.NETのセッション固定対策について書きました。

その結論をまとめると、

  • ASP.NETにはセッションIDを変更するまともな方法が存在しない。
  • そのため、ASP.NETではフォーム認証機構(FormsAuthentication)を使ってログイン状態管理を行うべき。
  • FormsAuthenticationは、通常のセッションID(ASP.NET_SessionId)とは別の認証チケット(Cookie)をログイン成功時に発行し、この認証チケットによってログイン後のユーザの識別を行う仕組み。

ということになります。

ASP.NETのサイトに限らず、セッション(PG言語やフレームワークに組み込みのセッション機構)と、認証チケットの両方を使用しているサイトはたまに見られます*1

特にポータルサイトのような大規模なサイトは、ログインをつかさどるシステムと、会員向けのブログや日記、ニュース、ショッピングなどの各種機能を提供する多数のサブシステム(開発言語やサーバの物理的な場所などはバラバラ)から構成されています*2。これらのシステムでSSO(Single Sign On)を実現するために、ログインをつかさどるシステムが認証チケットを発行し、各種会員向け機能を提供するサブシステムでは、サブシステム毎のデータを扱う個別のセッションと、認証チケットの両方を使用していることがあります。

本日はそのようなサイトで見られる脆弱性について書きたいと思います。ただし、いわゆるセッション機構と認証チケットの両方を使用するといっても、その使い方はサイトによって千差万別であり、脆弱性のあり方もまた千差万別です。あまり網羅性は気にせずに、思いつくままに書いていきたいと思います。

前提とするのはPCブラウザ向けのサイトです。認証チケット、セッションIDともにCookieに格納しているとします。ただし、WebアプリはGETパラメータで与えられた認証チケット・セッションIDも受け付けると仮定します(説明を判りやすくするため)。認証チケットは「AuthTicket」、セッションIDは「SessID」という名前であらわします。

1.セッションIDと認証チケットの両方を見るタイプ

まずは、セッションIDと認証チケットの両方を、関連付けることなく使用しているサイトをとりあげます。
つまり、ログイン後のページでは、認証チケットをデコードして会員IDを得る処理を都度行うとともに、それとは独立して仕掛りデータの保存用にセッション変数を使うアプリです。

ASP.NETのFormsAuthenticationを使った場合も、特に何も考えずにセッションを使用したならばこの状態となります。

例としてとりあげるのは、ログイン後に会員個人情報の変更を行うアプリです。このアプリでは、ユーザがフォームで入力した個人情報をセッション変数に一時保存します。変更完了処理では、セッション変数から個人情報を取り出して、それを会員に紐付けて会員情報DBに保存します。

1.1 セッション固定

このようなアプリでありがちなのはセッション固定の脆弱性です。

攻撃は、被害者が個人情報の入力を開始するよりも前に、被害者のブラウザにセッションIDを植えつけることからはじまります。

ただし、このアプリでは、セッションIDを固定化しても被害者のユーザになりすますことはできません。なりすますには認証チケットの方が必要で、なにか別の脆弱性でもない限り攻撃者はそれを入手することはできないからです。

しかし被害者は攻撃者が植えつけたセッションIDを持っており、被害者がフォームで入力した個人情報は、そのセッションIDに紐付いてセッション変数に保存されます。実際に、いま被害者は個人情報の入力を終えて確認画面を表示しており、セッション変数には被害者の個人情報が保存されているとします。攻撃者の狙いはこのデータを奪うことです。

このアプリでは、利用者が確認画面から入力画面に戻って情報を修正できるようにしており、このときセッション変数から情報を引き出してフォームに埋め込んだ画面をユーザに戻しているとします。

攻撃者はこの挙動を利用します。攻撃者は自ら以下のようなURLにアクセスします。

https://www.example.jp/inputProfileBack.cgi
        ?SessID=(被害者に使わせたセッションID)
        &AuthTicket=(攻撃者の認証チケット)

inputProfileBack.cgiは、(確認画面から戻って)個人情報を入力するフォームを表示するCGIです。

本来の正常なフローであれば、inputProfileBack.cgiはセッションID・認証チケットともに被害者のもの(Cookie)を受け取ります。しかし、前述のように攻撃者は被害者の認証チケットを入手することはできません。そのため、被害者の認証チケットの代わりに攻撃者の認証チケットを付けて入力画面にアクセスしています。何らかの認証チケットがないと、常にログイン画面にリダイレクトするようなアプリでは、このような小細工が必要です。

アプリ(inputProfileBack.cgi)がセッション変数内の個人情報の持ち主である会員と、認証チケットが指し示す会員が同じかをチェックするロジックを持たないならば、アプリにとっては有効な認証チケットと、有効なセッションIDの両方を受け取ることになります。アプリは素直にセッション変数内にある被害者の個人情報を埋め込んだ画面を攻撃者に返してしまうでしょう。

1.2 別人でコミットさせる

セッションIDが都度変化するような対策が施されている場合には、1.1の攻撃は成功しません。しかし他の方法による攻撃が成功する場合もあります。

さきほどと同じく、ログイン後に個人情報を変更するアプリを取り上げます。被害者は、個人情報の入力を終えて確認画面を表示しているとします。被害者のセッション変数には、入力された被害者の個人情報が保存されており、攻撃者はこれを奪おうとしています。

攻撃者はまず、被害者に以下のURLを踏ませます。

https://login.example.jp/login.cgi
        ?UserID=evil&password=evilpass

login.cgiはログインを行うCGIです。このURLを踏まされた被害者は、攻撃者の会員アカウントである"evil"でログインした状態("evil"の認証チケットCookieを持つ状態)となります。

一方、被害者のセッション変数には相変わらず被害者の個人情報が入っています。したがって、このまま被害者に個人情報変更を完了させれば、セッション変数内の被害者の個人情報を、攻撃者アカウントのものにすることができるかもしれません。

被害者に個人情報変更を完了させるには、通常はCSRF対策を突破しなければなりません。CSRF対策方法にもいくつかの種類がありますが、このアプリではパスワードを入力させるタイプのCSRF対策が取られていたとします。そうであれば、CSRF対策を突破するのは簡単です。

被害者に以下のURLを踏ませます。

https://www.example.jp/commitProfileChange.cgi
        ?Password=evilpass

commitProfileChange.cgiは個人情報変更を確定させるCGIです。被害者は今や"evil"でログインした状態ですので、ここでは"evil"のパスワードである"evilpass"をパラメータとしてつければよいことになります。

先ほどの1.1と同じく、アプリ(commitProfileChange.cgi)がセッション変数内の個人情報の持ち主である会員と、認証チケットが指し示す会員が同じかをチェックするロジックを持たないならば、セッション変数内の被害者の個人情報は、攻撃者の"evil"アカウントに紐付いて会員情報DBに保存されるでしょう。攻撃者は自分のアカウントである"evil"でログインして会員個人情報を参照するページに行けば、被害者の個人情報を盗み見ることができます。

ちなみに個人情報変更のCSRF対策として、パスワード以外のものを使うアプリも多くあります。そういう場合には、別のもう少し手のかかる方法を使って被害者に変更確定を強制できる場合もありますし、被害者が確認画面上の変更確定ボタンを押すのをじっと待つしかないこともあります。

なお、ここで書いたような攻撃(無理やり別人アカウントでログインさせた上で、変更をコミットさせる攻撃)は、認証チケットを使っているサイトでのみ成功するわけではありません。ルーズな処理を行っているならば、セッションだけを使っているサイトでも成功することがあります。

1.3 CSRF

今度はCSRF(Cross Site Request Forgery)攻撃です。今までと違って、攻撃者の狙いは被害者の情報を奪うことではなく、攻撃者が指定した値で被害者に個人情報変更を実行させることです。

認証チケットとセッションを併用するサイトにおいては、会員アカウントと紐付かないCSRF対策用のトークンを使用している問題に起因するCSRF脆弱性がしばしばみられます。

そのような問題を持つ場合、以下のような手順で攻撃が成功します。

まず、攻撃者は自らのアカウントでログインして個人情報変更の確認画面まで進めます。これにより、攻撃者のセッション変数には、攻撃者が入力した個人情報が保存されます。先ほどまでのアプリとは違い、このアプリではCSRF対策にワンタイムトークン(AntiCSRFToken)が使われているとしましょう。攻撃者は自身の確認画面のhiddenに入っているワンタイムトークンと、自身のセッションID(Cookie)をメモしておきます。

そして、認証チケットCookieのみを持っている被害者を、以下のような罠のURLにアクセスさせます(commitProfileChange.cgiは個人情報変更を確定させるCGIです)。

https://www.example.jp/commitProfileChange.cgi
        ?SessID=(攻撃者のセッションID)
        &AntiCSRFToken=(攻撃者のトークン)

被害者が罠を踏むと、サーバに送信される認証チケットCookieは被害者のものです。同時に送信されるセッションIDとCSRF対策用トークンは攻撃者のものです。

アプリが、セッションやトークンをアクセスしている会員と関連付けていない場合、アプリは受け取ったセッションIDとトークンを「妥当なペアである」と判断して、会員情報DBへの書き込み処理を進めようとするでしょう。アプリが受け取るセッションIDとトークンは、攻撃者が実際に自分のアカウントでログインして確認画面から取得した"本物"だからです。

攻撃者が入力した個人情報はセッション変数に入っています。これもアプリが会員と関連付けていないならば、この個人情報は被害者アカウントのものとして会員情報DBに登録されるでしょう。というのは、被害者が罠を踏んだ時にアプリに渡される認証チケットCookieは、被害者アカウントのものだからです。

2.最初だけ認証チケットを見るタイプ

「1.セッションIDと認証チケットの両方を見るタイプ」のアプリは、認証チケットとセッションの両方を、独立して使用するタイプのものでした。

それに対して、ここで取り上げる「2.最初だけ認証チケットを見るタイプ」のアプリは、最初のタイミングで認証チケットをデコードして会員IDを取り出し、それをセッション変数に保存します。既にセッション変数に会員IDが保存されている状況では、認証チケットは参照せずに、セッション変数の会員IDだけを見てアクセス者がどの会員なのかを識別します。

このようなアプリでも、セッション固定の脆弱性は多くみられます。

まず、攻撃者はWebサイトにアクセスしてセッションID Cookieを取得します。その後に、認証チケットCookieのみを持っている被害者を、以下のような罠のURLにアクセスさせます。

https://www.example.jp/id_init.cgi
        ?SessID=(攻撃者のセッションID)

id_init.cgiは、認証チケットをデコードして会員IDを取り出して、セッション変数に保存する処理を行うものだと思ってください。

攻撃者がつけたGETパラメータのセッションIDは、被害者の認証チケットCookieとともにWebアプリに送られます。アプリは認証チケットから被害者の会員IDを取り出して、それを攻撃者のセッション変数に保存してしまいます。

セッションIDがこのタイミングで変化しないならば、攻撃者はそのセッションIDを使ってアプリにアクセスすることで、被害者会員へのなりすましに成功します。

対策

様々な対策方法が考えられますが、なるべくシンプルなものを挙げます。まずは対策が簡単な「2.最初だけ認証チケットを見るタイプ」の対策から説明します。

「2.最初だけ認証チケットを見るタイプ」の対策

このタイプの対策は、通常のセッションのみを使うアプリと基本的に変わりません。

  1. ログイン時に、セッションIDを変更する。
  2. セッション変数内の情報をログイン時に消す。

ひとことでいうと、「ログイン時にセッションを再生成せよ」ということになります。

注意が必要なのは、ここでいう「ログイン」とは「認証チケットをデコードしてセッション変数に入れる」タイミングであるということです。また、2番目の「セッション変数内の情報をログイン時に消す」については、「1.2 別人でコミットさせる」のようなタイプの攻撃への対策として必要です。

「1.セッションIDと認証チケットの両方を見るタイプ」の対策

こちらは様々な対策方法がありますが、大きく分けて2つのアプローチがあると思います。

セッションと会員を関連付ける対策

既にみたように、このタイプのアプリへの攻撃の常とう手段は、セッションIDと認証チケットの片方を攻撃者のものに置き換えることです。ですので、両者をきちんと関連付けして、片方だけを置き換えられないようにしようというのが基本的な考え方です。

具体的には、会員と紐付けられるべき情報をセッション変数に出し入れする際に、(最低限)以下の処理を行います。

  1. 最初に、セッションがどの会員のものであるか、セッション変数に"しるし"をつける。
  2. 認証チケットが指し示す会員と、セッションに付けた"しるし"が異なる場合はエラーとする。

ASP.NETのFormsAuthenticationとセッションを同時に使う場合には、この方法をとるしかないと思います。

なお、セッションをまるごと会員と関連付けるのではなく、セッション変数内の個別の情報の単位で会員と関連付ける方法もあります。

セッションと会員を関連付けない対策

会員とセッションの関連付けをしないのならば、以下の対策が必要です。

  1. セッション変数に書き込みを行う都度、セッションIDを変更する。
  2. CSRF対策用トークンには、会員と紐付く情報(認証チケットやパスワード)と、セッションと紐付く情報(セッションIDそのものやセッション変数に入れたトークン)の両方を使う。

このケースでは、セッションは会員と関連付かないため、ログイン前のセッション固定対策と同じように、毎回のセッションID変更が必須となります。

また、CSRF対策には2つのトークンが必要になります*3。なぜならば、このケースでは、ユーザは「認証チケットを持ったある会員」という顔と、「セッション変数の持ち主である匿名の誰か」という2つの顔を持つ存在であり、2つの顔の片方が置き換えられないようにする必要があるからです。

しかし、一人のユーザが2つの顔を持つという状況はややこしいので、セッションと認証チケットを関連付けするか、「1.最初だけ認証チケットを見るタイプ」にした方が無難ではないかと思います。

*1:認証チケットのことをセッションと呼ぶこともあるため紛らわしいですが、この日記でいう認証チケットとは、ログイン時に発行され、チケットそのものに会員IDやログイン有効期限等の情報を含んでいるトークンを指しています。またセッションとは、JSESSIONID、PHPSESSID、ASP.NET_SessionIdなどのように、キーをクライアントにCookie等の形で渡して、それに紐付く情報をサーバ側のセッション変数に保存できる仕組みを指しています。

*2:私自身も過去にこのようなシステムの開発に携わっていました。

*3:正確には、2つを統合した1つのトークンを使うことも可能です。