前回の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チェックも突破でき、サンプルサイトの登録完了画面まで無事遷移できます。




続きます。

次へ

今回からは「Zest」編です。

OWASP ZAPを利用しているとZAPのあちこちのメニューに「Zest」云々というのがあるので、OWASP ZAPでスクリプト処理を行う際に、使いやすい言語は「Zest」なのかな? という印象が何となくあります。

しかし、実際にZAPでスクリプトでの処理をやろうとして「Zest」を調べてみると、サンプルコードを見ても通常の手続き型言語からかなりかけ離れたコードになっていて敷居が高く、どうやって作ったらいいのかよく分からず、ECMAScript(やrubyやpython)で処理書けそうだからいいか、となってしまって手を付けずに諦めてしまう人が多いのではないでしょうか。

ただ、調べてみると「Zest」はOWASP ZAPが暗に推すだけあって、ECMAScript等にはない便利さがあり、非常に使える言語でした。

今回も調査は手探りなので、何か漏れやら非効率等おかしなところもあるかもしれませんが、調べた結果を公開してみます。

Zest編(1)モチベーションを上げる


Zestはかなり敷居が高いので、最初の段階からモチベーションを上げて取り掛からないと挫折率が高そうなので、モチベを上げる情報を最初に書いて、それから各種説明に入りたいと思います。

Zestを触ってみた印象では「ZAPをスクリプトで操作したい」という人が求めてるものはZestにあるのではないか、という感じがしました。

ZAPでZestを使うと

・複数画面遷移
・(CSRFチェックの入っている画面の)複数画面遷移
・複数画面遷をした後のページの自動スキャン(※)
・複数画面遷移を行いながらのファジングおよび判定ロジックの実装


こんな処理が結構簡単に設定できます。

やってみたくなってきましたでしょうか。

※ただし、後述しますが、Zestでできるのは「(複数)画面遷移を行った後でその画面にとどまっての動的スキャン」であり、「動的スキャンの1診断項目ごとに複数画面遷移を行ってPOST/GETを繰り返すような処理」はできないようなので、あまり期待はしすぎないようお願いします。

Zest編(2)サンプルサイトの作成


CSRFチェックの入った画面遷移を行うサイトのサンプルを以下に置きます。あまり良いコードではないかもしれませんが、これをサンプルサイトとして使います。

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

このzipをダウンロードするとphpファイルが4つ解凍されますので、それを自己管理下のphpが動作するWEBサーバーに置きます。

本例では以下、このサンプルサイトの4ファイルを http://localhost/zaptest/csrftest/ 下に配置したことにします。

Zest編(3)サンプルコードの作成


OWASP ZAPを立ち上げ、画面左上のScriptsタブ - スクリプト - Stand Aloneを右クリックし、「新規スクリプト」を選択します。
すると「新規スクリプト」設定用のウィンドウが立ち上がるので、

Script名: script_test7
タイプ:Stand Alone
Script engine:Zest: Mozilla Zest
テンプレート:Standalone default template.js
開始時にロード:オン

という設定で「保存」を押下します。



すると保存後、「Edit Zest Script」という別のウィンドウが立ち上がるのですが、これはひとまず値は変えずにそのまま「保存」を選びます。

それからZAPをプロキシとしたブラウザでさきほど配布したサンプルサイトにアクセスします。

サンプルサイトは

http://localhost/zaptest/csrftest/index.php

http://localhost/zaptest/csrftest/confirm.php

http://localhost/zaptest/csrftest/complete.php


という遷移になっていますので、この順にブラウザでアクセスし、ZAPの履歴にこれらのURLを記録します。

ZAPの履歴上で、

http://localhost/zaptest/csrftest/index.php

への履歴を右クリック - 「Zest Scriptへ追加」 - 「script_test7」を選択。すると「Scripts」タブに切り替わり、「script_test7」の下に「→GET http://localhost/zaptest/csrftest/index.php」というノードが追加されます。



