RCE in Visual Studio Code’s Remote WSL for Fun and Negative Profit

RCE in Visual Studio Code’s Remote WSL for Fun and Negative Profit

The Visual Studio Code server in Home windows Subsystem for Linux uses a neighborhood
WebSocket WebSocket connection to keep in touch with the A long way-off WSL extension.
JavaScript in net sites can join to this server and make arbitrary instructions
on the aim device. Assigned CVE-2021-43907 and -5 USD bounty (the cost
of the EC2 machine to host the proof-of-principle).

Or no longer it’s in actuality funny that PlayStation paid 15Okay USD for nearly the same
with 2.2 million subscribers (it used to be out of scope of their
program, too), but MSFT doesn’t pay for an official extension with more than 10
million installs (obviously, no longer every install is outlandish) for if truth be told one of their most
in style merchandise. But you aren’t here to listen to my rants. So, learn on.

These bugs would per chance perhaps well furthermore be chained:

  1. The local WebSocket server is listening on all interfaces. If allowed through
    the Home windows firewall, outdoors purposes would per chance perhaps well also join to this server.
  2. The local WebSocket server doesn’t ascertain the Starting put header within the
    WebSocket handshakes or non-public any mode of authentication. The JavaScript in
    the browser can join to this server. This is glowing although the server is
    listening on localhost.
  3. We are able to spawn a Node inspector occasion on a advise
    port. Or no longer it’s furthermore listening on all interfaces. External purposes can
    join to it.
  4. If an outdoors app or a neighborhood net site can join to both of these servers,
    they can drag arbitrary code on the aim machine.

Here’s a cool proof-of-principle.

Popping calc from a website
Popping calc from a net site

Discover the Limitations
share for assumptions on this proof of principle.

This helps you find when you luxuriate in to ought to exhaust time reading this weblog or real discontinue
after the abstract.

  1. Yet one other delivery local WebSocket server.
  2. What VS Code Server is.
    1. How A long way-off WSL works.
  3. The adaptation between Visual Studio Code and Code - OSS.
    1. VS Code DRM.
  4. Reverse Engineering a personalized binary protocol with offer code find entry to.
    1. Navigating a TypeScript code depraved with Visual Studio Code.
  5. Exploiting exposed Node Inspector cases.
  6. Exploiting Node processes by injecting surroundings variables.
  7. The vscode protocol handler.
  8. No More Free Bugs1.

The weblog assumes you

  1. can learn some JavaScript and TypeScript.
  2. are acquainted with ideas luxuriate in Identical-Starting put Protection (SOP), WebSockets and non-public
    some files in regards to the browser safety mannequin.
  3. are a bit acquainted with the Home windows Subsystem for Linux or WSL.

Recount About Supply Code

The Visual Studio Code repository is constantly updated. I am going to exhaust a advise commit

To apply along:

$ git clone https://github.com/microsoft/vscode
$ git reset --arduous b3318bc0524af3d74034b8bb8a64df0ccf35549a

We are able to exhaust Code (lol) to navigate the provision code. Genuinely, I created the
proof-of-principle for this vulnerability in WSL with the same extension.

We won’t seek on the extension code on this weblog. The extension is no longer delivery
offer, but it’s likely you’ll perhaps well presumably extract the vsix file and find entry to the minified and
transpiled JavaScript code.

The A long way-off WSL extension is magic. You would exhaust it to
make in “Linux” (WSL) from Home windows with out the exhaust of a Virtual Machine. On the
time of writing it has been do in 10.5 million cases.

Visual Studio Code (Code shifting forward) runs in server mode inner WSL and
talks to a Code occasion on Home windows (I’m calling it the Code client). This
enables us to edit recordsdata and drag purposes in WSL with out running all the pieces

Remote Development Architecture - Credit: https://code.visualstudio.com/docs/remote/faq
A long way-off Vogue Structure – Credit rating: https://code.visualstudio.com/doctors/far away/faq

Or no longer it’s that it’s likely you’ll perhaps well presumably deem of to non-public far away pattern on far away machines by process of SSH
and in containers. GitHub Codespaces uses the
same technology (presumably by process of containers).

How to make exhaust of it on Home windows:

  1. Open a WSL terminal occasion. You’ll be able to ought to non-public the A long way-off WSL extension in
    Code on Home windows.
  2. Speed code /course/to/one thing in WSL.
  3. If the Code server is no longer do in (or is old-usual) it’s downloaded.
  4. VS Code on Home windows runs.
  5. That it’s likely you’ll also uncover a Home windows Firewall popup for an executable luxuriate in this:

Server's firewall dialog
Server’s firewall dialog

Discover how it in actuality works:
https://code.visualstudio.com/doctors/far away/faq#_how-non-public-the-far away-pattern-extensions-work.

Chasing the Firewall Dialog

