本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
弊社では(既製の製品ではなく)独自開発したWebアプリスキャナを使用しており、品質や作業効率の向上のため、そのツールを毎年少しずつ改善させています(開発には主に筆者があたっています)。
本記事では、今年(2020年)の4~6月に開発した「XSSのトドメを刺す」機能について簡単に紹介します。
機能の概要
XSSの「トドメを刺す」というのは、ちゃんとJavaScriptが動作しそうな値を自動で生成して送り、それが本当に動きそうかを自動で確認することを指しています。
トドメ機能が実現する前は、記号等が含まれる値を送信し、それが反射する箇所の文脈(コンテキスト)の情報と、出力に施されるエンコードやフィルタの情報だけをもって脆弱性の有無を判断していました。例えば、通常の要素内容テキストにパラメータの値が反射するケースで言えば、基本的にツールが確認するのは「<
」が反射するか否かまでで、実際に有意なタグを送ってJavaScriptを動作させる仕事(トドメを刺す仕事)は、診断員の手作業に任されていました。
今回の開発は、その手作業を可能な限り自動化して作業を効率化するのが狙いです。作業の効率化により浮いた時間を、本当に人間の知恵と手作業が必要な診断作業に振り分けられれば、診断全体の品質向上にもつながるはずです。
XSSのトドメを刺すことは、シンプルなケースでは簡単です。しかし、XSSのパターンは本当に多様ですし、サーバ側でのフィルタ(入力値のチェックや変形)の存在を考えた上で、さらにリクエスト数を極力増やさずにやろうとすると、非常に面倒になり開発工数も増えていきます。
そこで今回の開発では、比較的対応しやすい文脈のみに対応を絞り、また単一の文字レベルでのフィルタ(特定の記号がはじかれる等)にはある程度対応するものの、本格的なWAFのバイパスのような手のかかるロジックは開発しない、といった方針としました。実際のところ診断で検出されるXSSの大半はシンプルなものなので、そこまで複雑な仕組みを作らなくても9割程度のケースはカバーできるだろうという目論見です。全てのケースに対応することはできないので、従来の文脈とエンコードのみでの判定も残しており、その結果を参考にして可能ならばトドメを刺しに行くという流れにしています。
トドメに成功したか、すなわち本当に挿入したJavaScriptが動作するかの確認には、既存の独自Parserを拡張して使用することにしました。ヘッドレスブラウザを使う手もあり、より確度の高いチェックができるメリットはありますが、処理の負荷が大きくなる上に、各ブラウザの固有の挙動に対応する余地がなくなるというデメリットもあります。また、ヘッドレスブラウザの場合、イベントを発生させないと発動しないものをどう確認するかという問題も出てきます(やる方法はあるとは思いますが)。もちろんParserとヘッドレスブラウザのハイブリッド方式もありえるので、将来的にはヘッドレスブラウザの使用を検討する余地はあると考えています。
全体の話はこれくらいにして、もう少し個別の詳細を見ていきたいと思います。今回の記事ではJS(インラインのscriptタグの中身やイベント系属性)に値が出力されるケースを取り上げます。JSのケースを取り上げるのは、(改修の本丸ではないのですが)他のスキャナでは余り実装して無さそうな?機能を今回実装したからです。
BurpとZAPの挙動
テスト用に脆弱なアプリ(意味がある処理を行うものでは無い)を開発し、BurpとOWASP ZAPでスキャンしてみました。
以下はBurpでスキャンした結果です。
下はZAPのスキャン結果です(該当行のみ抜粋)。
送信値が反射した箇所が網掛けになっています。いずれもシングルクォートからの脱出には成功していますが、そのままではalertは動作せず、厳密に言えば攻略(トドメ)に至っていません。
動作しない理由は、「シングルクォートで括られた文字列」という文脈は考慮しているものの、その1つ上の文脈(オブジェクトリテラルのvalue部)を考慮することなく「;
」や「//
」を使用しており、それらが構文エラーを起こすからです。JSのparserは厳格で、構文エラーを1つでも含むJSは全く実行されないため、JSのトドメを刺すためには構文エラーを起こさないことが大前提です。
しかし、構文エラーにならない値(例: '-alert(0)-'
)を送ればよいのかというと、それだけでも不十分です。上のプログラムは、function → if → try → if ... という制御フローを持っており、上位の関数であるhoge()が呼び出されない、あるいは上位のifがfalseになる、といった場合はalertにたどり着かないからです。そのため、より確実にalertを動かすには、制御フローを無効化する値を送信する必要があります。イメージ的には、「])};alert(0);function x(){([
」のように、上位の階層を打ち消してトップレベルまで登るための閉じ括弧類と、後半の帳尻を合わすための開き括弧類を含む値です。
実際の診断で、このようにJSの階層の下の方に値が出力されるケースがどれくらいあるかというと、筆者の過去の経験では、本当に深いところに出力される場面に出会うのはせいぜい年に数回、浅いところだとその数倍くらい、という感覚です。これまでそういうケースでは、送信する値の中で括弧を閉じたり開いたりする調整を手動でしていました。この調整ではJSのソースを遡って調べる必要がありますし、単純に括弧の数を合わせるだけでは正しい構文にならないので、階層が深い時はちょっとした手間です。
開発した機能
そこで開発したのは、括弧類を自動で調整する機能です。上述のように、JSに値が出力されるのはそれほどの頻度ではないため、トドメ機能のおまけとして、若干の個人的な興味/趣味も兼ねて作ってみたというところです。
改良後のツールでスキャンした結果が以下です。
赤い網掛け部分が、ツールが生成して送信した値です。オブジェクトリテラルやifやtryを閉じていって、トップレベルの関数(hoge)の外に出ています。alertの後ろは、挿入される箇所以降の帳尻を合わせるための文字列です。構文エラーも無いため、この値を送るとalertが実行されます。
トドメの値の生成において実施している処理は、
① 挿入箇所より前のJSコードをtokenizeする
② tokenを頭から順に調べて、閉じる/開くための情報をスタックに積んでいく
③ スタックに積んだ情報から最終的な値を作る
という比較的シンプルなものです。処理の要となるのは②と③ですが、そこまで複雑なことはしていないため、その部分のプログラムは数百行しかありません。
ただし、考慮しなければならないことは意外に多くあります。今回はテストの一環として、jQueryの全ての文字列リテラル部分(約1,000箇所)への挿入を試したのですが、失敗するケースがかなり見つかったため、それなりの量のプログラム修正が必要でした。例えば、三項演算子に挿入するケース(下)は、このテストでNGとなり修正したものの一つです。
function bar(x) {return x ? 'aaa' : 'bbb';}
当初は三項演算子への考慮が漏れていたため、単純に「'};alert(0);function_(){'
」のような値を送信していました。しかし、三項演算子の真ん中で関数を閉じると、その前も後ろも構文エラーになってしまいます。これについては、三項演算子がある場合には「:0
」と「0?
」を前後に入れる等の処理(下記)を追加することで対処しました。
':0};alert(0);function _(){0?'
このような改修を何度か行い、最終的にjQueryの通常版と圧縮版の文字列リテラル部分への挿入については全て成功するようになりました。jQueryのようなコード、つまり古いブラウザを考慮したオーソドックスな構文を用いるJSについては、一定程度は対応できたと言えるでしょう。
将来的な改善
将来的な改善の余地としては、(どこまでやるべきかは別として)、①対応できるケースを増やす、②生成する値を短くする、の2点があります。
①については、今後様々なJSコードで試せば失敗するケースが新たに出てくるはずなので、そういったものに(原理的に出来そうならば)対応していくということです。現状はtokenizerでJSの解析をしていますが、parserの力が必要な場面も出てくるかもしれません。②の「値を短くする」は、現状でも多少は試みてはいますが、ロジックを改良する余地は多くあります。
おすすめ記事