Persistence with VSCode plugin backdoors

30. Jun 2024, #offensive 

It turns out, Visual Studio Code does not check installed plugins for modification after they have been installed. I discovered this by accident when debugging an extension I had been working on.

Charley Célice’s ‘Using Visual Studio Code Extensions for Persistence’↗ touches on the concept of using VSCode for persistence by installing a malicious plugin. I’m going to take this a bit further and show you how you can achieve persistence by modifying existing plugins.

Finding the entry point #

Plugins, by default, are stored in $HOME/.vscode/extensions or $HOME/.vscode-oss/extensions. Each plugin has its own folder which then contains a JSON file named package.json. This file specifies general plugin information and looks like the following:

{
    "name": "go",
    "displayName": "Go",
    "version": "0.42.0",
    "publisher": "golang",
    "description": "Rich Go language support for Visual Studio Code",
    "author": {
        "name": "Go Team at Google"
    },
    "license": "MIT",
    "icon": "media/go-logo-blue.png",
    "categories": [
        "Programming Languages",
        "Snippets",
        "Linters",
        "Debuggers",
        "Formatters",
        "Tes
        ting"
    ],

    ...

    "main": "./dist/goMain.js",
    ...
}

The key we are interested in is called main. Its value is the relative path (from the plugin root) to the JavaScript app that gets executed once the plugin loads. This can be either a path to a .js/.cjs file or a path to a directory that then contains extension.js.

GOLANG.GO-0.42.0-UNIVERSAL
│   .vsixmanifest
│   CHANGELOG.md
│   doc.go
│   LICENSE.txt
│   package.json            <----
│   README.md
├───dist
│       debugAdapter.js
│       goMain.js           <----

...

Adding a backdoor #

Following the path from main, you will find a (usually minified) JavaScript file. Now all you have to do is add the following snippet to the beginning of the JavaScript file. If the JS starts with a (, you’ll need to put it inside those parentheses. Don’t forget to escape the backslashes.

require('child_process').exec('C:\\Windows\\system32\\calc.exe');

That’s all there’s to it. Once the plugin gets loaded, the command will get executed.

Disabled Plugins #

A user can disable plugins. In this case, your backdoor will not be launched upon start. The state of installed plugins can be found in the SQLite database located in $HOME/.config/<VSCODE>/User/globalStorage/state.vscdb.

Using the query below, an array of id and uuid pairs will be returned, with id having the format <PUBLISHER>.<NAME>. Both values can be gathered from the package.json file. If your targeted plugin is on this list, it is disabled.

SELECT value FROM ItemTable WHERE key = "extensionsIdentifiers/disabled"