This firewall dialog used to be explanation why I went down the rabbit gap. The
dialog appears to be like because VS Code server desires to listen on all interfaces (sure non-public

I started with my trusty Direction of Video display:

  1. Ran course of display screen.
  2. Ran code . in WSL.
  3. Instruments > Direction of Tree.
  4. Add course of and children to Consist of filter beneath the terminal occasion where I
    ran code (e.g., Home windows Terminal.exe).

Procmon's process tree
Procmon’s course of tree

This gave me some files, but no longer loads. After some digging, I stumbled on out in regards to the
VSCODE_WSL_DEBUG_INFO surroundings variable. I simply added
export VSCODE_WSL_DEBUG_INFO=glowing to ~/.profile in WSL. We find further files
after running the server.


The output is cleaned up and the feedback are mine.

$ code
+ IN_WSL=glowing
# Converts a WSL course to its Home windows the same
# Recount: This is no longer pure text processing, if the course doesn't exist we can find an error
+ wslpath -m /mnt/c/Program Files/Microsoft VS Code/sources/app/out/cli.js
+ CLI=C:/Program Files/Microsoft VS Code/sources/app/out/cli.js
# Extension ID
+ WSL_EXT_ID=ms-vscode-far away.far away-wsl
# Speed code
+ ELECTRON_RUN_AS_NODE=1 /mnt/c/Program Files/Microsoft VS Code/Code.exe
   C:/Program Files/Microsoft VS Code/sources/app/out/cli.js --find-extension ms-vscode-far away.far away-wsl
# Speed wslCode
+ /mnt/c/Customers/Parsia/.vscode/extensions/ms-vscode-far away.far away-wsl-0.58.5/scripts/wslCode.sh
   b3318bc0524af3d74034b8bb8a64df0ccf35549a safe /mnt/c/Program Files/Microsoft VS Code/Code.exe code .vscode
# Test for updates
+ /mnt/c/Customers/Parsia/.vscode/extensions/ms-vscode-far away.far away-wsl-0.58.5/scripts/wslDownload.sh
   b3318bc0524af3d74034b8bb8a64df0ccf35549a safe /house/parsia/.vscode-server/bin
# Speed the server
+ VSCODE_CLIENT_COMMAND=/mnt/c/Program Files/Microsoft VS Code/Code.exe
   VSCODE_CLIENT_COMMAND_CWD=/mnt/c/Customers/Parsia/.vscode/extensions/ms-vscode-far away.far away-wsl-0.58.5/scripts
   VSCODE_CLI_AUTHORITY=wsl+Ubuntu-18.04 VSCODE_CLI_REMOTE_ENV=/tmp/vscode-distro-env.v7syDw
   VSCODE_STDIN_FILE_PATH= VSCODE_AGENT_FOLDER=/house/parsia/.vscode-server
+ exit 0

Checking the expose-line parameters.

# cleaned up
$ ps -aux | more

sh /house/parsia/.vscode-server/bin/b3318bc0524af3d74034b8bb8a64df0ccf35549a/server.sh
   --port=0 --exhaust-host-proxy --with out-browser-env-var --disable-websocket-compression
   --print-ip-tackle --allow-far away-auto-shutdown --disable-telemetry

   --port=0 --exhaust-host-proxy --with out-browser-env-var --disable-websocket-compression
   --print-ip-tackle --allow-far away-auto-shutdown --disable-telemetry

I saw the magic observe WebSocket and used to be in the present day .

Ran Wireshark and captured the net site traffic on the loopback interface. Then I ran
Code in WSL again. I would per chance perhaps well also look two WebSocket handshakes.

WebSocket connections captured in Wireshark
WebSocket connections captured in Wireshark

The server port in that drag used to be 63574. We are able to furthermore look this within the logs. Open
the expose palette (ctrl+shift+p) within the Code client on Home windows and drag
> A long way-off-WSL: Recount Log.

Remote-WSL: Show Log
A long way-off-WSL: Recount Log

The the leisure line has the port: delivery a neighborhood browser on 63574/version.
We are able to furthermore look the 2 separate WebSocket connections from the Code client on
Home windows to the server.

C:> netstat -an | findstr "63574"
  TCP 63574              LISTENING
  TCP 49782 63574        ESTABLISHED
  TCP 63574 49782        ESTABLISHED
  TCP 63574 64725        ESTABLISHED
  TCP 64725 63574        ESTABLISHED
  TCP    [::]: 63574             [::]:0                 LISTENING

Why is it Listening on All Interfaces?

The server is an occasion of RemoteExtensionHostAgentServer at

Or no longer it’s used by createServer (within the same file). We are able to exhaust Code (lol) to procure
its references and ticket it to remoteExtensionHostAgent.ts (same

// /src/vs/server/remoteExtensionHostAgent.ts
import { createServer as doCreateServer, IServerAPI }
   from 'vs/server/remoteExtensionHostAgentServer';

// ...

/ invoked by vs/server/predominant.js
export feature createServer(tackle: string | rep.AddressInfo | null):  Promise<IServerAPI> {
   return doCreateServer(tackle, args, REMOTE_DATA_FOLDER);

The comment tells us to hunt inner predominant.js (same course, again).

// /src/vs/server/predominant.js
/@form {string | import('rep').AddressInfo | null} */
let tackle = null;
const server = http.createServer(async (req, res) => {  // [Parsia]: <--- SEE
    if (firstRequest) {
        firstRequest = false;
    const remoteExtensionHostAgentServer = await getRemoteExtensionHostAgentServer();
    return remoteExtensionHostAgentServer.handleRequest(req, res);

Further down in the same file, we see the server can get the host and port
from parameters passed to main.js.

// /src/vs/server/main.js
// ...

const nodeListenOptions = (
        ? { path: parsedArgs['socket-path'] }
         // [Parsia]: Get `host` and `port` from command-line parameters.
        :  { host: parsedArgs['host'], port: parsePort(parsedArgs['port']) }

// [Parsia]: Pass nodeListenOptions to the server.
server.listen(nodeListenOptions, async () => {
    const serverGreeting = product.serverGreeting.be part of('n');
    let output = serverGreeting ? `nn${serverGreeting}nn` :  ``;

    if (typeof nodeListenOptions.port === 'amount' && parsedArgs['print-ip-address']) {
        const ifaces = os.networkInterfaces();
        Object.keys(ifaces).forEach(feature (ifname) {
            ifaces[ifname].forEach(feature (iface) {
                if (!iface.internal && iface.family === 'IPv4') {
                    output += `IP Handle: ${iface.tackle}n`;
    // ...

predominant.js is invoked by server.sh:

sh /house/parsia/.vscode-server/bin/b3318bc0524af3d74034b8bb8a64df0ccf35549a/server.sh
   --port=0 --exhaust-host-proxy --with out-browser-env-var --disable-websocket-compression
   --print-ip-tackle --allow-far away-auto-shutdown --disable-telemetry

   --port=0 --exhaust-host-proxy --with out-browser-env-var --disable-websocket-compression
   --print-ip-tackle --allow-far away-auto-shutdown --disable-telemetry

There’s no longer any IP tackle passed to the scripts which I deem is why the server
listening on all appealing. port=0 presumably tells the server to make exhaust of an
ephemeral port. when you are queer, this files comes from wslServer.sh within the
same itemizing.

The Native WebSocket Server

Every time you look a neighborhood WebSocket server, you should ascertain WHO can
join to it.

WebSocket connections aren’t sure by the Identical-Starting put Protection and JavaScript
within the browser can join to local servers.

WebSockets delivery with a handshake. It’s consistently a “easy” (in
the context of Homely-Starting put Resource Sharing or CORS) GET demand so the browser
sends it with out a preflight demand.

However the Identical-Starting put Protection!

“However the demand can no longer look the response with out the
Entry-Adjust-Allow-Starting put header.” YES! Your JavaScript is no longer sending the
GET handshake demand. The browser does and can look the response.
HTTP/1.1 101 Switching Protocols within the response tells the browser to continue.

I if truth be told non-public learned most of what I know on this niche from Tavis and Eric and likewise you
ought to, too.

But wait, there could be more! Yours if truth be told has furthermore created thunder!

Checking out Native WebSocket Servers

Checking out for this field is terribly immediate. Gain a test page that tries to join
to a neighborhood WebSocket server on a advise port. Host it someplace far away (e.g.,
S3 bucket) and delivery it on the machine. If the connection is winning we’re in

I furthermore register Burp. Gain the WebSocket handshake in Burp Repeater. Modify
the Starting put header to https://instance.rep. If the response has
HTTP/1.1 101 Switching Protocols, you are real to scoot!

Testing in Burp
Checking out in Burp

Recount: This most efficient issues for localhost servers. The server here is furthermore
externally exposed (it’s sure to Attackers aren’t sure by the
browser. They’ll join without delay to the server and present any Starting put header.

The subsequent item on the agenda is having a gape on the net site traffic in Wireshark. Actual-click on
on if truth be told one of many WebSocket handshake GET requests from earlier than and find
Shriek > TCP Breeze. This could per chance screen us a screen screen with some readable text. Shut
it and look most efficient the packets for this circulation. This enables us to real apply this

That it’s likely you’ll also demand why I closed the popup that comprises most efficient the thunder of the
messages. This is no longer suited here. By RFC6455 the messages from
the client to server must be masked. It capacity they are XOR-ed with a 4-byte key
(that’s furthermore provided with the message). Wireshark unmasks every packet when
selected however the payloads seem as masked within the initial circulation popup. So we
will look server messages in plaintext whereas client messages are masked and
gibberish. Wireshark unmasks the payload when you click on on particular particular person messages,
but it completely would be awesome if we would per chance perhaps well also conception all of them here and search in
messages, too.

Succor From the Future

I spent about a days reverse engineering the protocol. Later, I noticed I will be able to
real look the protocol’s offer code in

/ A message has the next format:
 |             HEADER            |      |
 |-------------------------------| DATA |
 | TYPE | ID | ACK | DATA_LENGTH |      |
 The header is 9 bytes and consists of:
 - TYPE is 1 byte (ProtocolMessageType) - the message form
 - ID is 4 bytes (u32be) - the message id (would per chance perhaps well furthermore be 0 to screen to be overlooked)
 - ACK is 4 bytes (u32be) - the acknowledged message id (would per chance perhaps well furthermore be 0 to screen to be overlooked)
 - DATA_LENGTH is 4 bytes (u32be) - the size in bytes of DATA
  Solely Traditional messages are counted, other messages aren't counted, nor acknowledged.

The Protocol Handshake

The principle message from the server is a KeepAlive message.

00000000  04 00 00 00 00 00 00 00 00 00 00 00 00           |.............|

In the protocol definition we can look the a style of message forms.

const enum ProtocolMessageType {
   None = 0,
   Traditional = 1,
   Adjust = 2,
   Ack = 3,
   KeepAlive = 4,
   Disconnect = 5,
   ReplayRequest = 6

/src/vs/platform/far away/total/remoteAgentConnection.ts,
it’s called an OKMessage and heartbeat in other substances of the code.

export interface OKMessage {
   form:  'okay';

The client handles this in connectToRemoteExtensionHostAgent in
/src/vs/platform/far away/total/remoteAgentConnection.ts.
We are having a gape on the code connecting to the server here.

The client (Code on Home windows) sends this packet which is a KeepAlive and a
separate auth message.

# OK
0000   04 00 00 00 00 00 00 00 00 00 00 00 00

# new message
# form 02,                       size 2nd
0000  02 00 00 00 00 00 00 00 00 00 00 00 63 7b 22 74  |............c{"t|
0010  79 70 65 22 3a 22 61 75 74 68 22 2c 22 61 75 74  |ype":"auth","aut|
0020  68 22 3a 22 30 30 30 30 30 30 30 30 30 30 30 30  |h":"000000000000|
0030  30 30 30 30 30 30 30 30 22 2c 22 64 61 74 61 22  |00000000","files"|
0040  3a 22 68 75 45 6d 37 2b 4d 34 49 2f 56 42 75 76  |:"huEm7+M4I/VBuv|
0050  67 6d 77 79 70 54 4b 59 4f 7a 62 62 33 32 48 73  |gmwypTKYOzbb32Hs|
0060  42 68 4d 50 68 74 6f 77 41 4a 35 63 51 3d 22 7d  |BhMPhtowAJ5cQ="}|

At the delivery, I believed the size field is 12 bytes in preference to 4 for the reason that relaxation
of the bytes were consistently empty. Then I noticed most efficient Traditional Messages exhaust the
message ID and ACK fields and I if truth be told non-public most efficient viewed handshake messages that aren’t

    "form": "auth",
    "auth": "00000000000000000000",
    "files": "huEm7+M4I/VBuvgmwypTKYOzbb32HsBhMPhtowAJ5cQ="

Earlier than the repair, this used to be no longer checked.

// [Parsia]: The client sending the auth demand.
const authRequest: AuthRequest = {
   form:  'auth',
   auth: choices.connectionToken || '00000000000000000000',
   files: message.files

Recount: Earlier than the 2021-11-09 replace (commit
b3318bc0524af3d74034b8bb8a64df0ccf35549a) the client did no longer ship the files.
On the opposite hand, the exhaust of this commit we can peaceable ship a message with out this key and it
would work. This is one thing we give the server to connect to ascertain that we’re
connecting to the glowing server (here is DRM, it has its non-public share).

The server responds with a attach demand.

0000  02 00 00 00 00 00 00 00 00 00 00 00 79 7b 22 74  |............y{"t|
0010  79 70 65 22 3a 22 73 69 67 6e 22 2c 22 64 61 74  |ype":"attach","dat|
0020  61 22 3a 22 36 32 61 72 35 4e 66 45 6b 30 6b 71  |a":"62ar5NfEk0kq|
0030  38 6f 51 6e 33 56 71 56 4d 63 48 74 6e 36 50 49  |8oQn3VqVMcHtn6PI|
0040  6a 37 51 4a 37 35 65 42 39 4c 67 6d 63 6c 73 3d  |j7QJ75eB9Lgmcls=|
0050  22 2c 22 73 69 67 6e 65 64 44 61 74 61 22 3a 22  |","signedData":"|
0060  31 36 34 62 62 37 31 38 2nd 62 33 66 64 2nd 34 61  |164bb718-b3fd-4a|
0070  30 63 2nd 61 36 66 61 2nd 39 61 36 61 63 38 36 35  |0c-a6fa-9a6ac865|
0080  36 66 37 63 22 7d                                |6f7c"}|

One other JSON object:

    "form": "attach",
    "files": "62ar5NfEk0kq8oQn3VqVMcHtn6PIj7QJ75eB9Lgmcls=",
    "signedData": "164bb718-b3fd-4a0c-a6fa-9a6ac8656f7c"

The server has signed the records that we sent within the earlier message and has
responded with its non-public files demand.

The client validates the signed files to ascertain if it’s far a supported server. We
can simply skip this when we produce our client.

// [Parsia]: Client reads the server's message.
const msg = awaitreadOneControlMessage<HandshakeMessage>(
   combineTimeoutCancellation(timeoutCancellationToken, createTimeoutCancellation(10000))

// [Parsia]: Construct no longer continue if the message form is no longer `attach` or it's no longer a string.
if (msg.form !== 'attach' || typeof msg.files !== 'string') {
   const error: any = new Error('Unexpected handshake message');
   error.code = 'VSCODE_CONNECTION_ERROR';
   throw error;

choices.logService.ticket(`${logPrefix} 4/6. acquired SignRequest control message.`);

// [Parsia]: Validate `signedData` from the server.
const isValid = predict raceWithTimeoutCancellation(
      choices.signService.validate(message, msg.signedData),

if (!isValid) {
   const error: any = new Error('Refused to join to unsupported server');
   error.code = 'VSCODE_CONNECTION_ERROR';
   throw error;

There’s some funky validation occurring here. Signing and stuff in total capacity
DRM. I know, I work with videogames!

Your Editor has DRM

I chased the choices.signService.validate blueprint within the code for an hour and
I got to /src/vs/platform/attach/node/signService.ts.

export class SignService implements ISignService {
   declare readonly _serviceBrand: undefined;

   non-public static _nextId = 1;
   non-public readonly validators = new Method<string, vsda.validator>();

   non-public vsda():  Promise<typeof vsda> {
      // [Parsia]: vsda
      return new Promise((resolve, reject) => require(['vsda'], resolve, reject));
   // [Parsia]: Removed.

vsda is a Node native addon written in C++. Mediate of
Node native addons as a shared library or DLL. This addon is in
a non-public repository at https://github.com/microsoft/vsda and used to be an NPM bundle
till spherical 2019 according to https://libraries.io/npm/vsda/.

Or no longer it’s bundled with VS Code client and server:

  • Home windows: C:Program FilesMicrosoft VS Codesourcesappnode_modules.asar.unpackedvsdamakeLaunchvsda.node.
  • Server (WSL): ~/.vscode-server/bin/{commit}/node_modules/vsda/make/Launch/vsda.node.
    • As of as of late (2021-11-09)2 the Linux version has symbols and it’s resplendent
      puny. Will non-public to be a transient reversing exercise.

So, what is it? Or no longer it’s DRM3. But yeah your editor has DRM!

Code OSS (delivery offer), and the make from Microsoft are a style of according to


Parts of the A long way-off Vogue extensions are used in developer companies
that are drag beneath a proprietary license. Whereas these extensions non-public no longer require
these companies to work, there could be satisfactory code reuse that the extensions are furthermore
beneath a proprietary license. Whereas the bulk of the code is within the extensions and
within the Code – OSS repository, a handful of puny modifications are within the Visual
Studio Code distribution.


Ingredients of the code to barter a connection to the Visual Studio Code server are

I stumbled on https://github.com/kieferrm/vsda-instance and discovered
how to make exhaust of it to present and attach messages after some experiments.

  1. Gain a brand new message with msg1 = validator.createNewMessage("1234"). The
    enter ought to be on the least 4 characters.
  2. Mark it with signed1 = signer.attach(msg1).
  3. Validate it with validator.validate(signed1) and the response
    is "okay".

There’s a monumental caveat. If you occur to present a brand new message, it’s likely you’ll perhaps well presumably no longer validate veteran
messages anymore. In the provision code, every message has its non-public validator.

// In a Node REPL.
// `vsda.node` for your OS is in essentially the latest itemizing or within the course.

> const vsda = require('vsda');
// Gain a validator and a signer.
> v1 = vsda.validator();
validator {}
> s1 = vsda.signer();
signer {}

// Gain a message.
> msg1 = v1.createNewMessage("1234");

// Mark it.
> signed1 = s1.attach(msg1);

// Validate the signature.
> v1.validate(signed1);

// Gain a brand new message.
> msg2 = v1.createNewMessage("1234");

// Now, we can no longer validate the earlier signature because we non-public now created a brand new message.
> v1.validate(signed1);

// But we can attach and validate essentially the most newest message.
> signed2 = s1.attach(msg2);
> v1.validate(signed2);

Gentle DRM Reversing

The Linux version has symbols and is spherical 40 KBs. Tumble it into IDA/Ghidra and
you should be real to scoot.

I spent a whereas on it and got here up with this pseudo-code. This could per chance no longer be glowing
but presents you the usual opinion of how this signing works.

  1. Initialize srand with essentially the latest time + 2*(msg[0]).
    1. This could per chance most efficient produce random numbers between 0 and 9 (inclusive).
  2. Append two random chars from the license array.
  3. Append one random char from the salt array.
  4. SHA256.
  5. Bad64.
  6. ???
  7. Revenue
// bewitch
msg = input_string;

// Test if enter is more than 4 characters.
if strlen(msg) <= 3 return;

// Initial srand with time and the first personality of the message.
t = time(NULL);
srand(t + msg[0] + msg[0]);

non-public twice {
   idx = rand() % 10;  // random amount between 0 and 9.
   license_char = license_array[idx]; // uncover a char from the license array - look beneath
   msg = append(msg, license_char);

// Append one personality from a determined array named salt.
idx2 = rand() % 10;  // random amount between 0 and 9.
salt_char = Handshake::CHandshakeImpl::s_saltArray[idx2];
msg = append(msg, salt_char);

// SHA256 and Bad64 and return.
return Bad64(SHA256(msg););

Solely characters from the first 10 positions are chosen from the license array.
Or no longer it’s consistently rand() % 10 but doubled for the salt array.

The license array is that this string:

That it's likely you'll also most efficient exhaust the C/C++ Extension for Visual Studio Code with Visual Studio
Code, Visual Studio or Visual Studio for Mac application to abet you are making and
test your purposes.

The principle 32 bytes of the salt array (watch Handshake::CHandshakeImpl::s_saltArray) are:

00000000  56 2b 79 2c 28 48 60 76 26 41 5c 40 78 2b 3b 34  |V+y,(H`v&A@x+;4|
00000010  47 75 4b 3c 24 7a 5d 2e 2e 3f 38 23 77 56 5a 6e  |GuK<$z]..?8#wVZn|

I never if truth be told checked if my analysis is glowing or no longer. Or no longer it's presumably no longer.
But I did no longer ought to grab that. I knew how to connect messages the exhaust of the addon and
that used to be satisfactory.

The Finishing Transfer

Subsequent, the client must attach the files from the server and ship it reduction to screen that it's far a "legit" Code client.

// [Parsia]: Client code.
// [Parsia]: attach the records sent by server.
const signed = predict raceWithTimeoutCancellation(choices.signService.attach(msg.files), timeoutCancellationToken);

// [Parsia]: Send a message to the server.
const connTypeRequest: ConnectionTypeRequest = {
   form:  'connectionType',
   commit: choices.commit,
   signedData: signed,
   desiredConnectionType: connectionType
if (args) {
   connTypeRequest.args = args;

The server responds with

The client sends this very very appealing message:

    "form": "connectionType",
    "commit": "b3318bc0524af3d74034b8bb8a64df0ccf35549a",
    "signedData": "997N934vpN7zWC1lGN88DC4p3B9N+L5GlNoVb5t//y/Iy8=",
    "desiredConnectionType": 2,
    "args": {
        "language": "en",
        "damage": glowing,
        "port": 55000,
        "env": {
            "env-var-1": "designate-1",
            "SHLVL": "1",
            // ...

commit ought to compare the server's commit hash. This is no longer a secret. Or no longer it's
presumably the leisure safe launch commit (or if truth be told one of many earlier few). This real
checks if the client and server are on the same version. Or no longer it's furthermore accessible at
http://localhost:{port}/version. Your browser JavaScript would per chance perhaps well no longer be ready to
look it (muh SOP), but external customers non-public no such restrictions.

signedData is the consequence of signing the records we got from the server within the
earlier message.

args is the largest part of this message. It will order the server to
delivery a Node Inspector occasion on a advise port.

  • damage: Break after starting the Inspector occasion.
  • port: The port for the inspector occasion.
  • env: A listing of surroundings variables and their values that are passed to
    the inspector occasion course of.

A Node Inspector occasion would per chance perhaps well furthermore be used to debug the Node
application. If an attacker can join to such an occasion on your machine then
it's game over. In 2019, Tavis stumbled on
VS Code enabled the far away debugger by default.

This is resplendent nifty, neh?!

What Else Can We Enact?

Earlier than we find by this Node Inspector occasion, let's snatch a step reduction
and scrutinize other chances. Mediate the exhaust case. This entire setup is
designed to permit the Code client on Home windows to make remotely in WSL,
containers, or on GitHub Codespaces. This means it'll non-public all the pieces it wants on
the far away machine.

So, if a net site can join to your local WebSocket server and bypass the DRM
(which is rarely a secret), it'll emulate a Code client. It has far away code
execution on your device and doesn't need the Node Inspector occasion.

To this point we non-public now stumbled on two techniques to exploit the device:

  1. Spawn and join to the Node Inspector occasion.
  2. Emulate the Code client and work along with the far away machine the exhaust of the personalised

The Node Inspector Occasion

Let's seek on the args from the earlier message.
/src/vs/server/remoteExtensionHostAgentServer.ts processes them on
the server.

} else if (msg.desiredConnectionType === ConnectionType.ExtensionHost) {

   // This ought to become an extension host connection

   // [Parsia]: msg.args is the cost of args from the client.
   // [Parsia]: Default designate if args is no longer provided.
   const startParams0 = <IRemoteExtensionHostStartParams>msg.args || { language:  'en' };

   // [Parsia]: Decide a free debug port.
   const startParams = predict this._updateWithFreeDebugPort(startParams0);

The IRemoteExtensionHostStartParams interface is equivalent to the JSON object we
saw earlier than:

export interface IRemoteExtensionHostStartParams {
   language: string;
   debugId?: string;
   damage?:  boolean;
   port?: amount | null;
   env?:  { [key: string]:  string | null };

_updateWithFreeDebugPort checks if the port is free. If no longer, this is able to perhaps strive the
subsequent 10 ports. The the leisure free port is saved in startParams.port.

non-public _updateWithFreeDebugPort(startParams: IRemoteExtensionHostStartParams):  Thenable<IRemoteExtensionHostStartParams> {
   if (typeof startParams.port === 'amount') {
      return findFreePort(startParams.port, 10 /strive 10 ports */, 5000 /strive up to 5 seconds */).then(freePort => {
         startParams.port = freePort;
         return startParams;
   // No port sure debug configuration.
   startParams.debugId = undefined;
   startParams.port = undefined;
   startParams.damage = undefined;
   return Promise.resolve(startParams);

The chosen port is shipped reduction to the client so we know where to scoot:

// [Parsia]: The line that sends reduction the debug port.
   VSBuffer.fromString(JSON.stringify(startParams.port ? {debugPort: startParams.port} :  {}))

// [Parsia]: What the response looks luxuriate in.
{ debugPort: 55001 }

And finally, it calls con.delivery(startParams); in

public async delivery(startParams: IRemoteExtensionHostStartParams):  Promise<void> {
   strive {
      let execArgv: string[] = [];
      if (startParams.port && !(<any>course of).pkg) {
         // [Parsia]: Hear on ``.
         execArgv = [`--inspect${startParams.break ? '-brk' : ''}=${startParams.port}`];

      // [Parsia]: Add the surroundings variables.
      const env =
         predict buildUserEnvironment(
            startParams.env, startParams.language, !!startParams.debugId, this._environmentService, this._logService
      // [Parsia]: This is no longer a security filter.

      const opts = {
         silent: glowing

      // Speed Extension Host as fork of newest course of
      const args = ['--type=extensionHost', `--uriTransformerPath=${uriTransformerPath}`];
      const useHostProxy = this._environmentService.args['use-host-proxy'];
      if (useHostProxy !== undefined) {
      // [Parsia]: Fork it, Potato!
      this._extensionHostProcess = cp.fork(FileAccess.asFileUri('bootstrap-fork', require).fsPath, args, opts);
      const pid = this._extensionHostProcess.pid;
      this._log(`<${pid}> Launched Extension Host Direction of.`);

     // [Parsia]: Removed.

This looks complicated. Let's damage it down:

  1. The Node Inspector occasion will listen on
    1. This is cross. If the user accepts the Home windows firewall dialog, this is able to perhaps be
      externally accessible.
  2. We are able to furthermore inject into the Inspector's surroundings variables.
  3. The removeDangerousEnvVariables blueprint is no longer a security filter
    and real will get rid of DEBUG, DYLD_LIBRARY_PATH, and LD_PRELOAD surroundings
    variables if they exist to forestall crashes.

What is Node Inspector?

It would per chance perhaps well furthermore be used to debug Node processes. There are customers and libraries that
toughen this but in total, I exhaust Chromium's built-in
devoted DevTools for Node (chrome|edge://peep).

After connecting to the Inspector occasion we can delivery up the console and drag
require('child_process').exec('calc.exe');. It in actuality works though we're
in WSL.

The JavaScript within the browser can no longer join to the Inspector occasion. The
client talks to the occasion with one other WebSocket connection. On the opposite hand, we need
to grab the debugger session ID. It's accessible at


The JavaScript within the browser can ship this GET demand but can no longer look the
response due to the SOP (the response doesn't non-public the
Entry-Adjust-Allow-Starting put header). Masses of customers non-public no longer non-public this
limitation and for the reason that inspector is provided externally, we can join to
it from outdoors.

I created a easy proof-of-principle:

  1. Open a net site and enter the port (we can scan for it but it completely's sooner to enter it manually).
  2. The JavaScript within the net site completes the handshake.
    1. I created a Node app (in WSL with Code, lol) with a /attach API to make exhaust of
      the vsda addon.
  3. As rapidly as the Node Inspector occasion is spawned, a 2d API used to be called
    with the debugPort.
  4. A Node app the exhaust of the chrome-far away-interface library connects to
    the Inspector occasion and runs calc.

You would look the provision code at:


Emulate the Code Client

I did no longer scoot this route but I spent a whereas having a gape on the net site traffic. It uses
the same protocol that we saw within the handshake messages. Or no longer it's fully
that it's likely you'll perhaps well presumably deem of to non-public all the pieces when it's likely you'll perhaps well presumably determine the glowing messages and their

The code to present a consumer and join to the server with the protocol is in
the VS Code GitHub repository. Or no longer it'll be a style of reproduction/paste and
resolving. I did no longer exhaust more than about a hours on it.

Let's non-public a recap. We are able to:

  1. Connect to the local WebSocket server from an net thunder or externally.
  2. Full the handshake and order the server to delivery a Node inspector occasion
    on a advise port.
  3. The VS Code server creates the occasion and listens on all interfaces (again).
  4. The VS Code server returns the port for the inspector occasion.
  5. If the machine is available we can join to the Node inspector provider
    from outdoors.
  6. ???
  7. A long way-off code execution.

I created a transient proof-of-principle in ironically WSL far away (lol) the exhaust of Node.
This makes some assumptions:

  1. We've stumbled on the local WebSocket port.
  2. We are able to join to the Node inspector occasion from outdoors.

Or no longer it's furthermore that it's likely you'll perhaps well presumably deem of to non-public initial steps but mimic the Code client and non-public regardless of
we need.


Discovering the local WebSocket port is no longer arduous. It real takes a whereas. Scanning
for local servers from the browser is no longer a new component. The server is furthermore
accessible externally so we're no longer sure by the browser there.

Deep dive into Visual Studio Code extension safety vulnerabilities
is a real helpful resource that talks about same bugs in VS Code extensions.

It talks about scanning local WebSocket ports. Chrome throttling has no raise out
for the reason that WebSocket server needs a webserver to tackle the handshake. I'm
furthermore queer if the WebSocket throttling is a Chrome advise protection or is
part of Chromium (e.g., does Edge non-public it?).

Curiously, Chrome browser has a protection mechanism which prevents a
malicious actor from brute forcing WebSocket ports β€” it begins throttling
after the 10th strive. Unfortunately, this protection would per chance perhaps well furthermore be with out complications bypassed
because every the HTTP and WebSocket servers of the extension are started on
the same port. This could per chance be used to brute drive all that it's likely you'll perhaps well presumably deem of local ports by
checking the presence of a image on a advise localhost port by including an
onload handler to an img designate.

That mentioned, here's a pattern surroundings and the user is presumably developing
within the WSL all day and never closes their browser tabs so chances are we can
procure it if they delivery our net site.

Connecting to the Node inspector occasion is one other matter. We are able to no longer non-public it
from the browser so we need the sufferer's machine to be accessible to our server.
Or no longer it's presumably within the aid of a NAT. But when it's performed on a pattern server we would per chance perhaps well also
non-public a smarter probability.

The 2d exploitation blueprint (emulating the Code client) has none of these
boundaries for the reason that browser can consult with the local server and compose all
actions. It real needs us to reverse engineer the protocol and determine the
glowing messages to ship.

So How Enact We Repair This?

I if truth be told non-public Safety Engineer in my title so I ought to know the blueprint to repair this! RIGHT?!

  1. Construct no longer listen on!
  2. If you occur to uncover a WebSocket pork up demand, ascertain the Starting put header towards
    an allowlist. The Code client sends vscode-file://vscode-app in that header
    so we can exhaust this to delivery.

Fixing one with out the opposite will no longer work. Properly, kinda!

Fixing #2 will forestall net sites from connecting to the WebSocket server because
browsers situation the Starting put header on every horrible-starting put demand (on the least they
ought to, if they non-public no longer you should nod disappointingly on the Chromium team).
But, if the server is exposed externally, then it fixes nothing.

If you occur to repair #1 and never #2, it's better. But net sites can peaceable join to the
server and mess with the dev surroundings. Probabilities are we can non-public RCE during the
surroundings variables.

What Become Truly Mounted?

My ideas were to change the VS Code Server. Actually, I deem they are
better ideas.

As an alternative, the A long way-off WSL extension used to be modified. Now we can no longer ship a bunch of
zeros within the auth demand and we non-public now to non-public a connection token. I did no longer
dig loads but appears to be like luxuriate in now the wslDaemon.js file within the extension creates a
random int and passes it as the connection token.

const p = String(a.randomInt(0xffffffffff)).

  1. Does it repair the components? Yes.
  2. Enact I deem there are other safety components here and we can bypass this? Moreover,
  3. Enact I ought to exhaust more time doing free work for a firm with a 2.5
    TRILLION market cap? Hell, no.

Working instance, the default connection-token for the online browser mode in far away
server is 00000. Discover /sources/server/bin-dev.

// Connection Token
serverArgs.push('--connection-token', '00000');

If you occur to appear a VS Code server talking to an net browser and listening on port
9888 do that connection token. Discover the
The Native Web Server
share beneath to search how to make exhaust of it with the /vscode-far away-helpful resource path to
learn local sources.

I sat on this worm for a month because I desired to reverse engineer the protocol
fully. Now I'm overjoyed I did no longer.

2021-11-22 Reported to MSRC
2021-12-1 Case created
2021-12-10 Triaged
2021-12-13 Out-of-scope for bounty notification
2021-12-15 Repair released. CVE-2021-43907 assigned

I deem here's a real share to non-public. We learn by failing and these techniques
would per chance perhaps well also work in other cases. I will be able to furthermore add any further files that did
no longer make it to the most considerable sections.

Injecting Atmosphere Variables

I would per chance perhaps well also inject surroundings variables (abbreviated to env var within the leisure of this
share) within the Node Inspector course of. I attempted to find RCE that blueprint.

I stumbled on this awesome writeup by MichaΕ‚ Bentkowski or
@SecurityMB a

Be a part of the pack! Be a part of 8000+ others registered users, and find chat, make groups, post updates and make pals spherical the enviornment!



β€œSimplicity, patience, compassion.
These three are your greatest treasures.
Simple in actions and thoughts, you return to the source of being.
Patient with both friends and enemies,
you accord with the way things are.
Compassionate toward yourself,
you reconcile all beings in the world.”
― Lao Tzu, Tao Te Ching