kazokmr's Blog

試したこと、読んだこと、見たこと、聴いたことを書きたくなったら書くブログ

OWASP ZAP でペネトレーションテストを学ぶ(後編)

これは何の記録か?

ペネトレーションテストを理解するために、実際にOWASP ZAP (Ver.2.11.1)を使って検証したことのまとめ

前編では、OWASP ZAPのドキュメントを読んで理解したことをまとめた。

kazokmr.hatenablog.com

試したこと

  • OWASP ZAPの基本機能を使ったペネトレーションテストの実施手順
  • GraphQL と OpenAPI のURLを取得するためのスキーマ定義のインポート
  • リクエストとレスポンスのデータを動的変更するためのScriptの作成

試していないこと(いずれ試したい)

  • OWASP ZAP を Dockerコンテナから実行する
  • CI/CDパイプラインからOWASP ZAPでぺネトレーションテストを実行する
  • OWASP ZAP を 活用した手動ペネトレーションテストの実施

OWASP ZAP を試してみた感想

この後の記録が長いので感想を先に書いておく

  • ペネトレーションテストが導入しやすい
    • 基本的な脆弱性の検査を一通り行えるので、脆弱性の知見に乏しくても最低限の検証は行える
    • 検査対象のURLリクエストの探索を網羅的に収集したいときに便利
    • 開発段階で最低限の脆弱性検査が頻繁に行えそう
  • このツールに頼り切るだけでは必要な検査はカバー出来ない
    • SPA(CSR)のようなサーバーとの通信が発生しないフロントエンドアプリは検査しづらい
    • 検証結果・探索結果が適切か判断は人が確認する必要がある
  • 検査する脆弱性と検査しない脆弱性を把握しコントロールしなければならない
    • 初期設定のままだと必要以上に検査時間が掛かったりする
    • 探索したい脆弱性の検出にデフォルトでは対応していないこともある
    • 何の検査をどの程度の強度で実施するかコントロールする
    • 自分達でスクリプトを作ることも時には必要
  • 日頃からぺネトレーションテストを意識した準備と対応が大事
    • 検査が必要な脆弱性の抽出とその検証方法を予め定義する
    • 検査対象のURLとリクエスト/レスポンス情報の正しさを確認できるようにする
    • 検証範囲を「PRが発行される都度」「日次のスモークテスト」「リリース前」などに分割しないと必要以上に検査に時間を取られる
    • セキュリティ専門チームを置くと良さそう

準備

インストールは前編に書いているので省略。

テスト対象Webアプリ

OWASPが提供している WebGoat をローカルPCのDockerから起動して利用した。 認証機能を持つため、予めユーザーアカウントを登録しておいた。

owasp.org

クライアント環境のプロキシ設定

ブラウザをOWASP ZAPから起動させる場合、事前に設定することは無かった。

通常、ブラウザとサーバー間の通信(リクエストとレスポンス)を傍受したり書き換えられるようにするには、クライアント端末でOWASP ZAPをプロキシとする設定が必要。 OWASP ZAP がWebdriverを使ってブラウザを制御することでこの設定が無くても通信の探索が出来ていると考えている。

アプリケーションを探索する (Explore)

テスト対象アプリケーションにアクセスする URL とその通信で送受信した リクエストレスポンス を探索し収集していく。

手動で探索する(Manual Explore)

