最新のECMAScript機能を活用したXSS Filtering Bypass
ECMAScriptの新しい機能を活用したXSSフィルタリング回避のテクニックとその原理の解説
この記事はAIによって韓国語から機械翻訳されています。原文と表現が異なる場合があります。
原文(韓国語)を読む最近、ブラウザのJavaScriptエンジンがECMAScript 2019 (ES10) までアップデートされ、多くの要素と機能が追加されました。そのため、従来のJavaScriptではなく、最新のECMAScript標準を利用したXSS Filtering Bypass手法が新たに生まれています。これらの内容について共有し、その原理について簡単に説明したいと思います。
XSS (Cross-Site Scripting) 脆弱性は、主にユーザーの不正な入力(invalid input)がブラウザのDOM内部に挿入されたり、何らかのJavaScriptの動作によって攻撃者が意図したコードを任意に実行させたり、コード実行のフローを変更したりする脆弱性です。
多くの場合、XSS脆弱性を防ぐために、以下のような防御メカニズムを設けます。
- コードレベルでのフィルタリング
ユーザーからパラメータを受け取り、それをDOMに出力する必要がある場合、ユーザーのパラメータを検証(validation)し、XSS攻撃に使用されるフレーズや文字が含まれている場合、リクエストを拒否したり、該当するフレーズ/文字を削除したりします。
代表的なものとして、" (Double Quote)、' (Single Quote)、` (Back Quote)、< (Left Angle Bracket)、> (Right Angle Bracket) などの文字、script、alert、eval、onerror などのHTML TagおよびAttributeなどが対象です。
XSSフィルタリングソリューションを通じてフィルタリングするか、または独自に実装したXSS Filter Functionを通じてフィルタリングすることになります。
例えば、Java Webサーバーの場合、<% String param1 = request.getParameter("param1").replaceAll("script", ""); %> のように置換したり、文字列が見つかった場合にパラメータを空にするなど、様々なフィルタリング動作を実装できます。
- WAF(Web Application Firewall)でのフィルタリング
コードレベルでのフィルタリングと同様に、特定のフレーズや文字がある場合、ユーザーのリクエストを拒否します。
WAFでのフィルタリングは、コードレベルよりもそのサーバーのページ全体に適用されるため、一般的に文字に対するフィルタリングより、HTML TagやAttributeのようなフレーズに対するフィルタリングが強力なのが特徴です(経験上)。
replaceなどの動作はほとんど行わず、blockを通じて攻撃を防ぐようです。
最近の十分に防御されたサイトの場合、ほぼすべてのHTML TagとAttributeがフィルタリングリストに含まれています。そのような場合、DOM XSSと呼ばれる ?param="><script>alert('xss');</script> のようにHTML Tagを直接挿入しなければならないXSSは、大部分がフィルタリングに引っかかり実行されません。(さらに、このようにHTML TAGを直接入力しなければならない場合、クライアント側のブラウザのXSS Auditorに検知され、一部の特殊なケースを除いてスクリプトが実行されません。このような場合、実際の攻撃を実行するためのweaponizing(武器化)は困難です。)
したがって、このようにフィルタリングが厳しく設定されている状況でXSS脆弱性が発生するケースの一つとして、<script> タグ領域内にユーザーの入力が「制限なく」挿入される場合があります。ここで言う「制限なく」とは、WAFでのフィルタリングがかかっていても、コードレベルでのフィルタリングが存在しないか不十分で、意図したコードフローを作成できる場合を指します。例を挙げて説明します。
XSS Vuln Code Example
<script>
var username = "<%=request.getParameter("username")%>";
alert(username+"님 로그인 하셨습니다.");
</script>上記のような場合、username パラメータに " (Double Quote) 文字を挿入することで、scriptタグ内の文字列に挿入されていたパラメータが文字列から抜け出し、任意のスクリプトを実行できるようになります。
しかし、これはフィルタリングが全くない場合の話であり、コードレベルのフィルタリングやWAFでのフィルタリングが存在する場合、該当する文字やフレーズを使用せずにコードフローを制御する必要があります。
本記事では、そのような状況で使用できる有用なフィルタリング回避構文をいくつか紹介したいと思います。
alertなど、javascript組み込み関数の実行を正規表現でフィルタリングしている場合
a=alert; a(document.domain);javascriptにおける関数(Function)は、Objectの一種です。javascriptにおけるObjectはオブジェクトとして変数にそのObjectを代入することができます。
したがって、変数 a に alert(組み込み関数)を代入し、これを呼び出すことで alert(document.domain) を上記のように表現できます。
正規表現によって(例: /alert(.+)/)特定の関数の実行を防いでいる場合、上記のように回避が可能です。
以下は上記と同じ説明です。
a=eval; b="aler"; c="t(documen"; d="t.domai"; e="n)"; a(b+c+d+e);a=eval; a(atob("YWxlcnQoZG9jdW1lbnQuZG9tYWluKQ=="));Quoteを使用できない場合
eval(8680439..toString(30)+String.fromCharCode(40,49,41))Int..toString(Int) は前の数字を文字列形式に変換しますが、引数として基数を渡します。つまり、ここでは30進法として解釈して文字列に置換せよという命令です。
String.fromCharCode(int, int) は、引数として渡された decimal integer 形式の数字を文字に置換して concatenation(結合)して返します。
したがって、上記の命令は eval("alert(1)") と同一です。
eval(/aler/.source+/t(1)/.source)javascriptにおいて / の間にある文字列は正規表現です。正規表現はObjectタイプの一種で、その下位関数の中には文字列にtypeを変更できる .source attributeが存在します。
したがって、Quote(Single, Double, Back)なしで、文字で構成された正規表現を .source attributeを通じて文字列に置換された正規表現文字を取得できます。
括弧を使用できない場合
Set.constructor`alert\x28document.domain\x29```Set: Javascript組み込み関数。ここではSetを使用しましたが、他のどの組み込み関数を入れても可能です。
constructor: Setという関数のコンストラクタを設定します。
: Back Quoteは関数に引数を渡すと同時に関数を実行できます。上記の構文を括弧と引用符を使用して表現すると Set.constructor("alert(document.domain)")() と同じです。
\x28,\x29: Ecma Scriptでは、Hex Ascii StringとUnicode String、Octal Stringに対してAuto Typecastingをサポートしています。
\x28 == ( == \50
したがって、Javascriptの組み込み関数Setのコンストラクタ関数の動作を alert(document.domain) に設定し、該当するSet関数を実行(``)することでコンストラクタ関数が動作するようにし、攻撃者が意図した任意のスクリプトを実行させることができるのです。
以下は上記と同じ説明です。
Set.constructor`alert\`document.domain\````Set.constructor`alert(document.domain)```setTimeout`alert\x28document.domain\x29`setInterval`alert\x28document.domain\x29`フレーズフィルタリング + Quoteフィルタリング + 括弧フィルタリング
_=URL+0,/aler/.source+/t/.source+_[12]+/documen/.source+/t.domai/.source+/n/.source+_[13]instanceof{[Symbol.hasInstance]:eval}前から順に見ていきます。
URLは組み込み関数です。関数(Function)はjavascriptにおいてObjectです。ObjectとStringタイプ間の加算演算が発生した時は、Object.toString() メソッドを優先的に実行してObject自体を文字列にType Castingした後、String文字列と加算します。
したがって、上の図のように文字列に変化します。このような動作を実行した理由は、括弧がフィルタリングされているため、括弧を使わずに括弧文字を取得するためです。
_ 変数に該当する文字列が代入されました。 _[12] == (, _[13] == )
上で見たように、正規表現を利用して文字列を取得および加算できます。このような演算を通じて "alert(document.domain)" という文字列を得ました。
最新のEcma Scriptでは、既存のString、Integerなどのデータ型の他に、Symbolというデータ型が新設されました。
このSymbolデータ型の属性の中に hasInstance という属性が存在します。該当するSymbolオブジェクトがinstanceなのかを判断し、その後の動作を再定義できる属性です。
上記のコードをもう少し見やすく表現してみます。
"alert(document.domain)" instanceof { [Symbol.hasInstance] : eval }
{ } はObjectです。このObjectの Symbol.hasInstance 属性(該当オブジェクトがinstanceなら動作するコード)を eval として定義しました。
そして "alert(document.domain)" 文字列を instanceof 演算子を通じて後ろのオブジェクト {} がinstanceなのかを問い、このオブジェクト(Object)がinstanceであれば、上で定義した eval の引数として文字列を入れて呼び出し、その結果を返します。
したがって、結果的に eval("alert(document.domain)") となります。
これと似た原理で構成されるコードは以下の通りです。
_=URL+0,Array.prototype[Symbol.hasInstance]=eval,/alert/.source+_[12]+1+_[13]instanceof[]_=URL+!0+!1,Array.prototype[Symbol.hasInstance]=eval,_[19]+_[38]+_[40]+_[33]+_[4]+_[12]+1+_[13]instanceof[]Event.prototype[Symbol.toPrimitive]=x=>/javascript:0/.source+location.search,onload=open上記のコードには少し異なる内容が混ざっているのでコメントします。
location.searchはuriで ? 以降の部分を指定します。
javascript: schemeを通じて該当schemeへページを移動させれば、その後、任意のJavaScriptコードを実行できます。
上記のコードを解釈してみると、
Event組み込み関数のprototype(コンストラクタ)の Symbol.toPrimitive の戻り値を、関数のホイスティング(Hoisting)とArrow Functionを通じて /javascript:0/.source+location.search に設定します。
上で説明したように、従来のEcma Scriptのデータ型はString、Integer、Undefined、Null、Booleanの5種類でした。
Ecma Script 6からはこれをPrimitive Typeと称し、これにSymbolというデータ型を新たに追加しました。
Symbolは既存のPrimitive Typeとは少し異なる性質のデータ型です。詳細は以下のブログを参考にしてください。
[ES6] 8. SymbolBlog posted about front end developmentjaeyeophan.github.io
Symbolタイプが既存のprimitive typeと一致する場合、これに変換してくれるattribute methodが Symbol.toPrimitive method です。
Symbol.toPrimitive メソッドの戻り値はArrow Functionと関数ホイスティングを通じて設定されています。 "javascript:0"+location.search
onload=open 構文を通じて document.onload の動作を open 関数に指定しています。
onloadはEvent属性なので、Event組み込み関数が実行され、これのprototypeに設定されていた Symbol.toPrimitive が実行されることになります。
open組み込み関数が toPrimitive method を経由し、
open(x => /javascript:0/.source+location.search)
-> x = function(x){return /javascript:0/.source+location.search}; open(x);
-> open(/javascript:0/.source+location.search) のような構文に変化することになります。
攻撃者がuriのリンクを https://ar9ang3.com/test.html?0:alert('xss')//¶m1=foo¶m2=bar のように設定後、xssをトリガーした場合、
location.searchの値は ?0:alert('xss')//¶m1=foo¶m2=bar となり、
全体の文字列は javascript:0?0:alert('xss')//¶m1=foo¶m2=bar となります。
0?0:alert('xss') 以降の文字列はコメントによって消え、これは3項演算の表現式であるため false?false:alert('xss') に変化し、結局、攻撃者が意図したJavaScript構文が実行されることになります。
ここまで書かれた構文以外にも、無数に多くの方法でフィルタリングを回避できます。もし新しい回避手法を思いついた場合は、共有していただけると幸いです。
読んでいただきありがとうございます。
コメント