「→GET http://localhost/zaptest/csrftest/index.php」の先頭に三角形のマークがあるので、クリックしてみると、

「Assert - Status Code(200)」
「Assert - Length(response body = [数値] +/- 1%)」

という二つの子ノードが表示されると思います。

これはZest ScriptにGETやPOSTのリクエストを挿入するとデフォルトで付くアサーションチェックの項目です。Zestスクリプトの実行時に、この条件に反する状態が発生しているとエラーとなります。



このアサーション項目の意味は、

「HTTPステータスコードが200」
「response bodyのlength(Content-Lengthとイコール)が[数値]のプラスマイナス1%の範囲」

ということです。
レスポンスのHTTPステータスコードが200以外だったり、レスポンスのbodyのlengthがZest登録時と大幅に違う場合は異常が発生したと判定しNGとなります。

試しに、ページをリネームなどで404になるようにして一度Zestを実行してみましょう。
Stand Aloneスクリプトの実行は、ZAPの画面の右上、Scriptコンソールタブの下に「実行」ボタンがあるのでそれを押下するだけです。



アサーションチェックの結果が表示されます。ステータスコードが404で200以外なのでバツマークとなります。



このアサーション項目はノード右クリックのメニューで削除することもできますし、ダブルクリックして内容を編集することも可能です。

※ここで一つ注意点があるのですが、このresponse bodyのlengthに関するアサーションにバグがあり、マルチバイト(日本語など)の文字列がページ内にあって、Content-Typeヘッダーに「charset=UTF-8」などの文字コードの指定があると、サイズの算出がおかしくなり、アサーション結果がNGになってしまいます。

このブログを書いているときにこのバグを見つけ、開発元のGitHubにIssueを投げましたので、そのうち解消されると思われますが、現状だと日本語を含むページを扱う際にはこの「Assert - Length(response body = [数値] +/- 1%)」のアサーションは削除しておく必要があります。(本項のサンプルサイトは日本語を使っていないのでアサーションの削除は不要です)


アサーションの動作確認が完了したら、次に残りのURL、

http://localhost/zaptest/csrftest/confirm.php
http://localhost/zaptest/csrftest/complete.php


の履歴も同様に、履歴上で右クリック - 「Zest Scriptへ追加」 - 「script_test7」を行います。
「script_test7」の下にPOSTのノードが二つぶら下がります。



この状態で、Zest画面上部の「実行」を押下すると「index.php」-「confirm.php」-「complete.php」へのリクエストが順に発生し、ZAP画面下部の「Zest Results」タブにアサーションの結果が表示されます。(アサーション結果がNGでも処理は止まらず、最後まで実行されます)



ここで、confirm.phpとcomplete.phpのアサーションがFAILEDになっているのは、CSRF対策用トークンがエラーになっていてエラー画面が表示されているため、正常系の画面とは別の画面が表示されていてresponse bodyのlengthが大幅に変わっているからです。

ここで単純な「複数画面遷移」には成功しましたが、CSRFチェックが入っている画面遷移には対応できていないので、次回はCSRFチェックが入っている複数画面遷移を成功させてみます。

次へ

Active Rulesスクリプト編(3) scan関数を使ったスクリプト


それでは前回からの続きで、scan関数を使ったサンプルスクリプトを作成します。

scan関数は、part4で確認した通り

・scan()関数は、「入力ベクトル」で指定された攻撃ベクター(URL Query String / POST Data 等)に合致する項目ごとにコールされる
(コールされる際、引数にパラメーターのkeyとvalueがそれぞれ入る)

なので、「パラメーターを修正して投げる」ような処理に向いています。
今回は、GETの各パラメーターのキーにブラケット([])を付けてリクエストを行うサンプルを書いてみます。

