Stand Aloneスクリプト編(3) オリジナルスクリプト作成


前回のスクリプトを拡張して、履歴からディレクトリ一覧を取得しリストアップするスクリプトを書いてみます。
プロテクトモード限定で動作し、ルートノードがスコープ内にある(コンテキストに入っている)URIのみディレクトリを抽出します。

・ZAPの履歴からディレクトリ一覧を取得するスクリプト(Stand Alone版)
// rootを取得
var root = org.parosproxy.paros.model.Model.getSingleton().
        getSession().getSiteTree().getRoot();

// modeを取得
var mode =  org.parosproxy.paros.control.Control.getSingleton().getMode();

// ディレクトリ一覧を格納する配列
var dirlist = [];

// protected modeの時のみ
if (mode.equals(org.parosproxy.paros.control.Control.Mode.protect)) {

    // ルートノードの子ノードを取得 EnumSiteNode:esn
    var esn = root.children();
    while (esn.hasMoreElements()) {
        // SiteNode:sn
        var sn = esn.nextElement();

        // ノードがスコープ内の時のみ
        if(sn.isIncludedInScope()){
            // ルートディレクトリを格納
            dirlist.push(sn.getHistoryReference().getURI().toString());
            // 子ノードを再帰で探索
            listChildren(sn, 0, dirlist);
        }
    }
}

// ディレクトリ一覧を表示
for(var i=0, len=dirlist.length; i<len; i++) {
    print("dir:"+dirlist[i]);
}

// 子ノードを再帰探索し、ディレクトリをリストアップする関数
function listChildren(node, level, dirs) {
    var str="";
    var i;
    for (i=0;i<level;i++) str+="  ";
    var j;
    for (j=0;j<node.getChildCount();j++) {
        var nodename = node.getChildAt(j).getNodeName();

        var uri = node.getChildAt(j).getHistoryReference().getURI().toString();

        // 現在のノードに拡張子がなければディレクトリ名とみなして配列に格納する
        if (nodename.indexOf('.') == -1) {
            // ディレクトリなので末尾に/がなければ付ける
            if(uri.substr(uri.length-1) != '/'){
                uri = uri + '/';
            }

            // 配列に既にあれば格納しない
            if(dirs.indexOf(uri) == -1){
                dirs.push(uri);
            }
        }

        listChildren(node.getChildAt(j), level+1, dirs);
    }
}
このスクリプトをStand Aloneとして保存し、ZAPをプロテクトモードにし、診断対象となるURLをコンテキストに入れた状態で、対象サイト内を一通り遷移した後に対象サイトのルートノードに対して実行すると、ZAPの履歴内からディレクトリをリストアップしてくれます。

上記条件を揃えて実行すると、

dir:http://localhost/
dir:http://localhost/dashboard/
dir:http://localhost/dashboard/images/
dir:http://localhost/dashboard/javascripts/
dir:http://localhost/dashboard/stylesheets/


のような結果が下のペインに表示されます。

ちょっといまいちなのが、ノード名に「.」が入っているかどうかでディレクトリかどうかの判定をしている箇所で、ここは雑すぎないか、という感覚があるものの、どのように判断したらURLがディレクトリなのかファイルなのか判定のロジックが思いつかなかった / 調べても良いサンプルが見つからなかったのでこういう形にしてあります。
(当然ながら、このロジックだと名前にドットが含まれているディレクトリをディレクトリとは判定しないバグがあります。そういうURLがある場合はスクリプトを改修(もしくは運用でカヴァー)して対応してください)

ディレクトリ一覧を洗い出すだけなら「プロテクトモードの中だけ」などの条件はいらないのですが、次にこのスクリプトにさらに手を加えて、「履歴からディレクトリ一覧を洗い出し、そのディレクトリに対してOPTIONSメソッドを送信し、レスポンスを見てOPTIONSが有効だったらログに出力する」というものにしてみたいと思います。

・ZAPの履歴からディレクトリ一覧を取得し、OPTIONSメソッドを実行するスクリプト(Stand Alone版)
// sender生成
var sender = new org.parosproxy.paros.network.HttpSender(
    org.parosproxy.paros.model.Model.getSingleton().getOptionsParam()
     .getConnectionParam(), true, 6) 

// rootを取得
var root = org.parosproxy.paros.model.Model.getSingleton().
        getSession().getSiteTree().getRoot();

// modeを取得
var mode =  org.parosproxy.paros.control.Control.getSingleton().getMode();

// ディレクトリ一覧を格納する配列
var dirlist = [];

