前言

在过去的几年中,无括号的XSS向量被一些研究人员热议。
在已知的payload中,利用有限字符集执行任意XSS并不少见。其中一种最简单的payload如下:

location=name

该payload再结合上window.name将重定向到'javascript:alert()' URL并执行存储在window.name中的任意XSS,足以满足上述需求。

但是我试图填补研究中的空白,那就是针对严格的内容安全性策略(CSP)执行任意无括号的XSS

XSS挑战

研究的结果是,我在Twitter上发起了XSS挑战,已经有7个人解决了其中的挑战,其中6个人执行了任意XSS。挑战的目标是仅使用有限字符集[a-zA-Z$_=.\u007f-\uffff]中的字符并使用严格的CSP策略来执行任意XSS :

default-src'self';
script-src'self';

这将成功阻止任何内联代码(例如location=name,<svg/onload=alert()>)以及字符串评估器的执行——例如eval("alert()"),Function("alert()")()。

但是,最初的挑战可以通过比预期容易的方式解决,这仅证明绕过CSP可以通过几种方式来完成。我发布了一个修复版本,该版本限制了注入重用脚本并简化了代码。修复版本仅由Roman(@shafigullin)和Ben Hayak解决,其中Roman提出了预期的解决方案,Ben巧妙地重用了挑战定义的某些功能以实现相似的结果。

解决方案

解决方案可以分为几个重要步骤:
1.控制回调函数中的方法
2.发现Unicode 行终止符 U + 2028和U + 2029
3.将任意HTML注入页面
4.制作有效载荷以执行任意XSS
5.绕过严格的CSP

对于“长文”版本,这是最终的PoC。

控制回调函数中的方法

这一步非常简单。为了将代码注入到回调端点中,您必须使用%26cb==alert [PoC]

发现Unicode行终止符

Unicode行和段落分隔符(U + 2028和U + 2029)可用作JavaScript中的行终止符,例如,eval('x=123\u2028alert(x)')将弹出alert。

将任意HTML注入页面

为了使挑战既不依赖window.name也不依赖location.href,在脚本开始时,我限制了两者的使用。令我惊讶的是,事实证明,绕过这两个限制对于大多数参与者来说比预期的要难,这导致参与者发现了一些很酷的技巧。
没有括号XSS的项目中,可以找到一种非常酷的技术,用于将任意HTML注入到页面,即:

document.body.innerHTML = location.search; 
document.body.innerHTML = document.body.innerText;
//in URL:?< img / src ="x" / onerror = alert(23)>

因为location的使用受到限制,所以预期的方法是改为使用document.referrer。
在我的解决方案中,我使用了

<img/id ="x"title ="alert(top.json.secret)">

这将生成:

<img/id ="x"title ="alert(top.json.secret)">

在innerHTML-> innerText链之后。

为任意XSS执行制作payload并绕过严格的CSP

这可能是最艰难的一步,因为它需要同时考虑CSP旁路和任意XSS执行。这也是我自己研究主题时花费最长的步骤。
由于前两天在挑战方面没有取得进展的参与者很多,所以我在Twitter上发布了隐晦的提示。

这本来是一个提示,但是Roman很快证明了该技术可用于挑战,即使我之前测试了该技术也无法解决该问题。我将在文章的最后一部分中详细说明。
该技术的关键问题是,如果直接使用该技术时,它将被CSP阻止。解决方案中最重要的步骤之一就是绕过CSP。

  • 反向代理来拯救

尽管所有的子页面都设置了严格的CSP标头,但我们仍可以在未设置这些标头的同一域中加载页面,这就要看反向代理是如何工作的。如果将反向代理与请求混淆,它将不会将请求转发到后端应用程序,因此将不会设置CSP标头。例如,harderxss.terjanq.me /%2f抛出“Not Found”,而/%GG则抛出“ Bad Request”,因为它无法对%GG字符串进行Url decode。还有一些其他的办法,使反向代理停止类似的要求。超长URI或过长的请求头(如Referer或Cookie炸弹)。

  • 没有CSP的iframe

实际上如果我们使用了/%GG不会设置CSP,并以以下方式注入了iframe:

document.body.innerHTML ="<iframe name=x src=%GG>%
x.eval("alert(location.href)")

看起来它可以绕过CSP,但实际上,它不会。*实际上,它确实绕过了Chromium中的CSP,这是我在研究过程中发现的,并且已经由外部研究人员进行了报道。

  • 为什么不起作用?

将iframe注入DOM后,它处于空白状态,在事件循环中等待加载。例如,x.eval('alert(location.href)'),该payload将弹出带有about:blankURL 的alert,而不是期望的alert %GG。由于空的iframe位于同一个域内,并且不是来自网络,因此会从其父级继承CSP,因此在挑战中该调用将被CSP阻止。

  • 为什么不等待iframe加载呢?

目标很简单,等待iframe加载,然后执行payload。这简单吗?您如何等待iframe加载?
在我最初的解决方案中,我将onerror + throw技术与原型覆盖技术相结合以实现目标。
让我们看看下面的代码会发生什么:

document.body.innerHTML ="<iframe id=i src=data:,1>"
i.onload = atob

我们可以在下面的屏幕截图中看到,这将引发调用错误,因为atob希望在Window对象上调用,但是在Iframe上调用了。我在文章的最后一部分中详细解释了它的工作方式。

因为其中一项规则是要在Chrome和Firefox上都实现任意XSS,所以让我们看看在两个浏览器上将作为参数传递的错误是什么。
Firefox上的调用错误:

Chrome上的调用错误:

我们可以注意到Chrome和Firefox之间存在细微差别。Chrome中的错误将以Uncaught TypeError开头:而Firefox中的错误将以TypeError开头:尽管消息有所不同,但它们都共享相同的错误类型,即TypeError。让我们尝试覆盖它的原型,以便它返回消息的受控部分。

我们可以在上面的屏幕截图中看到,当TypeError.prototype.name更改为任意字符串时,错误消息现在将在Firefox中以“ alert(/1337/)//:”开头。同样,Chrome中的“Uncaught alert(/1337/)//:”。为了使payload在两个浏览器中同时工作,我精心设计了一个简单的多语言版本

TypeError.prototype.name ="-alert(1337);var Uncaught//"

eval后,将在两个浏览器中触发alert。

  • 但是如何进行eval?

由于iframe %GG没有受CSP保护,因此我们可以eval其中的字符串。为此,只需替换onerror=e=>console.log(e)为onerror=i.contentWindow.eval,它将在%GG iframe中进行eval,因此会弹出alert!
最终的payload看起来像本节开头的图片,可以在此处查看完整的payload

替代解决方案

如前所述,如果有人已经发现如何绕过CSP,我的提示恰好是解决方案。这是由于在公开前遗漏测试了这种方法:)但是,只有两个参与者设法将提示应用到了挑战中。
让我们快速看一下,如果用setTimeout替换eval,那么我的payload将如何处理。

令人惊讶的是,即使它似乎是在没有CSP的iframe中执行的,它也会被CSP阻止。

  • 为什么会这样呢?

让我们看看尝试将setTimeout直接分配给onload事件时会发生什么。

如上图所示,它会引发调用错误,并提示以下消息:调用“ setTimeout”的对象未实现Window接口。我们可以从最后的两个截图中了解到setTimeout期望在Window对象上被调用。这是setTimeout和eval之间的关键区别。后者绑定到window,不需要this指向任何东西。

鉴于此,我认为前面提到的setTimeout技巧在挑战中不起作用。我没有意识到有一个功能实际上允许在iframe上下文中调用setTimeout。下面的两个示例显示了两个不同的onload处理程序如何返回两个不同的上下文。

当iframe处于空白状态时,在事件循环中等待加载,即使在浏览器开始从服务器下载iframe的内容之前,也可以在窗口上定义其onload事件!这是我所不知道的,也是我从Roman中学到的,它通过使用略有不同的方法以类似的方式解决了难题:)显然,只有在加载的iframe位于同一源时,这种情况才会发生违反SOP。
最终的有效负载如下所示:

原文地址
原作者:terjanq

点击收藏 | 1 关注 | 1
登录 后跟帖