前の投稿 次の投稿

OWASP ZAPのスクリプトを作ってみる part3(修正追記版)

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 サンプルコードの整形、本文の加筆修正(主に言い足りなかった点の加筆)を行いました。

次へ

Leave a Reply

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