// protected modeの時のみ
if (mode.equals(org.parosproxy.paros.control.Control.Mode.protect)) {

    // ルートノードの子ノードを取得 EnumSiteNode:esn
    var esn = root.children();
    while (esn.hasMoreElements()) {
        // SiteNode:sn
        var sn = esn.nextElement();

        // ノードがスコープ内の時のみ
        if(sn.isIncludedInScope()){
            // ルートディレクトリを格納
            dirlist.push(addSlashToURIString(sn.getHistoryReference()
                                                .getURI().toString()));
            // 子ノードを再帰で探索
            listChildren(sn, 0, dirlist);
        }
    }
}

// ディレクトリごとにループ
for(var i=0, len=dirlist.length; i<len; i++) {
    print("dir:"+dirlist[i].toString());

    var reqheader = new org.parosproxy.paros.network.HttpRequestHeader();
    reqheader.setMethod("OPTIONS");
    reqheader.setURI(convertStrToURI(dirlist[i]));

    // HttpMessageオブジェクト生成
    var msg = new org.parosproxy.paros.network.HttpMessage(reqheader);

    // リクエスト送信 & レスポンス受信
    sender.sendAndReceive(msg);

    // レスポンスヘッダを文字列として変数に格納
    var rsp = msg.getResponseHeader().toString();
    // レスポンスヘッダ内に"Allow:" ヘッダがあれば
    if(msg.getResponseHeader().getHeaders("Allow") != null){
        // "Allow:"ヘッダ の内容を出力
        print("[OPTIONS] " + msg.getResponseHeader().getHeader("Allow"));
    } else {
        print("-");
    }
    print("-------------------------------------------"
         +"-------------------------------------------");
}

// 子ノードを再帰探索し、ディレクトリをリストアップする関数
function listChildren(node, level, dirs) {
    var str="";
    var i;
    for (i=0;i<level;i++) str+="  ";
    var j;
    for (j=0;j<node.getChildCount();j++) {
        var nodename = node.getChildAt(j).getNodeName();

        var uristr = node.getChildAt(j).getHistoryReference().getURI().toString();

        // 現在のノードに拡張子がなければディレクトリ名とみなして配列に格納する
        if (nodename.indexOf('.') == -1) {

            uristr = addSlashToURIString(uristr);

            // 配列に既にあれば格納しない
            if(dirs.indexOf(uristr) == -1){
                dirs.push(uristr);
            }
        }

        listChildren(node.getChildAt(j), level+1, dirs);
    }
}

// URI文字列の末尾が'/'でなければ'/'を付けて返す
function addSlashToURIString(uristr){
    // ディレクトリとみなし、末尾に/がなければ付ける
    if(uristr.substr(uristr.length-1) != '/'){
        uristr = uristr + '/';
    }
    return uristr;
}

// URI文字列をURIオブジェクトにする
function convertStrToURI(uristr){
    return new org.apache.commons.httpclient.URI(uristr);
}

このスクリプトを実行すると、履歴からスコープ内のURLに限定してディレクトリを抽出し、OPTIONSメソッドのリクエストを発行して結果を判定してくれます。

実行結果はこのような感じです。

dir:http://localhost/
-
--------------------------------------------------------------------------------------
dir:http://localhost/testdir1/
[OPTIONS] OPTIONS,GET,HEAD,POST,TRACE
--------------------------------------------------------------------------------------
dir:http://localhost/testdir1/index_exists/
[OPTIONS] OPTIONS,GET,HEAD,POST,TRACE
--------------------------------------------------------------------------------------
dir:http://localhost/testdir1/testdir2/
[OPTIONS] OPTIONS,GET,HEAD,POST,TRACE
--------------------------------------------------------------------------------------
dir:http://localhost/testdir1/testdir2/testdir2_1/
[OPTIONS] OPTIONS,GET,HEAD,POST,TRACE
--------------------------------------------------------------------------------------


このスクリプトのロジックを改修すれば、TRACEメソッドやその他のHTTPリクエストメソッドを各ディレクトリに発行して反応を調べるなどのスクリプトも書けると思うので、改修すればいろいろと便利な診断用の処理が書けるのではないかと思います(当然ですが悪用厳禁です)。

このスクリプトには一点実装を諦めたポイントがあり、本当はOPTIONSが有効なディレクトリを見つけたらZAPのアラートとして記録したかったのですが、StandAloneスクリプトからアラートを登録するようなサンプルは見つからず、調べてコードを書いてみてもうまく行きませんでした
(スクリプトからZAPの全機能が使えるはずなので、やろうと思えば手段はあるとは思うのですが、うまい手段をご存じの方がおられたらご教示ください)。

あとは、このスクリプトはあくまで手動でアクセスしたディレクトリおよび親ディレクトリしか検出しないので、探査自体は自力でやる必要があるという欠点があります。が、「各ディレクトリごとにXXをする」という処理が自動化できるだけでもだいぶ手間が軽減されるのではないかと思います。


上記のコードはそんなに高度な内容ではないと思うのですが、資料がないのであちこち情報やサンプルコードを探す必要があり結構大変でした。