ブラウザを手動で操作することで、その時の通信情報をOWASP ZAPにキャプチャさせる。

  1. Workspaceの『Quick Start』タブから『Manual Explore』を選ぶ
  2. URL to explore に、探索するアプリのルートURL (e.g. http://localhost:8081/WebGoat)を入力する
  3. Enable HUD を利用しない場合はチェックを外す *1 *2
  4. Explore your applicationから、探索に使うブラウザを選択する
  5. Launch Browseを押すとブラウザが起動される
  6. 起動したブラウザでアプリを操作すると通信情報がキャプチャされていく
  7. 一通り操作を終えたらブラウザを閉じるとOWASP ZAPの探索も終了する

Manual Explore

通信の取得が目的なので以下のようなリクエスト送信が発生する操作を行う

  • リンクをクリックする
  • ボタンをクリックする
  • フォームに入力する
  • フォームをSubmitする

またこの後の自動探索を円滑に進めるための情報や操作も取得する

  • 想定通りのレスポンスが返るような操作
  • リクエストのqueryとbodyに含めるパラメータの組み合わせパターンを網羅する
  • 認証・認可に必要な情報が取得できる操作
  • ログイン状態(ログイン中orログアウト中)が把握できる情報の取得
  • ユーザーロールごとの探索

URLをContextで管理する

探索した通信情報(送信したリクエストと受信したレスポンス)は、URLと共に記録される。 SiteツリーではURLをパスの階層構造で表示し、Historyタブは探索した時系列で表示する。

探索されたURLには検査対象外の外部サイトが含まれたりするので、脆弱性検査したいURLをContextに追加する。

Contextで特定の関係にURLをまとめて管理することで、SpiderやActive Scanの検査対象を絞ったり(スコープ)、探索不要な外部サイトに誤って攻撃することを防ぐことができる。 また認証方法やセッション管理、ユーザーロールの登録などはContextで設定することになる。

Contextは検査範囲ごとに複数用意し管理することができる。

Siteツリー

認証方法・セッション管理方法を設定する

アプリケーションの認証情報を設定しておくことで、認証後の画面と機能を探索できるようにする。認証設定はContext をダブルクリックするか Session Properties メニューから行う

Authentication

Authentication Method

Form認証やHTTP認証などを指定する。他にも手動で認証操作を行ったり、カスタムスクリプトで認証を実行できたりする。

Form認証を例にすると、ログイン認証を行うリクエストURL と 必要ならページURLを用意し、POSTで渡す認証情報(credential)を定義する。これらの情報は探索済みのリクエストから Flag as Context で取り込むことができる。

UsernameとPasswordに渡す値は、{%username%}{%password%} と変数で指定することで、Userごとに認証情報の値が設定できる

Authentication Verification

送受信するリクエストとレスポンスの情報から、ログイン中またはログアウト中かを判断するAuthentication Stratagy とパターンを設定する

  • Check every Response: レスポンスに含まれる情報で判断する。例えばhtmlの中にログイン中であることが識別できる リンク・アイコン・メッセージの有無などを検索する
  • Check every Request: リクエストに含まれる情報で判断する。例えば、ヘッダに認証トークンの含まれているかどうかを検索する
  • Check every Request of Response: リクエストとレスポンスの両方で判断する
  • Poll the Specified URL: 指定した間隔で認証有無が判断できるURLにリクエストを送信した際のレスポンス情報で現在がログイン中かどうかを判断する。

Users

OWASP ZAP からWebアプリケーションにアクセスするときのUserが作成できる。UserはAuthentication Method で作成した username と password パラメータへの値を登録できるので、ロール別に複数Userを用意して使い分けることで、テストの網羅性を上げることができる。

Forced User

ログイン認証が必要なリクエストで利用するユーザーをUsersから指定する。この機能を利用するには探索やスキャンの前に ツールバーの "Forced User Mode" (鍵アイコン) をEnabledにする必要がある。

これとは別にSpiderやActive Scan実行時にUserが選択できるが、検証ではここでUserを選択しただけだと認証情報を利用してくれなかったので、Forced User Modeは必ず有効にする必要がありそうだった。

セッション管理

Webアプリのセッション管理方法を指定することができる。

  • Cookie-Based: CookieにセッションID(トークン)を管理する方法。別途、トークンを管理するCookie名を設定する
  • HTTP Authentication: リクエストヘッダで "Authorization" パラメータと値を渡す方法
  • Script-Based: 上記以外のセッション管理方式は、スクリプトを作成し管理することができる。

URLを自動探索する

Spider で探索する

SpiderはWebアプリをクローリングしてURLを自動探索する機能。 受信したレスポンスBodyに含まれる情報から、検索したURLにリクエストを送信することを繰り返して、URLと通信情報を取得する

Ajax Spider で探索する

アドオンとしてOWASP ZAPにプリインストールされているもう一つのSpider。

前述のSpider *3は、静的なURLリンクが検出できるがレスポンスから読み取れないURLは検出できない。 Ajax Spiderはブラウザを起動して実際にボタンやリンクを操作することで動的に生成されるリクエストURLを検出することができる。*4

Forced Browse で更にURLを探索する

Webアプリ用のミドルウェアフレームワークによって生成されるディレクトリなどは、開発チームの意図に関係なく公開されてしまうことがある。このようなディレクトリはWebアプリ内のページ操作では探索できず、直接URLをリクエストすることで探索できる。 このようなディレクトリ名からURLを生成して探索を行う機能がForced Browserアドオンでプリインストールされている。

ディレクトリ名のリストファイルは、プリセットされているもの、アドオンで追加するもの、独自にカスタマイズして追加するものが使える。

自動探索を行う際の注意事項

それぞれの機能の役割に従ってURLを網羅的に探索できるが以下のようなことに注意する

  • 検索対象の範囲指定を適切にする
    • 必要以上にURLを探索することで、外部サイトへアクセスしてしまう可能性がある
    • 検索の深さが大きいため再帰的に何度も探索してしまい探索が終わらない。

これらは実行する前にOptionで設定ができるので、検査仕様を決めた上で実施するとよい

  • 検査結果が正しいか確認し、必要であれば修正して再実行する
    • 認証情報や認可のためのトークンが不正のためエラーレスポンスしか返っていない。
    • 必要なリクエストパラメータの値が間違っている。または不足している。

適切な値で送信しないと適切なレスポンスが受信できない場合もあるので手動で修正する必要がある。

他にも後述する リクエストURLの「パスパラメータ」や「クエリパラメータ」といったURLの構造の調整 (Structural Modifiers)も必要に応じて設定する

データ駆動コンテンツ (Data Driven Content)

標準ではURLが異なれば機能も異なると認識して整理されるが、パスの一部をパラメータとして利用するケースがある。
例: ユーザーの詳細情報を取得するURLに /user/{userId}/detail のような ユーザーIDをパスに含める場合。

この場合はContextのStructure設定で "Data Drive Node" として動作するURLパスを正規表現で定義する。

構造パラメータ (Structural Parameters)

先ほどのData Driven とは逆で、URLは同じだがクエリパラメータの値によって機能が別れるケースもある。
例: ユーザーの詳細情報を取得するURL /user/detail?userId={userId}&userType={userType} があり、ユーザーの種別(userType)によって返される画面が異なる場合。*5

この場合はContextのStructure設定で "Structural Parameter" として動作するクエリパラメータを正規表現で定義することで、クエリパラメータの値によってノードを分けて管理させる。

アプリケーションを攻撃する (Attack)

探索したURLや通信情報を基にWebアプリケーションの脆弱性を検査する。これを "スキャン" と呼ぶ。
スキャン方法は"Passive Scan" と "Active Scan" の2種類があり、特定できる脆弱性が異なる。

スキャンする脆弱性は "スキャンルール" として定義されている。

スキャンルール

スキャンルールは脆弱性を特定するための方法を定義したもので、定義内容に基づいてスキャンを実行する。

スキャンルールは定期的に更新されており、OWASP ZAP にプリセットされている "Release"版だけでなく、検証段階に応じて "Beta" "Alpha" とされているルールもアドオンで追加したり、独自のカスタムルールを作成してスキャンさせることもできる。
どのようなルールが定義されているかは公式マニュアルを参照。次のリンクは "Rlease"版で定義されているスキャンルール。

OWASP ZAP – Passive Scan Rules

OWASP ZAP – Active Scan Rules

スキャンポリシー

スキャンポリシー(Scan Policy)は、スキャンルールに対する実行レベルが定義できる。*6

Threshold

どのレベルで潜在的脆弱性を検査・報告するかをが設定できる。スキャンルールごとに Low~High および OFF を選択する

  • High: そのスキャンルールの最低限の検査だけに限定する。検査時間は早くなるが、潜在的脆弱性が検索されにくくなる。
  • Low: そのスキャンルールで定義されているすべての検査を実施する。潜在的脆弱性が特定されやすいが、偽陽性は増え検索時間も長くなる
  • Medium: High と Low の中間。デフォルト設定は大体これ。 *OFF: そのスキャンルールで定義されている脆弱性は検査しない(スキップする)

Strength

脆弱性を特定するための攻撃数を制御する。これはActive Scanのみの設定。スキャンルールごとに Low~High または Insane を選択する

  • High:より多くの攻撃を行うので発見する脆弱性の数も増える。その分、検索時間が長い。
  • Low: 攻撃の数が少なくなるので検索時間は短い。その分、脆弱性の特定が漏れるので偽陰性となる可能性がある。
  • Medium: High と Low の中間。デフォルト設定は大体これ。
  • Insane: 非常に多くの検査を実施するので時間がかかる。アプリ全体ではなく特定の機能に対して行うのがおすすめ。*7

Passive Scan 結果を確認する

Passive Scanは "送信リクエスト" と "受信レスポンス" の内容を基にスキャンする。出力された情報から脆弱性を診断するだけで意図的な攻撃は行わない。

Passive Scanは これまでの探索の中で自動的に行われており、検出された脆弱性はAlertに登録される。同じリクエストで再スキャンしたい場合は、リクエストを再送信したりSpiderを実行する。

Active Scan を実施する

これまで探索したURL(リクエスト情報)に対して、既知の攻撃を行うことで潜在的脆弱性を検出する。スキャン対象アプリケーションへ実際に攻撃を行うことになるので、外部サイトなどが対象に含まれないように注意が必要。

実行手順

  1. ツールバーの一番左にあるModeプルダウンを ”Protected Mode” にする。Protecte ModeはSocpe対象のURLだけを攻撃対象とする
  2. テスト対象アプリが認証機能(Authentication)を持つなら、ツールバーの "Forced User Mode"(鍵アイコン)を有効にする
  3. テストするContextを右クリックし Active Scan を選択する
  4. ダイアログが表示されるので、利用するUserやScanPolicy、Technologyの設定を確認する。
  5. スキャンを開始する。

対象外アプリケーションを攻撃しないように設定する

テスト対象外のアプリへの攻撃を防ぐために必ず次の設定を行う。

  • "Protect Mode" にすることでScope内のURLに対してだけ攻撃を行う。
  • "Scope"は、Contextに含めることで対象となる。

つまり探索したURLのうち、テストするURLだけをContextに指定するようにし、Protect Modeで実行すること。

スキャンルール "Cross Site Scripting (DOM Based) "

Active Scanのルールの中に標準インストールされているアドオン "Cross Site Scripting (DOM Based)" は、DOM Based XSS を検出するルールであり、サーバーとの通信を行わず、クライアントサイド(フロントエンドアプリ)だけで発生する脆弱性を検出することができる。
そのため、ブラウザを起動してテストを行うので実行にとても時間を有する*8

このためこのスキャンルールを利用する際は、適切な設定や単独で実行するなどの対応が必要そうだと感じた。

Manual Scan を 実施する

最後に Active Scan では検出できない脆弱性を手動操作で検出する。
セキュリティテスト以外のテストでも同じことが言えるが、全てのテストを自動化することは難しい。 特にセキュリティテストは手法が決まっているわけでは無く攻撃者がどんな方法で攻撃を仕掛けるかはわからないし、開発プロセスや技術スタックによってアプリケーション特有のセキュリティホールが存在したりするので、自動化による汎用的なテストだけでは不十分なことが多い。

OWASP ZAPのマニュアルでも「ここまでの操作で基本的な脆弱性は発見できるが、より多くの脆弱性を発見するにはアプリケーションの手動テストも必要になる。」と説明するのと同時に、OWASP Testing Guideが紹介されている。

owasp.org

今回はツールを利用することが目的の一つだったので、手動テストは省略しているが、公式マニュアルでも手動テストを支援する方法を今後紹介する予定としている。

報告する (Report)

アラートの確認

画面下部の Alertタブで検出された脆弱性の詳細が確認できる。
Alert はリスクによって "High" > "Medium" > "Low" > "Informational" > "False Positive" の5段階のレベルがあり、脆弱性ごとにデフォルト値があるがカスタマイズもできる。

レポートを出力する

メニューバーの[Report] > [Generate Rerport] からレポートの出力が行える。 複数のレポートテンプレートが用意されているので、必要なものを選択して出力を行う。

GraphQLとOpenAPI に対するテスト例

公開されているAPIに対するテストを行う場合、API側が公開しているスキーマ定義を取り込んでURLを探索することができる。

OpenAPI (REST API) の API定義を取り込む

メニューバー の [Import] > [Import a OpenAPI definition from...] から 2種類のどちらかの方法で取り込む。

  • ローカルファイルから探索する: OpenAPI仕様に準拠した定義ファイル(.json/.yaml)をインポートする
  • URLから探索する: 公開されているOpenAPIドキュメント(Swagger UIなど)のURLを指定して取り込む

上記とは別にテスト対象アプリケーションのエンドポイントURLを指定し探索を行う。 探索したAPIはこれまで通り、URLが登録され自動的にPassive Scanまで行われるので、あとはこれまで記載した通り適切な送受信が行えるように手動で設定を行う。

GraphQL の API定義を取り込む

メニューバー の [Import] > [Import a GraphQL schema from...] から 2種類のどちらかの方法で取り込む。

  • ローカルファイルから探索する: GraphQLのスキーマ定義ファイル(*.graphql)を指定して取り込む
  • GraphQLエンドポイントURLから探索する: WebアプリケーションのGraphQL API用のエンドポイントURL (例: http://localhost:4000/graphql) を指定する。

上記とは別にテスト対象アプリケーションのエンドポイントURLを指定し探索を行う。
探索した後は、OpenAPIと同様に手動で補完を行う。

Scriptの使い方

通信の際に、受信したレスポンスに含まれる動的なパラメータを次のリクエストに渡して取得したい場合など、OWASP ZAP の標準機能やアドオンでは実現できない処理をスクリプトを用意して実行させることができる。

今回REST APIの検証で以下のアプリを利用したが、このアプリはCSRF対策として、Spring Secruityの利用した二重送信Cookie(Double-Submit-Cookie)を採用しており、サーバーはログイン認証が成功するとcsrfトークンを生成してクライアントのCookieにセットする。 クライアントではセットされたCookieからトークンの値を取得しリクエストヘッダーで渡して送信するようにしている。

github.com

このプロセスはOWASP ZAP では対応していないので、Scripts の "HTTPSender"を利用して、レスポンスを受信した時とリクエストを送信する時に動的なcsrfトークンをセットするようにしている。

var regexUrl = /\/login$/i;
var antiCsrfTokenName = "XSRF-TOKEN";
var reqHeaderCsrfTokenName = "X-XSRF-TOKEN";

// Response受信時の処理
function responseReceived(msg, initiator, helper) {
    // print('responseReceived called for url=' + msg.getRequestHeader().getURI().toString());
    // ログイン認証のレスポンスの場合に処理を行う *リクエストURIの最後が "/login"が該当する
    if (msg.getRequestHeader().getURI().toString().match(regexUrl) != null) {
        // レスポンスヘッダに含まれる Set-Cookieを検索する
        var cookies = msg.getResponseHeader().getCookieParams();
        var iterator = cookies.iterator();
        while(iterator.hasNext()){
            var cookie = iterator.next();
            // セットするCookieが、CSRF対策トークンで、値が渡される場合
            if(cookie.getName().equals(antiCsrfTokenName) && cookie.getValue() != ""){
                // print('Latest CSRF Token value: ' + cookie.getValue());
                // Global変数にcsrf対策トークンの値をセットする
                    org.zaproxy.zap.extension.script.ScriptVars.setGlobalVar("anti.csrf.token.value", cookie.getValue());
                }
        }
    }
}

// リクエスト送信時
function sendingRequest(msg, initiator, helper) {
    // 送信メソッドが"GET"以外、かつ ログイン認証ではない場合
    if(msg.getRequestHeader().getMethod() != "GET" && msg.getRequestHeader().getURI().toString().match(regexUrl) == null){
        // Global変数から csrfトークンを取得する
        var antiCsrfTokenValue = org.zaproxy.zap.extension.script.ScriptVars.getGlobalVar("anti.csrf.token.value");
        // print("antiCsrfTokenValue:" + antiCsrfTokenValue);
        // HttpRequestHeadeにcsrfトークンを追加する
        msg.getRequestHeader().setHeader(reqHeaderCsrfTokenName, antiCsrfTokenValue);
        // Cokieを取得する
        var cookies = msg.getCookieParams();
        var iterator = cookies.iterator();
        while(iterator.hasNext()){
            var cookie = iterator.next();
            // cookieがcsrf対策トークンの場合
            if(cookie.getName().equals(antiCsrfTokenName)){
                var secureTokenValue = cookie.getValue();
                // print("secureTokenValue:" + secureTokenValue);
                // csrfトークンの値がGlobal変数とCookieで異なればCookieの値をグローバル変数に更新する
                if (antiCsrfTokenValue != null && !secureTokenValue.equals(antiCsrfTokenValue)) {
                    cookie.setValue(antiCsrfTokenValue);
                    }
                return;
            }
        }
    }else{
        // HTTPリクエストヘッダーをクリアする
        msg.getRequestHeader().setHeader(reqHeaderCsrfTokenName, null);
    }
}

このスクリプトを有効にすると、探索やスキャンが行われるたびに実行されるようになる。

最後に

かなり長くなったが、実際に試行錯誤しながら使ってみることで、OWASP ZAP の仕組みがわかったし、ペネトレーションテストの勘所も掴めた気がする。

後は脆弱性への理解を深めて「どんな検証をするべきか」「どこまで検証するべきか」などの知識を深掘りして使いこなせるようになりたい。

*1:HUD(Heads Up Display)はOWASP ZAPをブラウザに重ねて表示できる機能。ブラウザとOWASP ZAPを往復しなくてもスキャン結果の確認や簡単な設定が行えるので便利。

*2:OFFにしたのはデュアルディスプレイだったのと、たまに機能しなくなることがあったため。

*3:マニュアルなどでは(Traditional) Spiderと書かれている

*4:SPAのようなクライアントサイドで画面を更新する操作はサーバーとの通信が発生しないため検出できない。

*5:実際はuserTypeなんて指定しないで、userIdを基にサーバー側でuserTypeを特定するとは思う

*6:Scan Policyの設定はActive Scanに対して行うが、ThresholdはPassive Scanでも有効なので便宜的ではあるがここでまとめて記載した

*7:Insaneという用語を使っているがそれ自体に差別的な意味は無い旨の説明が公式マニュアルに記載されている。

*8:検証でも数ページの探索を1日以上かけても進捗が50%にならずに途中でやめた。