スクリプト中のコメントにも書いてありますが、GETパラメーターのキーにブラケットを付けて、どんなレスポンスが戻ってくれば異常といえるのかは定義ができず、自動的な判定が難しいため、ひとまずステータスコードが200以外だったらアラート、という処理にはしてありますが、このスクリプトの場合人間の目による結果の監視が必要と思います。
// scannode関数
function scanNode(as, msg) {
    //nop 何もしない
}

// scan関数
function scan(as, msg, param, value) {

    // 停止ボタンが押されていたら止める
    if (as.isStop()) {
        return
    }

    // デバッグログ
    print('scan called for url=' + msg.getRequestHeader().getURI().toString() + 
        ' param=' + param + ' value=' + value);
    
    // リクエストをコピー
    var clmsg = msg.cloneRequest();
    
    // setParam (message, parameterName, newValue)
    as.setParam(clmsg, param+'[]', value);
    
    // sendAndReceive(msg, followRedirect, handleAntiCSRFtoken)
    as.sendAndReceive(clmsg, true, false);
    
    // レスポンスのステータスコードを取得
    statusCode=clmsg.getResponseHeader().getStatusCode();

    // ステータスコードが200以外だったら異常が発生したとみなす。
    // ただし、キーにブラケット([])を追加することによってどんなレスポンスが戻ってくれば
    // 異常と言えるか不明なので正確な自動判定は難しい。人間によるログ確認が必要。
    if(statusCode!=200){

        // raiseAlert(risk, int confidence, String name, String description, 
        //   String uri, String param, String attack, String otherInfo, 
        //   String solution, String evidence,int cweId, int wascId,
        //   HttpMessage msg)
        // risk: 0: info, 1: low, 2: medium, 3: high
        // confidence: 0: falsePositive, 1: low, 2: medium, 3: high, 4: confirmed

        as.raiseAlert(1, 2, '*** parameter bracket error ***', 
         '*** parameter bracket error ***',
         clmsg.getRequestHeader().getURI().toString(), 
         param, '(bracket)', '(bracket)', '(bracket)',
         param+'[]', 0, 0, clmsg);
    }
}

このスクリプトをpart4でやったように、ActiveRulesスクリプトとして保存・有効化し、例えば

http://localhost/zaptest/dir1/dir2/dir3/test.php?aaa=bbb&ccc=ddd&eee=fff

にアクセスして、それから手っ取り早くステータスコードを200以外にするためにWEBサーバーを停止してから、対象URLに対して「Script Active Scan Rules」のみ有効の動的スキャンをかけると、以下のようなアラートが「アラート」タブに記録されます。



また、ZAPがどのようなリクエストを行っているか、ZAPの先にFiddlerなどのもう一つのプロキシツールをかませて通信を記録すると、ちゃんと各パラメーターのキーにブラケットが付いていることが確認できます。(ZAP2.5.0の場合。2.4.3の場合、おそらくは例のバグの関連でキーにブラケットが付かないリクエストが飛び、意図通り動作しません)

実行時にZAPが発行したリクエスト(ZAPの先にFiddlerを接続して確認):

http://localhost/zaptest/dir1/dir2/dir3/test.php?aaa%5B%5D=bbb&ccc=ddd&eee=fff
http://localhost/zaptest/dir1/dir2/dir3/test.php?aaa=bbb&ccc%5B%5D=ddd&eee=fff
http://localhost/zaptest/dir1/dir2/dir3/test.php?aaa=bbb&ccc=ddd&eee%5B%5D=fff


Active Rulesスクリプト編はこれで終わりです。

次へ

Active Rulesスクリプト編(2) スクリプト作成時のテクニック・注意点


・「Script Active Scan Rules」のみを実行するスキャンポリシーの作成

part4で解説した通り、Active Rulesスクリプトは「動的スキャン(Active Scan)」の実行時に、スキャンポリシーの「一般」カテゴリの「Script Active Scan Rules」としてスクリプトが実行されます。

Active Rulesスクリプトを何度も実行してデバッグするようなときは、「Script Active Scan Rules」のみを実行するスキャンポリシーを作ってしまうと楽です。