コードを書く際、資料として有用だったものを挙げておきます。

・ZAPに入っているTemplateのスクリプト
・ZAPのヘルプの「Scripts」の項
・ZAPのJavaDoc (https://github.com/zaproxy/zaproxy/wiki/JavaDocs)のindex-all.htmlでのページ内検索
・Community Scripts(https://github.com/zaproxy/community-scripts)にあるスクリプトのコード
・Google Group: OWASP ZAP Developer Group(https://groups.google.com/forum/#!forum/zaproxy-develop)で不明な点を検索
(特にStand Aloneスクリプトからリクエスト/レスポンスを行う方法 https://groups.google.com/d/msg/zaproxy-users/QKV84rtv-7A/YSy25n0tEAAJ

次は、ActiveScanのスクリプトについて解説する予定です。

※2016/7/2 サンプルコードの整形、本文の加筆修正(主に言い足りなかった点の加筆)を行いました。

次へ

Stand Aloneスクリプト編(2) テンプレートスクリプトを動かす


Hello World成功だけでは面白くないので、今度はZAPに組み込まれているStand Aloneのテンプレートを使って、サンプルスクリプトを実行してみます。

1) 前回と同じように「スクリプト」タブのStand Aloneを右クリック-「新規スクリプト…」を選択。新規スクリプトのダイアログで以下のように入力し「保存」をクリック。

Script名:script_test2
タイプ:Stand Alone
Script engine:ECMAScript:Oracle Nashorn
テンプレート:Traverse sites tree.js
説明:任意の内容
開始時にロード:チェック



前回と違うのは、テンプレートとして「Traverse sites tree.js」を選択した点です。

「保存」を押下すると、「Scriptコンソール」の上ペインに以下のようなコードが現れます。
// This script traverses the sites tree - change it to do whatever you want to do :)
//
// Standalone scripts have no template.
// They are only evaluated when you run them. 

function listChildren(node, level) {
    var i;
    for (i=0;i<level;i++) print ("    ");
    var j;
    for (j=0;j<node.getChildCount();j++) {
        println(node.getChildAt(j).getNodeName());
        listChildren(node.getChildAt(j), level+1);
    }
}

root = org.parosproxy.paros.model.Model.getSingleton().
        getSession().getSiteTree().getRoot();

listChildren(root, 0);
ここに表示されたコードは、Script作成時に「テンプレート:」に指定したファイルから読み込んできたコードです。

テンプレートの元となっているファイルはどこにあるかというと、見落としやすいのですが「Scripts」タブの下の方の「Templates」の中にあります。

ZAPスクリプトを自作するにあたって、「Templates」にあるファイルは重要な参考資料になりますので、どんなコードがあるのか見て回ると良いかもしれません。


ではこの「script_test2」スクリプトを動かしてみましょう。

このスクリプトは、名前が「Traverse sites tree」であることからも分かるように、ZAP上に何らかの履歴がある(「サイト」タブに何かのサイトが表示されている)必要があります。
サイトタブが空だと実行しても何も起こらないようです。

ひとまず、このスクリプトを動かすため、ローカルWEBサーバーをXAMPPなどを利用して立て、それにZAP経由でアクセスして履歴をZAP内に記録します。(本例ではhttp://localhost とします)

それから「Scripts」タブで「script_test2」スクリプトを選択して、「Scriptコンソール」上部の「実行」ボタンを押下すると……printlnが未定義と言われてしまいます。

<eval>:11 ReferenceError: "println" is not defined<eval>:11 ReferenceError: "println" is not defined




ZAPのコミュニティなどでこのエラーメッセージの対処方法を探してみると、このエラーが出た場合はprintlnの代わりにprintを使えばよいとのこと。

(RhinoというJava 7までのJavaScript engineを利用しているとこのエラーが発生すると言っている人もいるのですが、スクリプト作成時に「Script engine:ECMAScript:Oracle Nashorn」とJava8のJavaScript engineであるNashornを選択しているのにこの問題が発生するので、これが原因ではなさそうです。もしかしたらこのエラーが発生せずにそのまま動作する環境もあるのかもしれませんが、情報が少なくてよく分かりません)

アドバイスに従い、コード中の"println"を"print"に書き換え、再度「実行」を押下すると……今度は動作しますが、おそらくはコード作者が意図しているようなツリーっぽい表示にはなりません。ノードのリストアップはされるものの、空行がたくさん挟まった見づらいリストになってしまいます。

実行結果:


(※なお、このスクリプトでは外部サイトへの新しいアクセスは行わず、ZAP内部の記録だけを使って処理をしているため、もし外部サイトに関するノードが実行結果に出てきてしまっても実際のアクセスはしていないので問題ありません)

おそらくprintlnをprintに書き換えたせいでスクリプトが想定した動作をしていないと思われるので、ちゃんとツリーっぽい表示になるように直してみます。
function listChildren(node, level) {
    var str="";
    var i;
    for (i=0;i<level;i++) str+="  ";
    var j;
    for (j=0;j<node.getChildCount();j++) {
        print(str + node.getChildAt(j).getNodeName());
        listChildren(node.getChildAt(j), level+1);
    }
}

root = org.parosproxy.paros.model.Model.getSingleton().
        getSession().getSiteTree().getRoot();

listChildren(root, 0);

これを実行すると、例えばこんな感じのツリー表示が下のペインに作成されます。
http://localhost
  GET:dashboard
  dashboard
    images
      GET:bitnami-xampp.png
      GET:xampp-logo.svg
      GET:fastly-logo.png
      GET:social-icons.png
      GET:favicon.png
    javascripts
      GET:modernizr.js
      GET:all.js
    stylesheets
      GET:normalize.css
      GET:all.css

今回はテンプレートスクリプトを動作させてみました。

余談として、ZAPはデフォルトだとスクリプトの出力が追記になるので不便なのですが、ほうきの隣にあるこのボタンを押下すると、スクリプト実行前に前回の出力を削除してくれるので開発時などに便利です。


次は、このテンプレートスクリプトを拡張してもう少し役に立つスクリプトを作成してみます。

次へ

OWASP ZAPにはカスタムスクリプトを作り、実行する機能がありますが、便利そうな機能なのに解説のドキュメントがあまりありません。

手探りで作り方を探ってみた結果、いくつか成果が得られたのでここに公開します。

Stand Aloneスクリプト編(1) HelloWorldを表示してみる


一番単純に、とりあえずHello World的なものを動かしてみるだけの手順です。

※後日(2016.11.9)注:執筆時筆者が「Scripts」タブはデフォルトで表示されていると思い込んでいたので、「Scripts」タブをクリックするところから解説を始めていたのですが、改めて確認したところ、デフォルトの状態のZAP2.5.0だと「Scripts」タブが表示されません。
下図のようにサイトツリーの上部に「Scripts」タブが表示されていない場合は、ZAPの[ヘルプ]-[アップデートのチェック]-[マーケットプレイス]で「Script Console」というアドオン(2016.11月現在 Betaレベル)をインストールしてください。
また、本記事のシリーズで「Zest」という言語を利用するので、同様に「Zest - Graphical Security Scripting Language」アドオン(2016.11月現在 Betaレベル)もインストールしておいてください。
(ミスすみません)


1) ZAPを起動し、「Scripts」タブをクリック。


2) Stand Alone を右クリック - 「新規スクリプト…」をクリック。


3) 新規スクリプトのダイアログで以下のように入力し「保存」をクリック。

Script名:script_test1
タイプ:Stand Alone
Script engine:ECMAScript:Oracle Nashorn
テンプレート:Standalone default template.js
説明:任意の内容
開始時にロード:チェック




4)「Stand Alone」の下に「script_test1」が作成されます。
「script_test1」を選択し、右上のペイン(「Scriptコンソール」タブ)に

