マクロアイテムから値を取得してリクエストに反映させるBurpExtenderを作りたくなった人(自分)のためにサンプルを調べて解説する

はじめに
---
こんにちは、株式会社シーエー・アドバンス技術統括本部の@sk888です。
普段はWEBサイトやスマートフォンアプリの脆弱性診断業務をしています。

業務では脆弱性診断用プロキシツールのBurpSuiteを使用しているのですが、
診断対象によってはBurpの通常機能では対応できないものもあり、
そんな時にはBurpのExtenderタブ内のBAppStoreから拡張機能を探したり、
Webを検索してみたりします。

必要としているBurp拡張がすんなり見つかるといいですが、自分で拡張機能が作れるようになると、
いろいろなシチュエーションに対応できるようになり、より強くなれると思います。

やりたいこと
---
というわけで今回は、私がBurp拡張の作成に手を出してみた際の学習メモ的な記事になります。
burpのマクロアイテムに登録したHTTPレスポンスから取り出したい値を抽出して、リピーターや
イントルーダーなどで投げるリクエストに反映させるというようなマクロを作るために必要なことを
調べました。

Extension作成の前に
---
今回は言語としてPythonを選択しました。
他の言語としてはjavaやrubyが使用できるようです。
下記のサイトに、Python、java、rubyそれぞれでExtension作成するにあたっての
Burpの設定や必ず実装するべきクラスやメソッドの情報が載っています。
pythonの場合は拡張機能の実行環境としてjythonを使用しているようです。
jythonのjarファイルをダウンロードしてBurpのExtenderタブ内のOptionsから設定しましょう。