作り方は、

1) ZAPのメニューの「ポリシー」-「スキャンポリシー」を選択。
2) 表示された「Scan Policy Manager」ウィンドウで「追加」を選択。
3) 表示された「スキャンポリシー」ウィンドウで、
・ポリシー名:任意(「script_only」など)
・Thresholdを、「一般」カテゴリの「Script Active Scan Rules」のみ「既定」、他は全てオフ
にして「OK」を押下

で「Script Active Scan Rules」のみを実行するスキャンポリシーが作成されます。

後は、動的スキャン実施時に表示されるダイアログで、「ポリシー」としてここで設定したポリシー名を指定すれば「Script Active Scan Rules」のみを手軽に実行させることができます。

注意点:ActiveRulesのスクリプト実行時printの結果が二重に入り乱れる場合

あと、気づきにくい注意点として、ActiveRulesのスクリプトを実行する際、ZAPのデフォルトの設定だと、「ツール」-「オプション」-「動的スキャンの設定」で「並列スキャンスレッド数:」の目盛りが「2」になっていますが、そうすると、作成したActiveRulesスクリプトも並列で2つ同時に実行されてしまうため、スクリプトの書き方によってはprintが二重に入り乱れて表示されてしまい混乱することがあります。

part4で作成したテンプレートそのままのサンプルでは大丈夫なようですが、スクリプトテスト時に妙な現象でハマらないために、ActiveRulesスクリプトの動作確認時には「並列スキャンスレッド数:」は「1」にしておくことをお勧めします。



Active Rulesスクリプト編(2) ディレクトリ一覧を表示するスクリプトの作成


ではActive Rulesスクリプトの関数の呼ばれ方が分かったので、関数の中身を作っていってみます。

まずは、scanNode関数がノードごとに呼ばれることを利用して、URLがディレクトリであるかどうか判定してprintするような簡単な処理を書いてみます。

・ZAPの履歴からディレクトリ一覧を取得するスクリプト(ActiveRules版)
// scanNode関数
function scanNode(as, msg) {
    // URI文字列
    var uristr = msg.getRequestHeader().getURI().toString();

    // ディレクトリであれば
    if(isDir(msg)){
        // URI文字列の末尾が'/'でなければ'/'を付ける
        uristr = addSlashToURIString(uristr);

        // ディレクトリ表示
        print("dir: "+uristr);
    }
}

// scan関数
function scan(as, msg, param, value) {
    //nop
}

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

// 現在のパスがディレクトリかファイルかを判定しtrue/falseを返す
function isDir(msg) {
    path = msg.getRequestHeader().getURI().getPath();

    var is_dir;

    if(path!=null){
        var patharr = path.split("/"); 
        var chkfilename = patharr[patharr.length-1];
        if (chkfilename.indexOf('.') > -1) {
            is_dir = false;
        } else {
            is_dir = true;
        }
    } else {
        // path==null:rootはディレクトリと判定
        is_dir = true;
    }
    return is_dir;
}
このスクリプトをpart4でやったように、ActiveRulesスクリプトとして保存・有効化し、例えば

http://localhost/zaptest/dir1/dir2/dir3/test.php?aaa=bbb

にアクセスしてから、http://localhost/ に対して「Script Active Scan Rules」のみ有効の動的スキャンをかけると

dir: http://localhost/
dir: http://localhost/zaptest/
dir: http://localhost/zaptest/dir1/
dir: http://localhost/zaptest/dir1/dir2/
dir: http://localhost/zaptest/dir1/dir2/dir3/

のような表示が得られます。

Active Rulesスクリプト編(3) ディレクトリに対してOPTIONSを実行する


上のスクリプトに少し手を加えれば、例えば各ディレクトリにOPTIONSを発行するスクリプトが作成できます。

各ディレクトリに対して一括でOPTIONSを発行し、サポートしているメソッドの一覧が参照できれば、効率よく情報収集ができますので、サンプルとして作成してみます。