print('hello zap script');

を書き入れ、左上のフロッピーディスクマークを押下し、スクリプトを保存します。



5) 「Scriptコンソール」の上下ペインの間の枠にあるほうきマークをクリックすると、下のペインがクリアされ白紙状態になります。
その状態で上のペインの「実行」をクリックすると、下のペインに「hello zap script」という文字列が現れます。


第一歩目としてStand Aloneスクリプトでの「Hello World」に成功しました。

次は、ZAPのサンプルスクリプトを動作させてみます。

次へ

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

[OWASP ZAP2.4.3にあったバグ4] 診断対象URLにスコープ外へのリダイレクトがある場合、ZAPがスコープ外へアクセスしてしまう場合がある(2016/6/20改題)


本家GitHubのIssuesに(多少マシになったような気がする英語(当社比)で)報告した
https://github.com/zaproxy/zaproxy/issues/2546
の解説です。

これはZAP2.5.0リリース後に報告したので、2016/6/20現在、この問題はZAP2.5.0にも存在しています。

※本記事の修正前のバージョンでは、
・http://localhost/zaptest/redirect.php に対する動的スキャンではスコープ外のリダイレクトが起こらない
・http://localhost/zaptest/redirect.php?a=b の動的スキャンの時にスコープ外への不正なリダイレクトが起こる場合がある
という内容を書いていました。

が、前者のパラメーターなしのURLに対する動的スキャンであってもリダイレクトが発生するケースがあることを発見したので(ディレクトリブラウジングの検査の場合)「スコープ外へのリダイレクトがある場合、リダイレクト先にアクセスしてしまうことがある」という形に改めてまとめ直すことにしました。

