DOMベースのXSSのファジング戦略:パート1
XSSは依然として圧倒的に最も多い種類の脆弱性です。本記事では、XSSの探索を自動化するための戦略を紹介します。
クロスサイトスクリプティング(XSS)は、依然としてWebアプリケーションで圧倒的に最も多い脆弱性です。他の脆弱性クラスと比べて、作り込みやすく、さらに見つけやすいものです。XSSは、反射型、格納型、DOMベースの3つの系統に分けられます。前者が最も多く見られ、3つの中で最も検出が難しいものでもあります。
DOMベースのXSSを探すには、Javascriptを解析し、ソースとシンクにテイントを付け、テイントを静的に伝播させるといった静的なアプローチを取ることができます。しかし、言語の動的な性質のため、Javascriptではこのアプローチは困難です。誤検知(フォールスポジティブ)が起きやすく、複雑で、多くのリソースを消費します。
動的なアプローチのほうがこの作業には適しているように見えるかもしれませんが、JavaScriptの実行時をイントロスペクトするためにJavascriptの計装が必要になります。考えられるアプローチは次のとおりです(このリストは決して網羅的なものではありません)。
その場でJavascriptコードを書き換えて計装用のコードを注入する。このアプローチは壊れやすく、ある程度複雑で、多くのリソースを消費します
ブラウザーのJavascriptエンジンを計装する。最もリソース効率の良いアプローチですが、ブラウザーの内部をいじる必要があり、これは困難(ほとんどのJavaScriptエンジンが扱うJITの魔法のため)なうえ、長期的に保守するにはコストがかかります
デバッガーAPIを使い、適切な箇所にブレークポイントを設定し、コードをステップ実行するなどの方法を取る。これは遅く、期待したほど機能が揃っていないことがわかっています。すべてのeval呼び出しにブレークポイントを設定するのは至難の業です。
カバレッジAPIを使って何が実行されているかを大まかに把握し、モンキーパッチとProxyオブジェクトの注入を組み合わせる。このアプローチは性能が良く、実装も比較的容易ですが、理論上はいくつかの制約があります
このブログ記事では、カバレッジガイド型のXSSファザーの簡単な概念実証(PoC)を構築します。ファザーは精密なカバレッジ情報を用いて新たに実行されたコードパスを特定し、その情報を使って新しいテストペイロードを生成します。ファザーはシンクメソッドを計装しますが、いくつかの制約があります(evalの頭痛の種を参照)。
PoCの範囲を限定するため、postMessageによるXSSに焦点を当て、Chromeブラウザーとリモートデバッガーを使用します。PoCはPython 3で記述し、ブラウザーとのやり取りにはPychromeライブラリを使用します。
前置きはこのくらいにして、始めましょう。
ChromeのデバッガーAPIとやり取りするには、次のコマンドラインで有効にする必要があります。
chrome --verbose --window-size=1200x600 --disable-gpu --remote-debugging-port=9222 --user-data-dir=/tmp/foo --disable-web-security
必要に応じて、ヘッドレスモードを有効にすることもできます。リソースを約20%節約でき、本格的なXSSファザーを構築する場合には重要です。
重要なフラグはremote-debugging-portで、それ以外は無視してかまいません。disable-web-securityはXSS auditorを無効にするためのものです(まずはXSSを見つけたいので、バイパスの方法は別の機会に考えます)。
APIにアクセスするには、次のようにインスタンス化するだけです。
import pychrome
debug_host = '127.0.0.1'
debug_port = 9222
url = f"http://{debug_host}:{debug_port}"
browser = pychrome.Browser(url=url)
準備が整ったところで、ファザーが何をすべきかを整理しましょう。
- 新しいタブを作成する
- ページで精密なカバレッジの収集を有効にする
- 対象ページにアクセスする(簡単に思えるかもしれませんが、実際はそうではありません)
- 計装用のコードを注入する
- XSS検出用のメソッドを注入する
- ペイロードを注入する
- コード内で実行された経路を検出する
- そこから新しいペイロードを生成する
ステップ6に戻る
最初のステップは単純です。これはインジェクタークラスの初期化処理で、新しいタブを作成して起動し、特定の種類のイベントを収集するためにChromeデバッガーの一連のAPIを有効にします。
def __init__(self, browser):
self.browser = browser
self.debugger = browser.new_tab()
self.debugger.start()
self.debugger.Page.enable()
self.debugger.Console.enable()
self.debugger.Runtime.enable()
コードカバレッジを有効にするのは簡単ですが、その出力を活用するのは少し複雑です。次のコードは、カバレッジ情報を活用可能な形に変換しようとするもので、実行されたコード片を教えてくれます。
class Coverage:
def __init__(self, debugger):
self.debugger = debugger
self.sources = {}
self.coverages = []
self.debugger.Profiler.enable()
self.debugger.Profiler.start()
self.debugger.Debugger.enable()
self.debugger.Debugger.setSkipAllPauses(skip=True)
self.debugger.set_listener('Debugger.scriptParsed', self._on_script_parsed)
self.debugger.Profiler.startPreciseCoverage(callCount=True, detailed=True)
def _on_script_parsed(self, scriptId, **kwargs):
source = self.debugger.Debugger.getScriptSource(scriptId=scriptId)
self.sources[scriptId] = source
ページへのアクセスは簡単ですが、すべてのケースをカバーするわけではありません。たとえばPOSTメソッドを送信する、特定のCookieを設定する、リクエストに特定のヘッダーを追加するといったケースでは、より複雑なコードが必要になり、場合によってはネットワーク上でリクエストを傍受する必要もあります。
PoCのため、ここでは単純なケースを想定します。
self.debugger.Page.navigate(url=url)
ページの読み込みが完了したことを検出するのは、また別の厄介な問題です。デバッガーAPIは読み込み完了の検出に役立つイベントを送信しますが、ここでもPoCのため、単純に待機します。
self.debugger.wait(2)
次のステップは、ページの残りの部分の読み込みが始まる前に必ず実行されなければならない計装用のコードを注入することです。これは、たとえばシンクメソッドにモンキーパッチを当てる必要がある場合に極めて重要です。Chromeデバッガーにはそのための次のAPIがあります。
source = open('instrument.js', 'r').read()
self.debugger.Page.addScriptToEvaluateOnNewDocument(source=source)
ここまでをまとめましょう。Chromeインスタンスを制御し、新しいタブを開始し、計装用のコードを注入し、対象ページにアクセスできるようになりました。記事が長くなりすぎないよう、残りのステップは次回のブログ記事で取り上げます。具体的には次のとおりです。
- XSSをどのように検出するか
- Javascript Proxy APIを使ってオブジェクトをどのように計装するか
- ペイロードをどのように注入するか
- カバレッジデータを活用して新しい分岐をどのように検出するか
- 新しいペイロードをどのように生成するか