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"