また、修正前のバージョンではリダイレクトしない正常なケースでの例示が、URLパスが改変された診断項目のものでリダイレクトが起こり得ないログを例示しており例としては不適切なものでした。ミスをお詫びいたします。(今回の修正版では記事が長くなってしまうため正常なケースの例示を削除しました)


再現手順:
1) ZAP 2.5.0、もしくはZAP 2.4.3を起動します。

2) FiddlerもしくはBurp Suiteを起動します。

3) ZAPとFiddlerもしくはBurp Suiteのネットワーク設定を行い、以下のように多段プロキシの状態にします。

[ブラウザ] - [ZAP] - [Fiddler または Burp Suite] - [診断対象のサイト]


4) 以下のphpファイルを作成し、自分の管理下のウェブサーバーに置きます。

[redirect.php]
<?php
header('Location: https://www.*****.com/');
exit;
?>

※このphp内に書いてある「https://www.*****.com/」は、自分の管理下にある(=ZAPで診断しても問題のない)URLを指定してください。

本例では、このファイルが http://localhost/zaptest/redirect.php というURLに設置されていて、
リダイレクト先は
header('Location: http://127.0.0.1/redirecttest/');
が設定されているものとして進めます。
(リダイレクト先の実体が外部サーバーでないので例として微妙ですが、「スコープ外」という判定になるはずのサーバということで…。ここに外部サーバーを指定しても同じ動きをします)


5) スコープの外部へのリダイレクトが起こってしまう設定例を以下に示します。

5-1) プロキシとしてZAPを設定したブラウザで、URLパラメーターをつけて http://localhost/zaptest/redirect.php?a=b にアクセスします。

5-2) ZAPを「プロテクトモード」にし、 'http://localhost/' をデフォルトコンテキストに入れます。

5-3) ZAPの履歴から http://localhost/zaptest/redirect.php へのアクセスを右クリックし、「入力ベクトル」タブの「Injectable Targets」で全てにチェックを入れます。


「ポリシー」タブの全てのカテゴリのthresholdをいったん全てオフにし、それから「インジェクション」カテゴリの'Server Side Include'のthresholdを「既定」に戻します(Server Side Includeのスキャンのみが実行される設定にします)。


いったん全てオフにした後に、


Server Side Includeだけを「規定」に戻します。

それからhttp://localhost/zaptest/redirect.phpの履歴へ動的スキャンを実施し、ZAPではなくFiddlerもしくはBurpのログを確認します。(他のプロキシを使う理由は、ZAPの実際の動作を確認するためです)

ZAPではこの場合リダイレクトのログが出ないため、ZAPではなくFiddlerもしくはBurpのログを確認します。

ZAPの動的スキャンの履歴では、以下のようにリダイレクト先にアクセスしている記録は出ないのですが、


FiddlerもしくはBurpのログを見ると、ZAPが http://127.0.0.1/redirecttest/ にアクセスしてしまっています。(例はFiddlerの画面です)


これが仕様なのかバグなのか不明だったのでIssueで質問したところ、まずそうなのでASAPで調べるという回答があり、その後比較的迅速に対応されつつあるようなので、まずいバグだったようです。

この問題の現在のステータスは、https://github.com/zaproxy/zaproxy/issues/2546 を見ると、修正が入り、クローズされていますが、修正版がまだリリースされていない状態です。(2016/6/20現在)


上記は問題が発生する一例で、リダイレクトがあるページにURLパラメーターが付いている場合だと、'Server Side Include'を含め多くのスキャンルールでリダイレクト先へのアクセスが発生してしまうようです。

また、いろいろ設定を変えて調べてみた結果、リダイレクトがあるページにURLパラメーターを付けないでアクセスした場合はZAPは大半のケースでリダイレクトせずに踏みとどまるのですが、'ディレクトリブラウジング'の検査の場合にはリダイレクト先へのアクセスが発生してしまうようです。

[この問題の回避策]
スコープ外へのアクセスを避けるため、回避策を調べてみたところ、

・ZAPの「セッションのプロパティ」-「コンテキストから除外」→効果なし(リダイレクト先へのアクセス発生)
・ZAPの履歴右クリック-「以下の処理から除外」-「プロキシ」→効果なし(リダイレクト先へのアクセス発生)

と、ZAPだけだとリダイレクト先にアクセスしないように設定することができないようなので、ZAPの先に多段でFiddlerもしくはBurpを繋げて、Fiddlerのオートレスポンダーで404で返すようにするか、Burpのスコープ外へのアクセスをドロップする設定で回避する手段しかないようです。

FiddlerのAutoResponderの設定方法:

(1) URLを選択
(2) AutoResponderタブを選択
(3) AddRuleボタンを押下
(4) If Request Matches... のところに(1)のURLが出てくる
(5) Rule Editorの欄に「EXACT:http://127.0.0.1/redirecttest/」というURLが入力される
(6) プルダウンより「404_Plain.dat」を選択し「Save」
(7) 「Enable rules」「Unmatched requests paththrough」にチェック

これで、当該URLにアクセスしようとすると404が返ることになります。

※この手順で指定しているAutoResponderの

EXTRACT:http://~

という指定は完全一致の指定なので、リダイレクト先URLが変化する場合は、regex:を使って正規表現でマッチさせる必要があります。正規表現による指定の方法は以下のページなどを参照ください。
http://docs.telerik.com/fiddler/KnowledgeBase/AutoResponder

Burp Suiteでのスコープ外へのアクセスをドロップする設定方法:
(1)「Target」-「Site map」からドメインを右クリックして「Add to scope」を選択


(2)「Options」-「Connections」の「Out-of-Scope Requests」の設定項目の「Drop all out-of-scope requests」のチェックを入れます。

これで、http://localhost/以外へのリクエストが発行されてもBurp Suiteのところでドロップされるようになります。

[OWASP ZAP2.4.3にあったバグ3]
出力にXSSがあっても、<html>~</html>の内部でないと自動診断で検出してくれない


本家GitHubのIssuesに(色々ダメな感じで)報告した
https://github.com/zaproxy/zaproxy/issues/2279
の解説です。

再現手順:
1) 以下のPOSTテスト用phpファイルを作成し、自己管理課のWEBサーバーに設置します。

[test.php]
<?php print_r($_GET); ?>


2) ブラウザのプロキシとしてZAPを設定し、1)で作成したファイルにURLパラメーター付きでアクセスします。

http://localhost/zaptest/test.php?param=123


URLパラメーターがページ内にダンプされる作りなので、例えば以下のURLにアクセスすれば、ブラウザ上でダイアログが立ち上がります(XSSフィルタが無効になっているブラウザで確認する必要があります)。

例)XSSサンプルURL:

http://localhost/zaptest/test.php?param=<script>alert(1)</script>


例)レスポンス:

HTTP/1.1 200 OK
Date: Sun, 12 Jun 2016 14:12:55 GMT
Server: Apache/2.4.12 (Win32) OpenSSL/1.0.1l PHP/5.6.8
X-Powered-By: PHP/5.6.8
Content-Length: 51
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8

Array
(
[param] => <script>alert(1)</script>
)


例)ブラウザ(Firefox)でダイアログが表示:


このように、ブラウザ上でscriptタグが動作します。
このような単純なXSSは、ZAPによる自動診断で検出されるはずです。

3) 2)でZAPに記録された履歴に対し、右クリック-動的スキャンの設定で「URL Query String」に対し「クロスサイト・スクリプティング(反射型)」のみの動的スキャンをかけます。

今回のテスト用のphpには、上記で確認したような非常に単純なXSSがあるのですが、ZAPはこの場合XSSを検出しません。



なぜこれが検出されないのだろう? と思い、本家GitHubのIssueに報告を上げたところ、psiinon氏から以下のような回答が返ってきました。

https://github.com/zaproxy/zaproxy/issues/2279#issuecomment-190119827

要は、反射型、持続型のXSSルールはユーザー入力がHTMLの中に反映されることを分析するものなので、HTMLタグ内でないと検出されない、ということのようです。
また、「DOM XSS ruleだったら検出できると思う」という追加情報もいただきました。

それでpsiinon氏の言うようにユーザー入力が反映される個所をHTMLタグの中にしてみたところ、反射型XSSルールで検出されるようになりました。

[test.php]
<html><body><?php print_r($_GET);?></body></html>


また、当初の、HTMLタグの中にない場所でのXSSは、これもpsiinon氏の言うように、ZAPの「DOM XSS Active Scanner rule」で検出されることが確認できました。(「DOM XSS Active Scanner rule」は本記事執筆の2016/6/12時点でAlphaレベルのアドオンとなっています。)

ただ、これだと、現実問題としてスクリプトが動作するのに、<html>~</html>の中でないからということで、ZAPの標準のスキャンルールでは検出できないということになるので、診断漏れの危険があります。

これについては、開発メンバー間で多少の議論があったのち、Issueのタイトルが「- False Negative」が付いたものに変更されたりしたので、この場合の検出ルールをどうするか、今後検討されるのではないかと思われます。

本現象のZAP2.5.0での検証結果:

本現象はZAP2.5.0でも発生します。


続きます。

[OWASP ZAP2.4.3にあったバグ2]
POSTパラメーターのキーにブラケット([])がついているものが複数あると、自動診断時に勝手に一つにまとめられてしまう


本家GitHubのIssuesに(ひどい英語で)報告した
https://github.com/zaproxy/zaproxy/issues/2153
の解説です。

