サーバセキュリティ

OpenAPIを読み込んでOWASP ZAPでDjango DRFを脆弱性検査する

Django DRF の CSRF 問題

Django Rest Framework で作成した REST API でセッション認証を使う場合、CSRF が無効化できない。

ログインのための POST のときも csrftoken が必要。

そのため、HTTP Sender でのスクリプトを使って対処する。

基本以下のサイトのパクリだが、DRF ならではの調整が必要。

何はともあれ OpenAPI ファイルを読み込み

「インポート」⇒「Import an OpenAPI Definition」をクリックし、OpenAPI ファイルを読み込む。

このとき、パッシブスキャンが走る。このスキャンでのエラーやアラートを表示するには左下の「履歴」の横にある「アラート」に表示される。もし「アラート」が表示されていない場合は「+」ボタンをクリックし、「アラート」を選択する。

コンテキストの作成

コンテキストを作成し、攻撃の対象やシナリオ、事前手続きを定義する。

攻撃対象

まずは攻撃の対象として、先ほどパッシブスキャンで読み込んだ URL (エンドポイント) を右クリックし、「コンテキストに含める」⇒「既定コンテキスト」をクリックする。

すると、既定コンテキストの設定画面が表示され、攻撃対象のエンドポイントが表示される。

認証方式

次に認証方式を選択する。今回は JSON-based Authentication を使う。

"Login Form Target URL *:" には、POST で認証を行う URL を指定する。ポイントの 1 つとして "URL to GET Login Page:"に、CSRF token を GET で取得するための URL を入れる。これを設定しないと二重送信クッキーの最初にクッキーから取得する token を取れず、POST に失敗する。

"Login Request POST Data *:" には認証情報を送信するための JSON フォーマットを指定する。ここを指定すると Username Parameter と Password Parameter が自動認識された。

Verification Strategy には、ログインできているかどうかを判定する方法を指定する。今回は API でログイン成功した場合とログイン失敗した場合にレスポンスに含まれる文字列がそれぞれ "Login Success!" と "Login Failed..." であったため、このように設定している。

ユーザーと強制ユーザー

特に迷わないと思う。ユーザーでは「追加」ボタンからユーザーを追加し、強制ユーザーでは攻撃時にどのユーザーを使うかを指定する。

また、設定した後、「強制ユーザーモード」を有効にするのを忘れてはならない。これを設定しないとうまく動作しない。

セッション管理

Django DRF でセッション認証を使っている場合は Cookie-based Session Management とする。

コンテキストの設定はこれでいったんおしまい。

スクリプトの作成

HTTP Sender を右クリックし、「新規スクリプト」をクリックし、Script 名 (ここでは xcsrftoken)、タイプは「HTTP Sender」、Script engine は「ECMAScript : Graal.js」を選択し、保存。

そして「スクリプトコンソール」にて以下のように記述。

var regexUrl = /\/login/;
// レスポンスに Set-Cookie にて返される二重送信クッキーの属性名を指定。Django の場合はデフォルトで csrftoken。
var antiCsrfTokenName = "csrftoken";
// 二重送信クッキーとして POST 時に付加しなければならない HTTP ヘッダー名を指定。Django の場合、デフォルトで X-CSRFToken。
var reqHeaderCsrfTokenName = "X-CSRFToken";

// 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();
            // print(cookie.getName());
            // セットする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"){
        // Global 変数から csrf トークンを取得する
        var antiCsrfTokenValue = org.zaproxy.zap.extension.script.ScriptVars.getGlobalVar("anti.csrf.token.value");
        print("antiCsrfTokenValue:" + antiCsrfTokenValue);
        // HTTP ヘッダーに Referer をセットする (Django の場合、CSRF 対策として Referer のチェックも行っている)
        msg.getRequestHeader().setHeader("Referer", "https://cvmdev.example.com/");
        // HTTP ヘッダーに X-CSRFToken ヘッダーを追加し、属性値として 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;
            }
        }
    }
}

そして出来上がったスクリプトを右クリックし「スクリプトを有効」にする。

動的スキャンの実行

最後に、今回の目的となる「動的スキャン」を実行する。コンテキストの下にあるコンテキストオブジェクトを右クリックし、「動的スキャン」をクリックする。

そして表示された動的スキャンの画面でユーザーを指定し、「スキャンを開始」をクリックする。

履歴のところでログインがうまくできていることを確認する。

コメント

タイトルとURLをコピーしました