また、Active Rulesスクリプトの場合、アラートを上げることが可能なので、実装のサンプルとしてOPTIONSが有効だったらInfomationレベルのアラートを上げるようにしてみます。

・ZAPの履歴からディレクトリ一覧を取得し、OPTIONSメソッドを実行するスクリプト(ActiveRules版)
// scanNode関数
function scanNode(as, msg) {
    // URI文字列
    var uristr = msg.getRequestHeader().getURI().toString();

    // ディレクトリであれば
    if(isDir(msg)){

        // リクエストをコピー
        var clmsg = msg.cloneRequest();

        // メソッドをOPTIONSにする
        clmsg.getRequestHeader().setMethod("OPTIONS") 

        // sendAndReceive(msg, followRedirect, handleAntiCSRFtoken)
        // 末尾が「/」でないURLの場合、いったん末尾が「/」のURLにリダイレクトされる
        // 場合があるのでfollowRedirect==true
        as.sendAndReceive(clmsg, true, false);

        // リクエスト送信・レスポンス受信
        rsp = clmsg.getResponseHeader().toString();

        print("-------------------------------------------"
             +"-------------------------------------------");

         // レスポンスヘッダ内に"Allow:" ヘッダがあれば
        if (rsp.indexOf("Allow:")>-1) {

            // URI出力
            print("[URI] "+uristr);

            // "Allow:"ヘッダ の内容を出力
            print("[OPTIONS] " + clmsg.getResponseHeader().getHeader("Allow"));

            as.raiseAlert(0, 2, '*** OPTIONS Method ***', 
             '*** OPTIONS Method ***', clmsg.getRequestHeader().getURI().toString(), 
             '(options)', '(options)', '(options)', '(options)',
             "[OPTIONS] " + clmsg.getResponseHeader().getHeader("Allow"), 0, 0, msg);

        } else {
            print("-");
        }

        // Check if the scan was stopped before performing lengthy tasks
        if (as.isStop()) {
            return
        }
    }
}

// scan関数
function scan(as, msg, param, value) {
    //nop 何もしない
}

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

// 現在のパスがディレクトリかファイルかを判定しtrue/falseを返す
function isDir(msg) {
    path = msg.getRequestHeader().getURI().getPath(); 

    var is_dir;

    if(path!=null){
        var patharr = path.split("/"); 
        var chkfilename = patharr[patharr.length-1];
        if (chkfilename.indexOf('.') > -1) {
            is_dir = false;
        } else {
            is_dir = true;
        }
    } else {
        // path==null:rootはディレクトリと判定
        is_dir = true;
    }
    return is_dir;
}
このスクリプトをpart4でやったように、ActiveRulesスクリプトとして保存・有効化し、例えば

http://localhost/zaptest/dir1/dir2/dir3/test.php?aaa=bbb

にアクセスしてから、http://localhost/ に対して「Script Active Scan Rules」のみ有効の動的スキャンをかけると

--------------------------------------------------------------------------------------
[URI] http://localhost
[OPTIONS] POST,OPTIONS,GET,HEAD,TRACE
--------------------------------------------------------------------------------------
[URI] http://localhost/zaptest
[OPTIONS] POST,OPTIONS,GET,HEAD,TRACE
--------------------------------------------------------------------------------------
[URI] http://localhost/zaptest/dir1
[OPTIONS] POST,OPTIONS,GET,HEAD,TRACE
--------------------------------------------------------------------------------------
[URI] http://localhost/zaptest/dir1/dir2
[OPTIONS] POST,OPTIONS,GET,HEAD,TRACE
--------------------------------------------------------------------------------------
[URI] http://localhost/zaptest/dir1/dir2/dir3
[OPTIONS] POST,OPTIONS,GET,HEAD,TRACE

のような表示が得られます(結果が多重かつ入り乱れて出てくる場合は、この記事の冒頭でお伝えした「並列スキャンスレッド数」が2以上の値を指していないか確認ください)。