再現手順:
1) 以下のPOSTテスト用htmlファイルを作成し、ブラウザからZAP2.4.3を通してPOSTを行います(POST先は自己管理下のWEBサーバーであればどこでも良いので環境に合わせて書き換えてください)。

<html>
<body>
<form method="post" action="http://localhost/zaptest/test.php">
<input type="text" name="param[]" value="value1"><br>
<input type="text" name="param[]" value="value2"><br>
<input type="text" name="param[]" value="value3"><br>
<input type="text" name="param[]" value="value4"><br>
<input type="text" name="param[]" value="value5"><br>
<input type="submit" value="submit"><br>
</form>
</body></html>


2) ZAP 2.4.3の履歴に以下のようなリクエストが表示されます。

POST http://localhost/zaptest/test.php HTTP/1.1

param%5B%5D=value1&param%5B%5D=value2&param%5B%5D=value3&
param%5B%5D=value4&param%5B%5D=value5


このURLへのアクセスのZAP上の履歴を右クリックし「動的スキャン...」-「入力ベクトル」タブの 「Injectable Targets:」で「POST Data」を選択、「ポリシー」タブで、「インジェクション」以外のThresholdを「オフ」にし、「スキャンを開始」ボタンを押下します。

3) ZAP2.4.3上で動的スキャンが開始されるので、動的スキャンの履歴を選択して「リクエスト」タブの内容を見てみると、以下のように、POSTデータがparam[]=xxxxx(診断用文字列)1つだけになってしまっています。

オリジナルのリクエスト:


「動的スキャン」時のリクエスト:


これはおそらく、ZAP2.4.3がPOSTパラメーターのキーにあるブラケット([])を配列を表す記号として解釈しておらず、「同じ値が5つ並んでいる」と判断したせいではないかと思われたので、試しにvalue5のキーだけ「name<>」に変えてみたところ、やはり動的スキャン時にvalue5のkey=valueだけ別の診断対象パラメーターとして扱われました。

ZAPのこの挙動は、POST値を配列で受け取り、その配列内の順番で値の意味付けを変えるようなWEBアプリケーションを診断する場合や、配列でx個の値がPOSTされてくることを前提として作られているようなWEBアプリケーションの診断の場合に、問題を発生させる恐れがあります。


本現象のZAP2.5.0での検証結果:

ZAP2.5.0で、上記と同じ手順で2)のURLに対して動的スキャンをかけると、ちゃんとPOSTパラメーターが5つあるという扱いになるので、本問題はZAP2.5.0で修正されています。





[残っている問題]

ZAP2.5.0では、POST値が配列となっている場合でも、動的スキャンは各key=valueごとに実施されることが分かりましたが、「診断結果が一つにまとめられる」という問題がまだ残っているようです。

ZAP only report the first XSS in array value #2496
https://github.com/zaproxy/zaproxy/issues/2496

で報告した問題ですが、これはZAP2.5.0でも同じ挙動になります。

再現の手順としては、

2-1) 上記[バグ2]の手順1)のPOST先のphpファイルを、以下のようにPOSTされた値を全てそのままエコーバックするような作りにします。

[test.php]
<html>
<body>
<?php print_r($_POST); ?>
</body>
</html>


2-2) このtest.phpに対し、上記[バグ2]の手順1)のhtmlからPOSTを行い、「クロスサイト・スクリプティング(反射型)」の診断を行ってみると、name[]に対するvalue1~5のそれぞれの箇所でXSSが検出されるはずなのに、検出されるXSSは一件のみで、「アラート」タブに出てくるXSSはvalue1の箇所のXSSのみとなっています。

これでは、value1~5の意味が違う場合などに、value1でXSSが検出された、という診断結果しか出てこないので、XSS脆弱性がある箇所を見誤る危険性があります。

これについては現状、ZAP2.5.0の修正待ちです。(ただし本家GitHubのIssuesに投稿後23日も経っていますが、コメントはおろか「Bug」タグも付かず何の処理もされていません)


続きます。

ZAP2.4.3にいくつかバグを見つけて本家GithubのIssuesに報告していたのですが、それが先日リリースされたZAP 2.5.0でどこまで直っているかを確認してみます。

[OWASP ZAP2.4.3にあったバグ1]
診断対象のURLのURLパラメーターのキーにブラケット([])等が付いていると自動診断が失敗する


再現手順:
1) 以下のテスト用phpファイルを作成し、自分の管理下にあるウェブサーバーに設置します。

[test.php]
<html>
<body>
<?php print_r($_GET); ?>
</body>
</html>


本再現手順では、例として http://localhost/zaptest/test.php に設置することにします。

2) ブラウザのプロキシ設定にOWASP ZAPを設定し、以下のURLにアクセスして、ZAPの履歴にURLが記録されることを確認します。

