Blocking Visual Studio Code embedded reverse shell before it's too late

Overview

Visual studio code tunnel

Introduction

Since July 2023, Microsoft is offering the perfect reverse shell, embedded inside Visual Studio Code, a widely used development tool. With just a few clicks, any user with a github account can share their visual studio desktop on the web. VS code tunnel is almost considered a lolbin (Living Of the Land Binary).

I am so glad that my users now have the ability to expose their computer with highly sensitive data right on the web, through an authentication I nor control, nor supervise. My internal network is now accessible from anywhere !

The worse part is that this tunnel can be triggered from the cmdline with the portable version of code.exe. An attacker just has to upload the binary, which won't be detected by any anti-virus since it is legitimate and singed windows binary.

It is therefore something to watch out for.

Execute it, open the link mentioned, log in to your github account and there is your reverse shell.

 1.\code.exe tunnel
 2*
 3* Visual Studio Code Server
 4*
 5* By using the software, you agree to
 6* the Visual Studio Code Server License Terms (https://aka.ms/vscode-server-license) and 
 7* the Microsoft Privacy Statement (https://privacy.microsoft.com/en-US/privacystatement).
 8*
 9
10Open this link in your browser https://vscode.dev/tunnel/sauceTomate/C:/Users/ipfyx/Downloads/vscode_cli_win32_x64_cli

The binary is definitely signed :

1Get-AppLockerFileInformation 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel.exe'|Select-Object -ExpandProperty Publisher
2PublisherName    : O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US
3ProductName      :
4BinaryName       :
5BinaryVersion    : 0.0.0.0
6HasPublisherName : True
7HasProductName   : False
8HasBinaryName    : False

Notice how there are nor ProductName nor BinaryName value, we will use them later.

pfiatde blog post already did a great job at describing what an attacker can do so I won't say much more. Go check his awesome blog post.

Mitigation

But now, as a defender, how do we block or detect its usage ?

Domains blacklist

Microsoft documentation states :

If you're part of an organization who wants to control access to Remote Tunnels, you can do so by allowing or denying access to the domain global.rel.tunnels.api.visualstudio.com.

Yeah it's not that simple. It will indeed block most users but, from my testing, it won't block an attacker that has already established a tunnel once. Meaning the tunnel can be kept active or restarted at will despite this domain being blacklisted.

From my understanding, VScode contacts global.rel.tunnels.api.visualstudio.com to get its "clusters" : https://global.rel.tunnels.api.visualstudio.com/api/v1/clusters

Here is a sample of the resulting json :

 1[
 2  {
 3    "clusterId": "auc1",
 4    "uri": "https://auc1.rel.tunnels.api.visualstudio.com",
 5    "azureLocation": "AustraliaCentral"
 6  },
 7  {
 8    "clusterId": "aue",
 9    "uri": "https://aue.rel.tunnels.api.visualstudio.com",
10    "azureLocation": "AustraliaEast"
11  },
12  etc.
13]

Those domains are also mentioned in another Microsoft documentation :

1- Dev Tunnels
2  - global.rel.tunnels.api.visualstudio.com
3  - [clusterId].rel.tunnels.api.visualstudio.com
4  - [clusterId]-data.rel.tunnels.api.visualstudio.com
5  - *.[clusterId].devtunnels.ms
6  - *.devtunnels.ms

Blocking those domains will block and cut off any VS code tunnel :

1*.tunnels.api.visualstudio.com
2*.devtunnels.ms

Applocker

Applocker is Microsoft application whitelisting technology. When enabled and configured, with default rules for example, everything is blocked by default except for the executables and scripts defined in the preceding rules. Let's pretend we use Microsoft default rules for demo purpose.

To generate them, in Group Policy Management Editor, go to Computer Configuration -> Windows Settings -> Security Settings -> Application Control Policies -> AppLocker. Right Click on Create Default Rules.

Then right click on AppLocker and Export Policy.

MS Applocker default rules generation
MS Applocker default rules generation

Here is the resulting xml :

 1<AppLockerPolicy Version="1">
 2  <RuleCollection Type="Appx" EnforcementMode="Enabled" />
 3  <RuleCollection Type="Dll" EnforcementMode="NotConfigured" />
 4  <RuleCollection Type="Exe" EnforcementMode="Enabled">
 5    <FilePathRule Id="921cc481-6e17-4653-8f75-050b80acca20" Name="(Default Rule) All files located in the Program Files folder" Description="Allows members of the Everyone group to run applications that are located in the Program Files folder." UserOrGroupSid="S-1-1-0" Action="Allow">
 6      <Conditions>
 7        <FilePathCondition Path="%PROGRAMFILES%\*" />
 8      </Conditions>
 9    </FilePathRule>
10    <FilePathRule Id="a61c8b2c-a319-4cd0-9690-d2177cad7b51" Name="(Default Rule) All files located in the Windows folder" Description="Allows members of the Everyone group to run applications that are located in the Windows folder." UserOrGroupSid="S-1-1-0" Action="Allow">
11      <Conditions>
12        <FilePathCondition Path="%WINDIR%\*" />
13      </Conditions>
14    </FilePathRule>
15    <FilePathRule Id="fd686d83-a829-4351-8ff4-27c7de5755d2" Name="(Default Rule) All files" Description="Allows members of the local Administrators group to run all applications." UserOrGroupSid="S-1-5-32-544" Action="Allow">
16      <Conditions>
17        <FilePathCondition Path="*" />
18      </Conditions>
19    </FilePathRule>
20  </RuleCollection>
21  <RuleCollection Type="Msi" EnforcementMode="NotConfigured" />
22  <RuleCollection Type="Script" EnforcementMode="Enabled" />
23</AppLockerPolicy>

When using those rules, everything that is in Program Files and Windows is allowed, everything else is blocked. Administrators are allowed to execute anything, VSCode for instance. But if you were to define a rule that blocks VSCode, this rule would apply first, since "Deny rules" are applied before "Allowed rules".

Let's build a rule to block VScode altogether. The rule applies to Everyone (SID S-1-1-0), therefore to Administrators (SID S-1-5-32-544).

1Get-AppLockerFileInformation 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\Code.exe'| New-AppLockerPolicy -RuleType Publisher -User S-1-1-0 -Optimize -Xml

In the resulting xml, I replaced Action="Allow" with Action="Deny". I also removed my current VSCode version from BinaryVersionRange to match any VScode version.

Warning : Do not import the following xml in Applocker. Imported alone, this rule will completely block your system. Add a Allow * rule beforehand.

 1<AppLockerPolicy Version="1">
 2	<RuleCollection Type="Exe" EnforcementMode="NotConfigured">
 3		<FilePublisherRule Id="1dd70b30-eb06-4220-b808-bd8d368624c0" Name="VScode" Description="" UserOrGroupSid="S-1-5-21-1935059010-3323107334-3352698520-500" Action="Deny">
 4			<Conditions>
 5				<FilePublisherCondition PublisherName="O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" ProductName="VISUAL STUDIO CODE" BinaryName="*">
 6					<BinaryVersionRange LowSection="*" HighSection="*" />
 7				</FilePublisherCondition>
 8			</Conditions>
 9		</FilePublisherRule>
10	</RuleCollection>
11</AppLockerPolicy>

Included in MS default rules, here is the resulting xml (called vscode.xml from now on) :

 1<AppLockerPolicy Version="1">
 2  <RuleCollection Type="Appx" EnforcementMode="Enabled" />
 3  <RuleCollection Type="Dll" EnforcementMode="NotConfigured" />
 4  <RuleCollection Type="Exe" EnforcementMode="Enabled">
 5    <FilePathRule Id="921cc481-6e17-4653-8f75-050b80acca20" Name="(Default Rule) All files located in the Program Files folder" Description="Allows members of the Everyone group to run applications that are located in the Program Files folder." UserOrGroupSid="S-1-1-0" Action="Allow">
 6      <Conditions>
 7        <FilePathCondition Path="%PROGRAMFILES%\*" />
 8      </Conditions>
 9    </FilePathRule>
10    <FilePathRule Id="a61c8b2c-a319-4cd0-9690-d2177cad7b51" Name="(Default Rule) All files located in the Windows folder" Description="Allows members of the Everyone group to run applications that are located in the Windows folder." UserOrGroupSid="S-1-1-0" Action="Allow">
11      <Conditions>
12        <FilePathCondition Path="%WINDIR%\*" />
13      </Conditions>
14    </FilePathRule>
15    <FilePathRule Id="fd686d83-a829-4351-8ff4-27c7de5755d2" Name="(Default Rule) All files" Description="Allows members of the local Administrators group to run all applications." UserOrGroupSid="S-1-5-32-544" Action="Allow">
16      <Conditions>
17        <FilePathCondition Path="*" />
18      </Conditions>
19    </FilePathRule>
20		<FilePublisherRule Id="1dd70b30-eb06-4220-b808-bd8d368624c0" Name="VScode" Description="" UserOrGroupSid="S-1-5-21-1935059010-3323107334-3352698520-500" Action="Deny">
21			<Conditions>
22				<FilePublisherCondition PublisherName="O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US" ProductName="VISUAL STUDIO CODE" BinaryName="*">
23					<BinaryVersionRange LowSection="*" HighSection="*" />
24				</FilePublisherCondition>
25			</Conditions>
26		</FilePublisherRule>
27  </RuleCollection>
28  <RuleCollection Type="Msi" EnforcementMode="NotConfigured" />
29  <RuleCollection Type="Script" EnforcementMode="Enabled" />
30</AppLockerPolicy>

Unfortunately, this rule won't apply to code tunnel because this binary doesn't have a product name (thanks Microsoft !) :

1Get-AppLockerFileInformation 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel.exe' | New-AppLockerPolicy -RuleType Publisher -User S-1-1-0 -Optimize -Xml                           
2New-AppLockerPolicy : Les règles ne peuvent pas être créées. Les informations de fichier requises sont absentes dans le fichier suivant:
3%OSDRIVE%\USERS\ipfyx\DOWNLOADS\VSCODE_CLI_WIN32_X64_CLI\CODE.EXE

To be sure, let's test the previously generated vscode rule.

 1# Portable code tunnel is blocked but not by the rule
 2Test-AppLockerPolicy -XmlPolicy .\vscode.xml -Path .\code.exe -User S-1-1-0|fl
 3FilePath       : C:\Users\ipfyx\Downloads\vscode_cli_win32_x64_cli\code.exe
 4PolicyDecision : DeniedByDefault
 5MatchingRule   :
 6
 7# Code tunnel is blocked but not by the rule
 8Test-AppLockerPolicy -XmlPolicy .\vscode.xml -Path 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel.exe' -User S-1-1-0|fl
 9FilePath       : C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel.exe
10PolicyDecision : DeniedByDefault
11MatchingRule   :
12
13# VScode is blocked by the rule
14Test-AppLockerPolicy -XmlPolicy .\vscode.xml -Path 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\Code.exe' -User S-1-1-0|fl
15FilePath       : C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\Code.exe
16PolicyDecision : Denied
17MatchingRule   : VScode

The PolicyDecision is DeniedByDefault, which means the rule we just built from the vscode binary does not apply to code-tunnel. If no rules matches, the default behaviour for applocker is to deny any execution. Code-tunnel is denied there, but it would not be if it was placed in the allowed directories. It could be placed there by an attacker or by a legitimate vscode, installed by an admin.

A solution could be to use a Hash Condition.

1# Export
2Get-AppLockerFileInformation .\code.exe | New-AppLockerPolicy -RuleType Hash -User S-1-1-0 -Optimize -Xml > .\vscode-tunnel-hash.xml
3
4# Code tunnel is blocked
5Test-AppLockerPolicy -XmlPolicy .\vscode-tunnel-hash.xml -Path 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel.exe' -User S-1-1-0|fl
6FilePath       : C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel.exe
7PolicyDecision : Denied
8MatchingRule   : code.exe

Warning : Do not import the following xml in Applocker. Imported alone, this rule will completely block your system. Add a Allow * rule beforehand.

 1<AppLockerPolicy Version="1">
 2	<RuleCollection Type="Exe" EnforcementMode="NotConfigured">
 3		<FileHashRule Id="15b4ef38-5f18-484b-bc83-e03d24076a0d" Name="code.exe" Description="" UserOrGroupSid="S-1-1-0" Action="Deny">
 4			<Conditions>
 5				<FileHashCondition>
 6					<FileHash Type="SHA256" Data="0xAC60D7CA817ED4BEF562D07DEB4BE730413BD23B3ABFE0DFC78B1DDC866F85BA" SourceFileName="code.exe" SourceFileLength="16738736" />
 7				</FileHashCondition>
 8			</Conditions>
 9		</FileHashRule>
10	</RuleCollection>
11</AppLockerPolicy>

But this solution is nor resilient nor sustainable since the hash can change at the first update.

GPO

Visual Studio has the so wanted features :

1- Dev Tunnels - controls test functionality

But as of today (1.8.82), VScode doesn't. You can force automatic updates though ! UpdateMode_default would be my goto.

1gc 'C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\policies\en-us\VSCode.adml'
 1<?xml version="1.0" encoding="utf-8"?>
 2<policyDefinitionResources revision="1.0" schemaVersion="1.0">
 3        <displayName />
 4        <description />
 5        <resources>
 6                <stringTable>
 7                        <string id="Application">Visual Studio Code</string>
 8                        <string id="Supported_1_67">Visual Studio Code &gt;= 1.67</string>
 9                        <string id="Category_updateConfigurationTitle">Update</string>
10                        <string id="UpdateMode">UpdateMode</string>
11                        <string id="UpdateMode_updateMode">Configure whether you receive automatic updates. Requires a restart after change. The updates are fetched from a Microsoft online service.</string>
12                        <string id="UpdateMode_none">Disable updates.</string>
13                        <string id="UpdateMode_manual">Disable automatic background update checks. Updates will be available if you manually check for updates.</string>
14                        <string id="UpdateMode_start">Check for updates only on startup. Disable automatic background update checks.</string>
15                        <string id="UpdateMode_default">Enable automatic update checks. Code will check for updates automatically and periodically.</string>
16                </stringTable>
17                <presentationTable>
18                        <presentation id="UpdateMode"><dropdownList refId="UpdateMode" /></presentation>
19                </presentationTable>
20        </resources>
21</policyDefinitionResources>

Detection

Process

Looking for code-tunnel execution

A simple detection could be :

1index=win sourcetype="XmlWinEventLog" EventCode=4688 code tunnel cmdline="*code*.exe*tunnel*"
2| stats min(_time) as time_min max(_time) as time_max count as occurence values(NewProcess) as NewProcess values(ParentProcess) as ParentProcess values(CommandLine) as CommandLine by host, SubjectUser file_name
host SubjectUser cmdline file_name time_min time_max occurence NewProcess ParentProcess
sauteTomate ipfyx "c:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel" tunnel --accept-server-license-terms --name totallyNotSuspicious code-tunnel.exe 2023-09-18 11:31:54 2023-09-18 11:31:54 1 C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\code-tunnel.exe C:\Windows\System32\cmd.exe

But the binary code.exe could be renamed. However, what an attacker can't change are the tunnel and --accept-server-license-terms options :

1index=win sourcetype="XmlWinEventLog" EventCode=4688 tunnel accept server license terms cmdline="*.exe*tunnel*--accept-server-license-terms*"
2| stats min(_time) as time_min max(_time) as time_max count as occurence values(NewProcess) as NewProcess values(ParentProcess) as ParentProcess values(CommandLine) as CommandLine by host, SubjectUser file_name
host SubjectUser cmdline file_name time_min time_max occurence NewProcess ParentProcess
sauteTomate ipfyx "c:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\c0de.exe" tunnel --accept-server-license-terms --name totallyNotSuspicious c0de.exe 2023-09-18 11:35:54 2023-09-18 11:35:54 1 C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\c0de.exe C:\Windows\System32\cmd.exe
sauteTomate ipfyx "c:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\code-tunnel" tunnel --accept-server-license-terms --name totallyNotSuspicious code-tunnel.exe 2023-09-18 11:31:54 2023-09-18 11:31:54 1 C:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\code-tunnel.exe C:\Windows\System32\cmd.exe

If you use sysmon, you can switch to EventCode=1 and Sysmon sourcetype instead of EventCode 4688.

Looking for suspicious child process

Great idea from lobas :

1IOC: Process tree: code.exe -> cmd.exe -> node.exe -> winpty-agent.exe

A straightforward search would be :

1index=win sourcetype="XmlWinEventLog" EventCode=4688 code (cmd OR powershell) ParentProcess="*code-tunnel.exe" NewProcess IN ("*cmd*", "*powershell*")
2| table _time, host, NewProcess, ParentProcess

But once again, code.exe could be renamed

We could use the search above to look for suspicious child process from our process with accept-server-license-terms option :

1index=win sourcetype="XmlWinEventLog" EventCode=4688 (cmd OR powershell) ParentProcess="*code*.exe" NewProcess IN ("*cmd*", "*powershell*")
2    [search index=win sourcetype="XmlWinEventLog" EventCode=4688 tunnel accept server license terms CommandLine="*.exe*tunnel*--accept-server-license-terms*"
3    | table host NewProcessId
4    | rename NewProcessId as ParentProcessId
5    | format]
6| table _time, host, NewProcess, ParentProcess
_time host NewProcess ParentProcess
2023-09-18 11:36:21 sauteTomate C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe c:\Users\ipfyx\AppData\Local\Programs\Microsoft VS Code\bin\c0de.exe

EDIT 20230924 : license terms agreement is not mandatory. And even if it were, this could be bypass by creating the file license_content.json with the right content (see below). All we have left is detecting the string "*.exe*tunnel*" in a commandline, which is subject to false positive.

Applocker

EDIT 20230924

Since we defined some Applocker rules, we got some log from it ! We could just search for that weird publisher value :

1index=win sourcetype="XmlWinEventLog" EventCode=8004 event_provider="Applocker" "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US\\\\\\0.0.0.00"
2| table _time, host, FilePath, PolicyName, RuleName Fqbn
_time host FilePath PolicyName RuleName Fqbn
2023-09-18 11:35:54 sauteTomate %OSDRIVE%\USERS\IPFYX\APPDATA\LOCAL\PROGRAMS\MICROSOFT VS CODE\BIN\CODE.EXE EXE O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US\\0.0.0.00

Got you ! There should not be that many binary signed without ProductName right ? Let's count them quickly in System32 for example :

1gci -Recurse C:\Windows\System32 -include *.exe |Get-AppLockerFileInformation -ErrorAction SilentlyContinue |Where-Object {$_.Publisher -ne $null -and $_.Publisher.ToString() -eq "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US\\,0.0.0.0"}|Measure-Object
2
3Count    : 20

Sigh. 20 binary names to masquerade as...

File creation

Another great idea from lobas :

IOC: File write of code_tunnel.json which is parametizable, but defaults to: %UserProfile%\.vscode-cli\code_tunnel.json

license_consent.json file could also be watched.

1PS > gc C:\Users\ipfyx\.vscode\cli\code_tunnel.json
2{"name":"sauceTomate","id":"9s43zAc9","cluster":"uks1"}
3PS > gc C:\Users\ipfyx\.vscode\cli\license_consent.json
4{"consented":true}

I do not have sysmon EventCode 11 yet, so I will leave the SPL search as an exercise for the user. Watching for those files creation inside the UserProfile directory is not enough since it can be changed with the --cli-data-dir option.

1.\code.exe tunnel help
2...
3GLOBAL OPTIONS:
4      --cli-data-dir <CLI_DATA_DIR>  Directory where CLI metadata should be stored [env: VSCODE_CLI_DATA_DIR=]

Web traffic monitoring

If you haven't blocked the domains I mentioned earlier, watch out for any HTTP traffic toward those domains.

Conclusion

A GPO parameter would be awesome but it is yet to be seen. An Applocker hash rule is not sustainable. Moreover, your non-domain-joined computers would not be concerned by neither of them. Therefore, for now, we are only left to blocking those two bad boys :

1*.tunnels.api.visualstudio.com
2*.devtunnels.ms