* [Writing your first Burp Suite extension](https://portswigger.net/burp/extender/writing-your-first-burp-suite-extension)

↓BurpのExtenderタブ内のOptions画面
![python_environment.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255483/1248d01e-f9b5-b2df-c319-837a05379d0a.png)
サンプルコードのダウンロード
---
さて、さっそく取り掛かっていきましょう。
BurpのExtenderのサンプルが下記のサイトに公開されており、ダウンロードできます。
ページを下までスクロールしてCustom session tokensのサンプルをダウンロードしましょう。
ダウンロードしたZIPファイルを解凍するとpythonフォルダがあり、その中のSessionTokens.py
ファイルがpythonのサンプルコードになります。

* [Burp Suite Extensibility](https://portswigger.net/burp/extender/)
![FireShot Capture 001 - Burp Suite Extensibility - PortSwigger - portswigger.net.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/255483/c281b188-b2d1-2fc5-80a5-8b7853e09296.png)

BurpのExtender作成の際にはBurpの通常機能にアクセスするAPIなどを利用して、
HTTPリクエストやレスポンスの取得、タブや右クリックメニューへの追加、
イントルーダーやScan等との連携、カスタムロギングなどが行えるので、
実際にロジック部分を自分で書いていく形になります。
自分が必要としている機能がどのクラスのメソッドを使用して実現できるのかを調べるために
下記のサイトでクラスやメソッドの仕様を確認したりしてみるといいでしょう。

[Javadoc](https://portswigger.net/burp/extender/api/)

サンプルの内容を解説してみる
---
それではSessionTokens.pyファイルの中身を見ていきます。


```SessionTokens.py
from burp import IBurpExtender
from burp import ISessionHandlingAction

SESSION_ID_KEY = "X-Custom-Session-Id:"
SESSION_ID_KEY_BYTES = bytearray(SESSION_ID_KEY)
NEWLINE_BYTES = bytearray("\r\n")

class BurpExtender(IBurpExtender, ISessionHandlingAction):

    #
    # implement IBurpExtender
    #

    def registerExtenderCallbacks(self, callbacks):
        # save the helpers for later
        self.helpers = callbacks.getHelpers()

        # set our extension name
        callbacks.setExtensionName("Session token example")
        callbacks.registerSessionHandlingAction(self)

    #
    # Implement ISessionHandlingAction
    #

    def getActionName(self):
        return "Use session token from macro"

    def performAction(self, current_request, macro_items):
        if len(macro_items) == 0:
            return

        # extract the response headers
        final_response = macro_items[len(macro_items) - 1].getResponse()
        if final_response is None:
            return

        headers = self.helpers.analyzeResponse(final_response).getHeaders()

        session_token = None
        for header in headers:
            # skip any header that isn't an "X-Custom-Session-Id"
            if not header.startswith(SESSION_ID_KEY):
                continue

            # grab the session token
            keylen = len(SESSION_ID_KEY)
            session_token = header[keylen:].strip()

        # if we failed to find a session token, stop doing work
        if session_token is None:
            return

        req = current_request.getRequest()

        session_token_key_start = self.helpers.indexOf(req, SESSION_ID_KEY_BYTES, False, 0, len(req))
        session_token_key_end = self.helpers.indexOf(req, NEWLINE_BYTES, False, session_token_key_start, len(req))

        # glue together first line + session token header + rest of request
        current_request.setRequest(
                    req[0:session_token_key_start] +
                    self.helpers.stringToBytes("%s %s" % (SESSION_ID_KEY, session_token)) +
                    req[session_token_key_end:])

```

IBurpExtenderとISessionHandlingActionをインポートします。
IBurpExtenderはBurpが拡張機能を読み込むときに読み込まれるメソッドを含んでおり、
拡張機能を作成するときは必ずインポートします。
ISessionHandlingActionはカスタマイズしたセッションハンドリングアクションをburpに登録
するために使用します。このクラスに含まれるperformActionメソッド中でマクロアイテムの
HTTPレスポンスを取得したり、抽出した値をリクエストにセットしたりします。

```SessionTokens.py
from burp import IBurpExtender
from burp import ISessionHandlingAction
```
このサンプルの例では「X-Custom-Session-Id:」というヘッダの値をレスポンスから取得するようです。
ヘッダを探すためのヘッダ名をSESSION_ID_KEY に格納し、2行目のbytearray(SESSION_ID_KEY)で
バイナリデータとしてSESSION_ID_KEY_BYTESに格納しています。
3行目は後半のリクエストをセットする時点で使われます。これは改行をバイナリデータにして
NEWLINE_BYTES に格納しています。 

```SessionTokens.py
SESSION_ID_KEY = "X-Custom-Session-Id:"
SESSION_ID_KEY_BYTES = bytearray(SESSION_ID_KEY)
NEWLINE_BYTES = bytearray("\r\n")
```
ここで定義されているregisterExtenderCallbacksが、拡張機能が呼び出されたときに読み込まれます。
後でレスポンスの解析や、値をリクエストにセットするための位置を取得したりするときに使用する
ヘルパーオブジェクトを取得しています。
setExtensionName()で、このExtensionの名前をセットしています。
registerSessionHandlingAction()はカスタムセッションハンドリングアクションとしてこのExtensionを
Burpに登録します。

```SessionTokens.py
 def registerExtenderCallbacks(self, callbacks):
        # save the helpers for later
        self.helpers = callbacks.getHelpers()

        # set our extension name
        callbacks.setExtensionName("Session token example")
        callbacks.registerSessionHandlingAction(self)
```
Burpがセッションハンドリングアクションの名前を取得する際に使用します。
セッションハンドリングルールのアクションを選択するときに、ここの値が表示されます。

```SessionTokens.py
    def getActionName(self):
        return "Use session token from macro"
```
performActionメソッドはcurrent_requestとmacro_itemsを引数としており、
このメソッドの中でmacro_itemsから値を抽出し、current_requestにセットします。
macro_itemsから.getResponse()でHTTPレスポンスを取得し、final_response に格納しています。
その後、self.helpers.analyzeResponse(final_response).getHeaders()でfinal_responseから
レスポンスの解析を行ってhttpヘッダーを抽出し、headers 変数に格納しています。

```SessionTokens.py
    def performAction(self, current_request, macro_items):
        if len(macro_items) == 0:
            return

        # extract the response headers
        final_response = macro_items[len(macro_items) - 1].getResponse()
        if final_response is None:
            return

        headers = self.helpers.analyzeResponse(final_response).getHeaders()
```
headers 変数からfor文でレスポンスに含まれていたHTTPヘッダーを一つずつ取り出して
if not header.startswith(SESSION_ID_KEY):で評価しています。
SESSION_ID_KEYの値から始まるヘッダが見つからない場合はcontinueでループを繰り返し、
SESSION_ID_KEYの値から始まるヘッダが見つかった場合はlen(SESSION_ID_KEY)でSESSION_ID_KEYの
長さを取得したあと、header[keylen:].strip()でヘッダーの終わり(つまり値の始まり)から値を取り出して
先頭の空白と末尾の改行コードを削除しています。
これでsession_token 変数にはマクロに登録したHTTPレスポンス中の"X-Custom-Session-Id"の値が格納されています。

```SessionTokens.py
        session_token = None
        for header in headers:
            # skip any header that isn't an "X-Custom-Session-Id"
            if not header.startswith(SESSION_ID_KEY):
                continue

            # grab the session token
            keylen = len(SESSION_ID_KEY)
            session_token = header[keylen:].strip()

        # if we failed to find a session token, stop doing work
        if session_token is None:
            return
```
current_request.getRequest()で送信するリクエスト内容を取得し、req変数に格納しています。
ヘルパーオブジェクトのindexOf()メソッドは指定したパターンがデータ内で最初に出現する部分を検索します。
第一引数reqの中を第二引数SESSION_ID_KEY_BYTESでパターン検索します。第三引数は検索で大文字小文字を区別するかどうかのフラグです。
第四引数と第五引数はそれぞれパターン検索の範囲(開始位置と終了位置のオフセット)を指定します。
戻り値として、指定された範囲で最初にパターンが出現した位置を返します。
ですので、session_token_key_start 変数には送信するリクエストの中で、パターン"X-Custom-Session-Id"が
最初に出現する開始位置が格納されており、session_token_key_end 変数には送信するリクエストの中で
session_token_key_start変数を開始位置としてパターンNEWLINE_BYTES 変数(最初に改行を格納しましたね)
の出現位置が格納されます。
もうちょっと簡単に言うとsession_token_key_start 変数には送信リクエスト中のX-Custom-Session-Idヘッダの
開始位置、session_token_key_end 変数には送信リクエスト中のX-Custom-Session-Idヘッダの終了位置が格納されています。

```SessionTokens.py
       req = current_request.getRequest()

        session_token_key_start = self.helpers.indexOf(req, SESSION_ID_KEY_BYTES, False, 0, len(req))
        session_token_key_end = self.helpers.indexOf(req, NEWLINE_BYTES, False, session_token_key_start, len(req))
```
最後にcurrent_request.setRequest()で送信リクエストの内容を更新します。
前のステップで送信リクエスト中のX-Custom-Session-Idヘッダの開始位置と終了位置を取得してあるので、
この２つの位置で送信リクエストを二つに分割し、その間にヘッダ名"X-Custom-Session-Id:"(SESSION_ID_KEY変数に格納)とヘッダ値(session_token変数に格納)を挿入することにより、レスポンスから取得した"X-Custom-Session-Id:"ヘッダの値をリクエストにセットする事が出来ました。

```SessionTokens.py
        # glue together first line + session token header + rest of request
        current_request.setRequest(
                    req[0:session_token_key_start] +
                    self.helpers.stringToBytes("%s %s" % (SESSION_ID_KEY, session_token)) +
                    req[session_token_key_end:])
```
最後に
---
ここまでサンプルを解説してみましたが、いかがだったでしょうか？
このサンプルはマクロのレスポンスから送信リクエストへ同名のヘッダーの値を受け渡すような動作になっていますが、ヘッダを検索するための変数をレスポンスとリクエストで分けることで、好きなレスポンスヘッダから好きなリクエストヘッダーに値を受け渡せるようになりますし、パターン検索の部分を正規表現検索にすることで自由度が増したりなど、必要に応じてもっと色々な事ができそうです。

ホントはサンプルを基にカスタマイズしたものを最後に載せたかったんですが、
色々なアレがあって間に合いませんでした。すいません。
完成したら公開しようとおもいます。ホントです。嘘じゃないです。