http://localhost/zaptest/test.php?a=b&c=d&e=f&g[]=h




3) 上記2)のアクセスのZAP上の履歴を右クリックし「動的スキャン...」-「入力ベクトル」タブの 「Injectable Targets:」で「URL Query String」を選択、「ポリシー」タブで、「インジェクション」以外のThresholdを「オフ」にし、「スキャンを開始」ボタンを押下します。



4) スキャンが開始されると、ZAPの「動的スキャン」タブがアクティブになり、ZAPがアクセスしたURLが「動的スキャン」タブ内の履歴のリストにどんどん溜まっていくので、一見、普段通りZAPが自動診断を行っているように見えます。



が、よく見ると履歴のリストに出てくるURLのGETパラメーターが変動していません。
診断が行われているなら色々とGETパラメーターが変化するはずなのに、パラメーターが全く変化せずにただひたすら同じURLへのアクセスを繰り返しています。

5) 比較として、2)のURLからブラケット([])を外したURLにアクセスし、そのURLに対して診断を行ってみます。

診断対象URL(改):

http://localhost/zaptest/test.php?a=b&c=d&e=f&g=h


診断ログ:


今度はちゃんとURLパラメーターが変化しており、診断がちゃんと行われていることが分かります。


このバグに気づいて本家GitHubのIssuesに投稿したところ、既知の問題だった模様で、#1848 のIssue とduplicate ということでそちらのIssueにマージされました。

(ひどい英語で)報告したIssueのURL:
https://github.com/zaproxy/zaproxy/issues/2152

Issue報告した後、しばらくZAPの次期バージョンが出るのを待っていたのですが、なかなか出ないので、とりあえず問題を周知させようとOWASP ZAP Japan Groupに投稿したりもしました。
https://groups.google.com/forum/#!topic/owasp-zaproxy-japan/dMWp_HKShbU


本現象のZAP2.5.0での検証結果:

ZAP2.5.0で、上記と同じ手順で2)のURLに対して動的スキャンをかけると、ちゃんと各パラメーターに対し診断が行われていることが確認できます。本問題はZAP2.5.0で修正されています。





[2016/6/10追記]

本問題に関連して、ブラケット以外の記号でも同様の問題が起こるケースがあることを発見し、本家GitHubのIssuesに関連情報として報告していましたが、私個人としてもその後業務でURLパラメーターのキーのほうにブラケット以外の記号がつくというケースに出会わなかったこともあり、本家でも重要な扱いを受けずに放置後クローズされたので、この件はあまり重要な問題でないという印象になっていました。

ただ、ケースとしてあまりないとはいえ、URLパラメーター側のキーに何かしらの記号が付くこともありうるので、今回洗い出してみたところ、ブラケットを含む13種類の文字で、今回のようなバグが発動するようです。

[調査対象文字]
ASCII文字のprintableな文字から英数を除いた全記号

[確認した手順]
http://localhost/zaptest/test.php?a=b&c=d&e[文字]=f

この[文字]の位置に記号を入れてテスト環境にアクセスしてZAPで上記の再現手順と同じ設定にして動的スキャンをかけてみました。

その結果、以下の記号が今回のバグを発生させるようでした。

記号ZAP2.4.3ZAP2.5.0
(半角スペース)NGOK
"NGOK
+NGOK
<NGOK
>NGOK
[NGOK
\NGOK
]NGOK
^NGOK
`NGOK
{NGOK
|NGOK
}NGOK

(※2016.6.14 記号が2つ欠けていたので追加しました)

また、上の表にあるように、ZAP2.5.0で動作に異常がないか確認したところ、すべての記号で問題なく動的スキャンが正常動作しました。
(念のためZAP2.5.0でもASCII文字のprintableな文字から英数を除いた全記号でテストしてみましたが、すべての記号で正常動作したので、このバグに関してはZAP2.5.0で完全に直っているようです)


※書いてみたら長くなったので以後続きます。

今後ZAP2.5.0で直っているか確認する予定のバグ:

バグ2: POSTパラメーターのキーにブラケット([])がついているものが複数あると、自動診断時に勝手に一つにまとめられてしまう
https://github.com/zaproxy/zaproxy/issues/2153

バグ3: 出力にXSSがあっても、<html>~</html>の内部でないと自動診断で検出してくれない
https://github.com/zaproxy/zaproxy/issues/2279

バグ4: ZAP2.5.0リリース後に報告したので2.5.0に存在。診断対象URLにスコープ外へのリダイレクトがある場合、ZAPがスコープ外へアクセスしてしまう場合がある
https://github.com/zaproxy/zaproxy/issues/2546

(2016.6.13 当初「バグ4」として書くつもりだった項目はバグ2の補足情報として書いたのでこのリストから消しました。)

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