ぶるーたるごぶりん

UI, UX, セキュリティとか😘

XSStrikeを読む part2 - XSS可能かを評価するための DOM base XSS スキャンメソッド編

はじめに

前回に引き続き、 XSStrike という XSSスキャナーを読んでいきます。

github.com

前回は Fuzzing するところだったので、XSS スキャナーっぽくなくて個人的には面白くなかったです・・・。

なので、今回は「XSS可能かを評価するための DOM解析部分」を読んでいきます。

前回記事:

brutalgoblin.hatenablog.jp

今回読んでいく部分(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は元気が出た時にでもしやります・・・。