また、OPTIONSが有効な場合ZAPのアラートも上げるようにしてあるため、



このような形で、OPTIONSが有効なディレクトリに対してアラートが作成され、アラートの詳細の「証拠」欄にOPTIONSの戻り値である有効なメソッド一覧が記載されています。



次はscan関数の使い方について書きます。

次へ

前回までのあらすじ


part1~part3まではOWASP ZAPのStand Aloneスクリプトでディレクトリ一覧を出力し、各ディレクトリにOPTIONSメソッドのリクエストを実行するというサンプルを書きました。

Stand Aloneスクリプトは、動かし方が分かりやすいので「Hello World」を実行する程度であれば作成が楽なのですが、少し複雑なロジックを組もうとすると、全てをイチから実装しなければならず、かつ現状では情報もサンプルコードも少ないので作成が大変になり、急激に初心者向きではなくなってしまいます。

「各ディレクトリにOPTIONSメソッドのリクエストを実行して実行結果を見る」という処理を書きたいのであれば、Stand Aloneスクリプトで実現するよりも、Active Rulesなど他の種類のスクリプトで実現したほうが実装が容易で、かつ、Stand Aloneだとできなかった「アラートを上げる」ということも可能になるため、今回はActive Rulesのスクリプトとしてpart3のサンプルを書き直してみます。

スクリプトの種類


まず、Active Rulesのスクリプトを作る前に、そもそも「Active Rulesのスクリプト」って何? というところから説明します。
OWASP ZAPに登録できるスクリプトには種類がいくつかあり、呼ばれるタイミングや実行方法などが違います。

OWASP ZAPの「ヘルプ」-「ユーザーガイド」の目次から「Add Ons」-「Scripts」の中に「Script types」という項目があり、ここでスクリプトの各タイプが解説されています。
英語なので訳してみます。(誤訳等あればお知らせください)

Script types

Different types of scripts are supported:
以下の種類のスクリプトがサポートされている:

Stand Alone - scripts that are self contained and are only run when your start them manually
Stand Alone - スクリプトは独立しており、手動で開始した時のみ実行される。

Active Rules - these run as part of the Active Scanner and can be individually enabled
Active Rules - これはActive Scannerの一部として動作し、個別に有効にすることができる。

Passive Rules - these run as part of the Passive Scanner and can be individually enabled
Passive Rules - これはPassive Scannerの一部として動作し、個別に有効にすることができる。

Proxy Rules - these run 'inline', can change every request and response and can be individually enabled. They can also trigger break points
Proxy Rules - スクリプトは'インライン'(「ZAPと一緒に」のような意味と思われる)で動作し、全てのリクエストおよびレスポンスを改変可能である。個別に有効にすることができる。またブレークポイントをtriggerすることもできる。

Targeted Rules - scripts that invoked with a target URL and are only run when your start them manually
Targeted Rules - スクリプトはターゲットとなるURLに対して起動する。手動で開始した時のみ実行される。

Authentication - scripts that invoked when authentication is performed for a Context. To be used, they need to be selected when configuring the Script-Based Authentication Method for a Context.
Authentication - スクリプトはコンテキストにおける認証処理の際に起動する。これを使うためには、コンテキストの「認証」が「Script-Based Authentication」である必要がある。

Script Input Vectors - scripts for defining exactly what ZAP should attack
Script Input Vectors - ZAPが攻撃すべき箇所を厳密に定義するスクリプト。


OWASP ZAPのカスタムスクリプトには上記の7種類のスクリプトがある、ということです。

(ZAPの実際のスクリプトの画面には、「Fuzzer HTTP Processor」など、もっと種類があり、ヘルプの記述が足りていない気がするので、何か情報見つけたら追記します)

Active Rulesスクリプト編(1) まずは関数の動作を確認する


ではActive Rulesのスクリプトを作って動きを見てみます。

