はじめに
前回に引き続き、 XSStrike という XSSスキャナーを読んでいきます。
前回は Fuzzing するところだったので、XSS スキャナーっぽくなくて個人的には面白くなかったです・・・。
なので、今回は「XSS可能かを評価するための DOM解析部分」を読んでいきます。
前回記事:
今回読んでいく部分(DOMの解析部分)は以下のファイルです。 XSStrike/dom.py at 0ecedc1bba149931e3b32e53422d5b7c089ba9dc · s0md3v/XSStrike · GitHub
前回も記載しましたが、 main 処理からの処理派生は以下の様になっています。
Main 大きく以下の4つの分岐 --> singleFuzz() --> bruteforcer() --> scan() --> photon() + crawl()
今回の dom()
を call しているのは scan()
の内部です。
XSStrike/scan.py at 0ecedc1bba149931e3b32e53422d5b7c089ba9dc · s0md3v/XSStrike · GitHub
馬鹿正直に読んだせいでアホみたいに疲れました。 では見ていきましょう。
dom() の概要
メソッドに引数とか description とか一切ないので読んだ限りのやつを記載しておきます。 具体的なフローは次の節にて。
dom()
の概要は、
XSSで悪用が可能な要素を見つけて、それに関わる行を返すというのが全体の処理になります。
(この XSSで悪用が可能
な要素のことを Injectable っぽそうな値 と勝手に読んでます)
つまり、 dom()
では、 Injectable っぽそうな関数・型・変数などを見つける処理を実施します。
これは
- sources と呼称される
document.location
,location.href
,history.pushState
などの要素 - sinks と呼称される
eval
,document.innerHTML
,Function
などの要素 - 及び
sources
,sinks
で見つかった関数・変数の結果を入れている変数 (e.g.var xxx = eval("...")
のxxx
)
を検出し、リストで返すという感じです。
全体フロー
では、全体のフローを見てみましょう。
今回も自作XSSスキャナーのためにかなり丁寧に読んでるので、分量が多いです。
前回の記載を踏まえて以下の様な書き方をしています。
- (ある程度)元のコードのインデントを尊重した形式で記載
- 処理のまとまりには
#
を使って勝手にコメント
1. HTMLデータから全ての `<script>...</script>` を取得し、タグの中身(文字列)を抽出 (以下、抽出した部分を "JSコード_FULL" と呼称) (つまり <script> 部分は除いた `console.log(0)` みたいな場所だけ抽出) 2. 取得した JSコード_FULL(複数) を使ってループ 1. JSコード(単体) を \n で split し、行単位になったものを使ってループ(以下、 `script` と呼称 ) ####################################### # JSコードから、 "var " で split する。 # おそらく JSコード から意味のある場所(特に XSS に悪用できそうな Injectable な可能性が高い場所)を抽出している # (この Injectable な可能性が高い箇所、というのは、次に出てくる大きな一塊の処理部分にて (`source` 変数に定義された regexを使って) 検出している) # このひとまとまりの処理の後方にて(バグってるから動かない気がするが...)変数っぽいパラメータがあったら # それを controlledVariables にいれている。 # # まとめると、この処理部分では # 1. 後述する Injectable っぽそうな処理を行っている行 # 2. その処理が入っている変数( 要するに、 Injectable っぽそうな変数 ) # を検出している。 # # この処理の塊の当該行数: # https://github.com/s0md3v/XSStrike/blob/0ecedc1bba149931e3b32e53422d5b7c089ba9dc/core/dom.py#L18-L25 ####################################### 1. parts なる部品にするため、 `script` を `var ` で splitする (JSコードの構成要素として分離している。この行以降、配列の状態であれば `parts`, 個別の場合は `part` と記載する。) 2. parts が存在する場合に以下のループを行う (が、現行では多分デッドコード) (原因は、途中で出てくる `controlledVariables`, `allControlledVariables` の変数が、ループの都度初期化されてるため。 過去ログ見ると前までグローバル変数だったのでおそらくバグ。コントリビュートチャンスですよ、誰か。 過去ログ: https://github.com/s0md3v/XSStrike/commit/3723a95db48b6cb25f098db2c4c16aa52c488236#diff-8ba4e7bf4b3f2db95f21f25a97061568e527589b36ec6d2d692a5d2c42c5c4f7L8) # 現行コード抜粋 > for newLine in script: > ... > controlledVariables = set() # ループ内で毎回初期化(コード的にこっちは正常っぽそう) > allControlledVariables = set() # ループ内で毎回初期化(こっちはバグっぽい) > if len(parts) > 1: # "var " が存在する行か(つまり、 Injectable っぽそうな行かの判別用?) > for part in parts: # part = ["var ", "aa=123"] 形式 > for controlledVariable in allControlledVariables: # (問題の箇所) 3行上で(毎ループ)初期化してるから、デッドコード > if controlledVariable in part: > controlledVariables.add(re.search(r'[a-zA-Z$_][a-zA-Z0-9$_]+', part).group().replace('$', '\$')) # 以下、上記のバグがない( Global 変数前提)で推測まじりに書く 1. `allControlledVariables` を使ってループを行う。 この `allControlledVariables` は、後ほど出てくる `source` の regex で発見された箇所が入ってくる。 ( `document.location`, `history.localStrage` などの検知 regex ) 1. allControlledVariables の中身のどれかが、現在処理中の part に部分的にでも含まれている場合、次の処理を行う 1. regex `[a-zA-Z$_][a-zA-Z0-9$_]+` で文字を抽出し、 `controlledVariable` に保管 regex 部分は `$abc`, `_abc`, `abc` などにマッチ。 controlledVariables は、 (先述した) `allControlledVariables` に値をあとで移し替える用の(一時的?)な配列っぽそう。 ####################################### # 行に `var xxx = document.location` などが含まれている場合 ( Injectable な可能性がある場合) # `document.location` の部分を取得する # その処理( document.location など)が、 parts の中に存在する場合、 # その変数名を抽出して `controlledVariables` に追記しておく。 # ついでに `sourceFound` Flagを True にしておく。 # # この処理の塊の当該行数: # https://github.com/s0md3v/XSStrike/blob/0ecedc1bba149931e3b32e53422d5b7c089ba9dc/core/dom.py#L26-L35 ####################################### 3. script の行に対し `source` ( document.location, location.href など)でサーチする 4. 見つかった Injectable っぽそうな JS の行でループする 1. `var xxx = location.href` などの見つけた箇所から、 `location.href` などの部分を抽出 2. parts 配列の中に `location.href` (このループで見つかった Injectable っぽそうな処理を含む行) はあるか? 1. 見つかった Injectable っぽそうな処理を含む変数を抽出し、 `controlledVariables` に追加 2. `sourceFound` フラグを true にする ####################################### # これまで見つかった `controlledVariables` を、(バグって初期化しまくっちゃう)`allControlledVariables` に追加する # その後、追加を行った `allControlledVariables` の各変数名が、現在ループ中のJSコードの行に含まれているかを確認する。 # 存在した場合は `line = ["tmp_dir"]` みたいな感じで、その要素を line 変数にいれる(かなり謎。 append ではなく上書きだし、バグでは?) # # この処理の当該行数: # https://github.com/s0md3v/XSStrike/blob/0ecedc1bba149931e3b32e53422d5b7c089ba9dc/core/dom.py#L37-L42 ####################################### 5. これまで見つかった `controlledVariables` を保持するために、 現行ループ内で見つかった Injectable っぽそうな変数名の一覧 `controlledVariables` の各要素を `allControlledVariables` に add する 6. (一つ上で追加した)これまでの全ての Injectable っぽそうな変数名の一覧 `allControlledVariables` でループ 1. 現在のJSコードの行に、これまでの Injectable っぽそうな変数名があるかチェック 1. もしマッチした行があればその変数名を抽出する ... のだが、なんか見つかったやつを毎度 line 変数に上書きしているので一個しか検出しなさそう。 ####################################### # JSコードの行部分から、Injectable っぽそうなメソッド部分(など)を抽出する。 # 例えば、行が ` eval("alert(0)") ` だったら `eval` のみを抽出する # # この処理の当該行数: # https://github.com/s0md3v/XSStrike/blob/0ecedc1bba149931e3b32e53422d5b7c089ba9dc/core/dom.py#L43-L49 ####################################### 7. JSコードの現在の行部分から、 `eval`, `Function` などのコードを Injection できる型や関数として抽出する 1. 対象の関数などがあれば、その要素だけを抽出する。 (つまり `eval` 部分のみを抽出) 2. sinkFound フラグを True にする ####################################### # これまでの結果をまとめる(返り値となる配列に要素を追加) # # この処理の当該行数: # https://github.com/s0md3v/XSStrike/blob/0ecedc1bba149931e3b32e53422d5b7c089ba9dc/core/dom.py#L50-L51 ####################################### 8. これまでの処理の中で、 * sinkFound のフラグが立った行 * sourceFound のフラグが立った行 * Injectableっぽそうな変数が含まれていた行 の場合は、その行を `highligted` 配列に追加する。 この highlited 配列が、 `dom()` メソッドの返り値になる(ならないこともある。それは後ほど) ################ # 当該処理: https://github.com/s0md3v/XSStrike/blob/0ecedc1bba149931e3b32e53422d5b7c089ba9dc/core/dom.py#L55-L56 ################ 3. これまでの処理で、 `sinkFound` と `sourceFound` が見つかった場合、 `highligted` 配列を返す。 なければ空配列を返す
まとめ (2021/05/02 追記)
まとめるのを忘れてました。 結局のところこのメソッドは何をやっているのか?ということですが、
「特定の変数」に対し、 document.innerHtml などのページ情報などからデータを読み込む様な処理を行いつつ、 しかもそれを書き出したりしている・実行している ( eval など)箇所を検出するというものです。
例えば以下の様なコードを検出し、各行をログとして出すイメージでしょうか。
(サンプルなのと、おそらくコード的にはこんな感じに抽出&表示されないと思うのであくまでイメージということで・・・)
> var pageUrl = location.href > var profile_url = document.querySelector("#profile_url") > profile_url.innerHtml(pageUrl)
まあ大筋の内容は理解できましたしよしとしましょう。 それと読んでいたりテストしていて思いましたが 自身のスキャナーを作る際に精度向上できそうな筋道が結構あるなーと思えました。 (逆に0ベースでこれを作るのはそれなりにダルそうだなーと)
例えば以下の内容を追加すると精度が上がる?
- 検知前の整形処理で method chain, Object の改行部分を整形し直す
- 変数を先に抽出する処理を行なってから Sink, Source の検知を行う ( XSStrike では合わせて抽出処理を行なっているため、コード前半に問題があるケースでは漏らしそうなイメージ。もしかしたらズレてるかも)
- sink, source の regex の改善 (検知タイプの追加)
おわりに
くぅ〜疲れましたw
途中 list(filter(None, ...))
が出てきて混乱したり、
2箇所ほど(片方は確実に)バグに出会って余計に混乱したりと、
読む気力が削がれまくって時間がかかりましたが、読み終わりました。
公式へのIssueは元気が出た時にでもしやります・・・。