XSStrikeを読む part2 - XSS可能かを評価するための DOM base XSS スキャンメソッド編
はじめに
前回に引き続き、 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は元気が出た時にでもしやります・・・。