ZAP左上の「サイト」タブの隣の「Scripts」タブ(場合によっては「+」マークのタブになっているかもしれませんが、その場合はその「+」タブをクリックすると「Scripts」タブが出てきます)を選択し、Active Rulesを右クリックし、「新規スクリプト」を選ぶと、スクリプト用のダイアログが出てきます。
以下の設定で新規スクリプトを作成します。

Script名: script_test4
タイプ:Active Rules
Script engine:ECMA Script: Oracle Nashhorn
テンプレート:Active default template.js
開始時にロード:オン



「保存」を押下すると、こういうコードがScriptコンソールの上部のペインに現れます。
// The scanNode function will typically be called once for every page 
// The scan function will typically be called for every parameter in every URL and Form for every page 

// Note that new active scripts will initially be disabled
// Right click the script in the Scripts tree and select "enable"  


function scanNode(as, msg) {
 // Debugging can be done using println like this
 println('scan called for url=' + msg.getRequestHeader().getURI().toString());

 ・・・(略)・・・
}

function scan(as, msg, param, value) {
 // Debugging can be done using println like this
 println('scan called for url=' + msg.getRequestHeader().getURI().toString() + 
  ' param=' + param + ' value=' + value);
 
・・・(略)・・・
}

このソースにはscanNode()という関数とscan()関数が定義されています。
この関数は一体何か?
ソース冒頭の英文のコメントを読んでみると

// The scanNode function will typically be called once for every page
// The scan function will typically be called for every parameter in every URL and Form for every page


scanNode関数は一般的に全てのページに対し一度コールされる
scan関数は一般的に全てのページの全てのURLの全てのパラメーターおよびフォームに対しコールされる

という動きをするそうです。

この説明だけだとよく分からないので実際に動かして確認してみます。

まずはソースを以下のように修正します。

・function scanNode()の冒頭の「println」を「print」に修正
・function scanNode()の冒頭の「print」(もと「println」)で出力する文字列を "scan called for url=..." を "scannode called for url=..."に修正
・function scan()の冒頭の「println」を「print」に修正

これはprintlnだとエラーになるための修正と、どちらの関数が呼ばれているか確認しやすくするための修正です。この修正をしたらスクリプトを保存してください。

次に、script_test4スクリプトを右クリックし「スクリプトを有効にします」を選択します。



Active Rulesに登録したカスタムスクリプトは、「スクリプトの種類」のところで翻訳した通り「Active Scannerの一部として動作」します。

Active Rules - these run as part of the Active Scanner and can be individually enabled
Active Rules - これはActive Scannerの一部として動作し、個別に有効にすることができる


具体的には「動的スキャン(Active Scan)」の実行時に、スキャンポリシーの「一般」カテゴリの「Script Active Scan Rules」が実施される際に登録したカスタムスクリプトが実行されます。



動的スキャンを実行するには、とりあえず動的スキャンを実施する対象サイトが必要なので、ZAPで診断しても問題のない自己管理下にあるWEBサイトを用意し、ZAPをプロキシに設定したブラウザを使ってアクセスし、ZAPにそのサイトのアクセス履歴を記録します。

ここでは、ZAPの動的スキャン対象のURLを、例として

http://localhost/zaptest/test.php?aaa=bbb

とします。

動的スキャンを実施するサイトの履歴を右クリックし「攻撃」-「動的スキャン」を選択し、「入力ベクトル」タブで「URL Query String」のみチェックを入れます。
「Script Active Scan Rules」だけを実行する形にしたいので、「ポリシー」タブを開いて全てのカテゴリのthresholdをいったんオフにし、「一般」の「Script Active Scan Rules」のthresholdだけを「既定」にします。
(ここで「入力ベクトル」タブや「ポリシー」タブなどが表示されていない場合、「Show advanced options」チェックボックスにチェックを入れてください)





これで「スキャンを開始」を押下し、動的スキャンを実施します。

(※もし、本ブログのStand Alone編からの続きで作業をしている場合、「Scriptコンソール」の「ほうき」の右横のボタンが押下された状態になっているかもしれませんが、Active Rulesスクリプトの場合、結果が一行ごとに消えてしまうような挙動になってしまうため、このボタンはオフにしてください。)


[スクリプトが実行時にエラーになった場合の注意点]

動的スキャンを実施して、シンタックスエラーなどでうまく動作しなかった場合、スクリプトが「無効」の状態に戻ってしまうので、エラー箇所の修正後、再度スクリプトを「有効」にします。



[スクリプト正常動作時]

スキャン対象に対して「Script Active Scan Rules」のみ有効の動的スキャンを実行してみると、Scriptコンソールの下のペインに、診断対象にしたURLに応じて

scannode called for url=http://localhost/zaptest/test.php?aaa=bbb
scan called for url=http://localhost/zaptest/test.php?aaa=bbb param=aaa value=bbb


というログが表示されたと思います。

このログからすると、scannode()とscan()がそれぞれ一度づつコールされたようです。

これでもまだ動作がよく分からないので、以下の手順でさらに詳しい挙動を確認してみます。


■テスト1:
スキャン対象を上位ディレクトリにしてみます。

「Scriptコンソール」の「ほうき」マークをクリックし、前回のスクリプトの実行時ログを消します。
それから、ノードツリー末端のtest.phpではなく、ルートノードである http://localhost/ を右クリックし、さきほどと同じ設定で動的スキャンを行ってみます。(動的スキャン「スコープ」タブの「再帰的」にチェックが付いていない場合は付けてから実行してください。)





実行結果:

scannode called for url=http://localhost/zaptest
scannode called for url=http://localhost
scannode called for url=http://localhost/zaptest/test.php?aaa=bbb
scan called for url=http://localhost/zaptest/test.php?aaa=bbb param=aaa value=bbb


順番は前後していますが、ノードツリーに含まれるノードの数(3つ)ぶんだけscannode()が呼ばれた事が分かります。




■テスト2:
動的スキャン対象URLのパラメーターを増やしてみます。

http://localhost/zaptest/test.php?aaa=bbb

http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff


前回の結果とごっちゃにならないよう、ZAPの「ファイル」-「新規セッション」でセッションをいったんリセットし、http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff にアクセスをして、このURLに対して「Script Active Scan Rules」のみ有効の動的スキャンを実行すると出力はこのようになります。



実行結果(表示上改行を入れています):

scannode called for url=http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff
scan called for url=http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff
param=aaa value=bbb
scan called for url=http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff
param=ccc value=ddd
scan called for url=http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff
param=eee value=fff


URLに含まれるパラメーターのキーの数(ここではaaa,ccc,eee)だけscan()が呼ばれた事が分かります。


■テスト3:

動的スキャン対象URLをさきほどのURLパラメーターが3つ付いたものとし、動的スキャンの「入力ベクトル」から「URL Query String」を外し、例えば「Cookie Data」にチェックを入れて「Script Active Scan Rules」のみ有効の動的スキャンを実行してみます。
対象:

http://localhost/zaptest/test.php?aaa=bbb&ccc=ddd&eee=fff


実行結果:

scannode called for url=http://localhost/zaptest/test.php?aaa=bbb
(scan関数は呼ばれない)


テスト結果から分かった挙動をまとめると、Active RulesのスクリプトにおけるscanNode()関数とscan()関数の挙動としては、

・scannnode()関数は、対象となる範囲に含まれるノードに対して都度コールされる
(ノードが3つであれば、3回コールされる)

・scan()関数は、「入力ベクトル」で指定された攻撃ベクター(URL Query String / POST Data 等)に合致する項目ごとにコールされる
(例えばGETパラメーターが3つあり、「入力ベクトル」で「URL Query String」にチェックが入っていれば3回コールされる。また、コールされる際、引数に処理対象パラメーターのkeyとvalueがそれぞれ入る)

となります。

続きます。

次へ

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