Apr 2 2024 |
Getting Intune with Bugs and Tokens: A Journey Through EPM
Written by Zach Stein & Duane Michael
SpecterOps Hackathon
Back in January, SpecterOps held our annual hackathon event, loosely based on Atlassian’s “FedEx Day” (now called “ShipIt Day”). The gist of the event was a bunch of hackers hanging out for a few days and presenting their results. At the event’s start, Specters broke into teams to work on novel or interesting problems or projects (i.e., technical problems unrelated to business or customers). They had 24 hours to work on the problem and present their results (i.e., deliver overnight, hence “FedEx day”). The results could be proof-of-concept code, discussion of findings, blog, etc. Fueled by pizza and beer, 40+ hackers descended upon the SpecterOps office in Seattle to collaborate on whatever they found interesting — and the results were awesome!
This blog post highlights one team’s journey through the hackathon event and the follow-on analysis work and disclosure process, with emphasis on “journey.” We will provide background information, an explanation of our understanding of how parts of the product work, a simple privilege escalation bug, our experience reporting the bug to MSRC, and then an analysis of Microsoft’s patch.
Our team was comprised of Zach Stein, Duane Michael, Garrett Foster, and Chris Thompson.
If you know us, you know we like sniffing around endpoint management platforms. Therefore, we chose to poke at Microsoft Intune, specifically the relatively new Endpoint Privilege Management feature.
What is Microsoft Intune?
Microsoft Intune is a cloud-based endpoint management and mobile device management (MDM) platform. First released in 2010, it has evolved over the years and can complement other platforms, such as Microsoft Configuration Manager, for a co-management model or full replacement. From a security perspective, Intune intrigued us for the same reasons Configuration Manager did. It’s a system that controls the fleet. If we can control that, then maybe we can control the fleet!
What is Endpoint Privilege Management?
Microsoft Intune has several optional add-on features that require additional licensing. One such feature is “Endpoint Privilege Management (EPM),” which allows privileged or elevated execution of allowed programs without the need for an administrator account. Sounds interesting, right? We thought so, too!
How Does it Work?
EPM deploys to Intune-managed Windows devices. The deployment contains multiple files and directories, many of which we won’t cover in this blog. EPM installs a Windows service and mini-filter driver, among other things.
From the Intune portal, a user can create a policy allowing a process on the client device to run elevated based on various constraints, such as file name and hash. We can also specify the “elevation type” setting, which describes the desired end-user behavior. In our example, it is set to “Automatic,” but it could also be set to the recommended “Confirm” option, which would require the end-user to click a UI dialog to confirm the elevation. “Automatic” will allow the specified program to run elevated without prompting the user.
Next, we can specify the “Child process behavior” setting, which describes how child processes of our elevated process will behave. There are three options here:
1. Require rule to elevate — The child process must have its own rule configured in order to run elevated
2. Deny all — The child process must not run elevated
3. Allow the child process to run elevated — The child process will run elevated
Configuration
First, we require a Microsoft Intune subscription with an EPM license and Intune add-ons enabled. The device must be joined to Entra ID, enrolled in Intune, and part of a group to which EPM policies are deployed. The logged-in user is an Entra account that is NOT a member of the “Administrators” group on the device.
Next, we create a rule to allow a specific application to execute elevated on the applied device(s). Here, we used the file hash from the test device to create a rule for notepad.exe. The “Elevation type” is set to “automatic,” so no user interaction is required. “Child process behavior” is set to “Deny all,” so all child processes spawned from notepad.exe will not run as elevated.
To summarize, the above configuration should allow a non-admin user to run notepad.exe as a high-integrity process automatically (i.e., no UI prompt) without an administrator present, and all child processes should run non-elevated.
Expected Behavior
Here, we outline the expected behavior for spawning the application that the policy and follow-on child process creation events allowed. The applied policy should allow any user on affected devices to run notepad.exe elevated. The policy explicitly denies all child processes of notepad.exe from spawning elevated.
A standard Entra account should be able to right-click on notepad.exe and select “Run with elevated access,” which should trigger the EPM rule. If the rule’s “Elevation type” setting is set to “Automatic,” this right-click behavior is not required and the application will auto-elevate when a user executes it.
EPM will evaluate the logged-on user’s token, determine that it is not an administrator, impersonate an elevated token the EPM service created for the virtual account, and then execute notepad.exe in an elevated context. Per the EPM policy, any child processes of notepad.exe should be spawned in a medium-integrity context.
Upon running notepad.exe elevated, we can inspect its token to find that it runs as the virtual account MEMAzureAD_ChrisCAPTest_$ in high-integrity.
Next, we validate the child process behavior for the rule, which is set to “Deny All.” Within notepad.exe, we can navigate to “File” -> “Open…” Here, we’re presented with a file explorer dialog. We can spawn arbitrary processes from the path bar, such as powershell.exe.
This newly created powershell.exe then spawns as a child process under our elevated notepad.exe process.
Upon inspection of the powershell.exe child process’s token, we find that it spawned as the virtual account in a medium integrity context, as intended.
EPM Analysis
So, let’s start digging in to understand how this product works…
First, let’s explore the file system. Within Program Files, a Microsoft EPM Agent directory with several interesting subdirectories is created.
All directories, except Logs, are restricted to an admin user. This is interesting because EPM’s logs are extremely verbose! This is helpful to understand the internals.
Next, we enumerate the services on the host and find two EPM-related services in the registry (i.e., HKLMSYSTEMCurrentControlSetServices). The first is MEMEPMAgent, which is the mini-filter driver service and beyond the scope of this blog post. The second is Microsoft EPM Agent Service, which handles the userland functionality that we’re interested in. We see this correlates to the EPMService directory from the previous figure and the service runs as LocalSystem.
We’ll use Process Hacker to inspect the EpmService.exe process. We find the process running as NT AUTHORITYSYSTEM with the .NET CLR loaded — Perfect for quick and simple analysis! We can obtain a decent starting point by inspecting the .NET assemblies and modules that EpmService.exe loaded.
Here, we find some interesting EPM-related DLLs loaded. Most of them are .NET assemblies. Note the EpmInterop.dll, which is a native DLL that will be important later in the blog.
Before diving into the static analysis, let’s use Process Monitor to analyze the behavior associated with the notepad.exe process creation with the EPM policy applied. Quite a few things happen…
First, a few things to note: notepad.exe launched in a medium integrity context as the AzureADChrisCAPTest account with PID 8860 and exits almost immediately. This is our standard logged-on user account.
Immediately after the Process Start and Thread Create operations, notepad.exe loads a program called EpmClientStub.exe. It appears that EPM is hooking or modifying process creations.
EpmClientStub.exe is a native binary that does “stuff.” We don’t cover this component in the blog (but maybe we will in a follow-up!), but how does notepad.exe load it? Well, from our analysis of the Windows services, we know that the MEMEPMAgent loads a mini-filter driver. Within the EPMDriver folder, we find several files: a driver setup INF file (required to install mini-filter drivers), a security catalog, and the driver itself. We find the same information from the registry key if we view the INF file.
Since we’re not mini-filter driver reverse engineers, we’ll hand-wave this for the purposes of this blog. Again, we hope to revisit the internals in a later post!
So, what else happens after the notepad.exe process is created? The notepad process that we launched exits, followed by the EpmServiceStub.exe launching. This sounds interesting and will be revisited later.
Remember that EpmInterop.dll we saw earlier? Well, here it is again, this time getting loaded by the service stub.
After the service stub does its thing, we see a new process creation for notepad.exe (PID 8840). This new notepad process launched in a high-integrity context as the MEMAzureAD_ChrisCAPTest_$ virtual account.
Time out! Virtual account? What’s that? Let’s take a step back and dig into that. Virtual accounts are managed local accounts designed to simplify administration overhead. They do not require password management and the accounts are automatically managed. The account name takes the form of <domain><account>$.
Rudy Ooms has a great blog post that specifically covers the EPM virtual account, which we found to be a great resource. James Forshaw also documents how to create virtual accounts on his blog. When the EPM service is installed, a virtual account is created. This account will have a fixed virtual domain called “MEM”, a domain group called “MEM”, and an account name of “<domain_loggedOnUser>_$”. That is, AzureADChrisCAPTest becomes MEMAzureAD_ChrisCAPTest_$. Now, we have a domain group on our pure Entra-joined device. Weird!
EPM calls two primary functions to create the virtual account: LsaManageSidNameMapping and LogonUserExExW. EPM will call LogonUserExExW to create an access token for the virtual account; however, this function requires strings for the domain and username. LsaManageSidNameMapping is used to map the domain and username to a virtual account SID, S-1–5–110, in this case.
There are several interesting things to note in the LogonUserExExW call. Let’s compare the function prototype with our disassembly pseudocode:
- lpszUsername is the username for the virtual account
- lpszDomain is the fixed virtual domain name, MEM
- lpszPassword is the password, which is null
- dwLogonType is the type of Windows logon, which is set to “2” for “Interactive”
- phToken is a handle to the newly created token, which can then be passed to calls to ImpersonateLoggedOnUser (more later)
As previously discussed, EPM also controls the creation of any child process. However, the flow varies from the creation of the parent process because the service stub is not called. Therefore, the service stub is absent from the creation of the child process.
However, the process is similar in that the child process is created in the current integrity (high), exited (likely controlled by the mini-filter driver), and then respawned in the desired integrity (medium).
Within EpmInterop!CreateProcessAsUserEx, there’s a call to ImpersonateLoggedOnUser. Let’s investigate that more.
If we attach API Monitor to EpmService and launch Notepad, we can key in on this behavior. We’re filtering on the EpmInterop.dll module to reduce the noise. We can also view EpmService’s token handles to better understand the API monitor output.
Next, we find a DuplicateTokenEx call where the virtual account’s token (0x7a4) is duplicated to 0xbb8.
The token’s existing access is requested with an impersonation level of “SecurityImpersonation”, and the duplicate token is an impersonation token.
Finally, the new process is created through a CreateProcessAsUserW call with the duplicate token. The resulting process will run in the context of the user associated with the token; in this case, it’s the virtual account. The lpCommandLine parameter will look familiar when we analyze the service stub in the next section.
Discovery of a Simple Bug
It’s clear that EpmServiceStub.exe is the process responsible for the elevation, so let’s start digging there. We’ll start by analyzing the assembly in dnSpy. There’s not much to analyze here: the Program class with its Main(string[]) function and the NativeMethods class with an intriguing RunAsAdmin(string, string, string) function.
The main function is straightforward: It parses command-line arguments and writes logs and errors. If all the checks pass, it calls RunAsAdmin(targetProgram, targetProgramDirectory, parameters).
So, what is this RunAsAdmin function, anyway? The log and error messages indicate that the function likely wraps ShellExecuteEx. The function is imported from EpmInterop.dll.
With the current policy configuration, we know we can spawn notepad.exe in a high-integrity context and any child processes should not be elevated. We can use NtObjectManager and Token Viewer to inspect and validate this further. As previously shown, we’ll create a PowerShell child process through Explorer.
Upon inspection of the child process’ token, we see that it has a primary token and runs as the MEMAzureAD_ChrisCAPTest_$ virtual account in medium integrity. We also see the “Elevation Type” field is set to “Limited”, which means this token has a linked token (split token) for UAC purposes.
A linked token, also known as a “split token,” is a security feature that was introduced with user account control (UAC) to handle elevated privileges for administrative users. When an administrative user logs in, two logon sessions are created: one with medium integrity and one with high integrity. A linked token connects these sessions. For example, when an administrative user needs to perform an action that requires elevated privileges, a UAC prompt will display. When the UAC prompt is approved, the process is launched using the linked high integrity token.
Now, we’ll inspect the linked token to verify. We find that the linked token is, indeed, an elevated and high-integrity token running in the context of the virtual account.
We know virtual accounts are passwordless by design. Therefore, if we can prompt UAC, it should be a simple “Yes/No” prompt without a password, even if it is configured to require one.
Before testing the hypothesis, let’s summarize what we know at this point:
1. We can run an allow-listed program with administrative privileges as a virtual account
2. The children of the allow-listed process should run unelevated
3. The token of the child process has a linked split administrator token
4. Virtual accounts are passwordless
Let’s take this one step further and launch another child process (grandchild process) from the newly created powershell.exe process. This process runs as a virtual account with medium integrity. We can specify the “RunAs” verb for the process creation and escalate the virtual account to a high-integrity token.
The graphical UAC prompt triggers, as expected.
After approving the UAC prompt, our new powershell.exe process spawns. Again, we verify the user context and token and find that it is high-integrity.
We have successfully run an arbitrary application (i.e., powershell.exe) as an administrator without an administrator being present.
Root Cause Analysis
The root cause of the bug lies in how the virtual account token was created. EpmInterop!CreateAutomaticAdminAccount is responsible for this. Let’s take a look.
EpmService imports a function called CreateVirtualAccountEx which maps to EpmInterop!CreateAutomaticAdminAccount. The adminToken pointer is aptly named, as this is the token on which the entire bug is built.
Pivoting to IDA, we find that CreateAutomaticAdminAccount has a lot going on. It creates the virtual account and sets up several security identifiers (SIDs), including a logon SID, a local SID, Administrators SID, and an EPM SID. These SIDs are added to the pTokenGroups TOKEN_GROUPS structure then passed to LogonUserExExW.
The resulting virtual account primary token is a member of the Administrators group. Thus, we could elevate from medium to high integrity as the virtual account.
MSRC Responsible Disclosure
We reported this bug to Microsoft on January 16, 2024. They quickly acknowledged and confirmed the bug and developed a fix. We debated whether it should be reported as a “security feature bypass,” as we effectively escaped a security boundary, or a privilege escalation because we effectively escaped a security boundary. We chose to submit it as a security feature bypass because we expected it to be taken more seriously. Ultimately, Microsoft assessed the bug as an “important” severity privilege escalation. Despite their assessment, they did not grant us a CVE. Below is a timeline of events.
- January 16, 2024 — Report submitted
- January 18, 2024 — Status changed from “New” to “Review/Repro”
- January 24, 2024 — MSRC confirmed the reported behavior
- January 24, 2024 — Status changed from “Review/Repro” to “Develop”
- January 24, 2024 — Submitted to the bounty team for consideration
- February 14, 2024 — Status inquiry
- February 25, 2024 — Status changed from “Develop” to “Complete”
- February 25, 2024 — SpecterOps asked MSRC to validate their patch
- February 26, 2024 — Inquiry about CVE and acknowledgment
- February 29, 2024 — Notification of no CVE
- February 29, 2024 — Request to review this blog
- February 29, 2024 — MSRC Online Services acknowledgement
- March 6, 2024 — Notification of no bounty (Intune is out of scope)
- March 6, 2024 — Notification that the vulnerability was assessed as “important” severity privilege escalation
- March 6, 2024 — Inquired why an important privilege escalation did not warrant a CVE
- March 12, 2024 — Unrelated Intune agent privilege escalation granted CVE
- March 15, 2024 — Repeated inquiry
- March 20, 2024 — Case manager agrees to investigate the rationale
Patch Analysis
After the disclosure, we returned to our EPM research to learn more about how it works under the hood. We created a new rule to allow cmd.exe to run elevated but deny all child processes from running elevated. However, this time, we noticed interesting behavior. We repeated the same steps of spawning an authorized process (i.e., cmd.exe) as administrator, launching a child process (i.e., powershell.exe), and attempting to run a program as administrator to display the “Yes/No” UAC prompt to elevate ourselves; but, interestingly, we were prompted for a password. This indicates that we no longer had an admin token.
To discover what had changed, we began analyzing the token for the medium-integrity child process, comparing it to our previous findings. We noticed certain group memberships had been removed. Notably, the BUILTINAdministrators group membership was missing.
Below are the original token groups:.
Below are the newly observed group memberships:
Using NtObjectManager’s Show-NtToken -All command, we analyzed the token information for both the high-integrity (i.e., cmd.exe) and medium-integrity (i.e., powershell.exe) processes. Of note, we observed that the new medium-integrity token Elevation Type is set to “Default” and no linked token is observed.
Analyzing the tokens was a good start, but to really understand what was happening, we needed to re-examine the service’s internals, specifically EpmInterop.dll.
As we still had the old EPM files from before our disclosure, we used BinDiff to compare the EpmInterop.dll binary from the original research with the new EpmInterop.dll. Overall, they were nearly identical outside of two functions.
Analyzing the call graph, these were two new functions that did not exist in the original EpmInterop.dll.
To figure out what these new functions were doing, we opened the new EpmInterop.dll within IDA and went to the associated memory addresses. The 180005CC0 address appeared to be associated with CreateRestrictedTokenGroups.
The 180005650 address appeared to be associated with CreateAdminTokenGroups.
Analyzing the cross-references to these functions, the CreateAutomaticAdminAccount function called both
Opening EpmService.exe within dnSpy once more, we could see that the CreateAutomaticAdminAccount function contained a new parameter called restrictedToken.
If you remember earlier in our blog, when analyzing the old EpmService.exe and CreateAutomaticAdminAccount, the restrictedToken parameter did not exist.
After creating the new restricted token groups, EPM called LogonUserExExW. As we know, this function logs a user onto the local computer and returns a handle to a token representing the logged-on user.
Interestingly, this function was now called twice. The first call was with admin groups/tokens, and the second with restricted groups/tokens introduced by the new functions. As LogonUserExExW returns a handle to a token, we could infer that instead of using the split token architecture from our pre-disclosure research, EPM appeared to have been updated to create two separate tokens altogether.
To further confirm our hypothesis, we analyzed the original EpmInterop.dll in the same location and saw only the single LogonUserExExW call with just the AdminToken.
In Closing
Research is a journey and, like all things in life, it’s all about the journey; not the destination.
We enjoyed the journey of finding this bug and researching EPM’s functionality. We hope others find this discovery process useful or insightful, as we believe the journey to discovery is just as important as the outcome.
Despite a frustrating disclosure process, we’re thankful Microsoft fixed this privilege escalation bug so quickly. We look forward to doing more research on Intune and its various features.
Please reach out to us on X or BloodHound Slack with any questions or feedback!
Duane Michael — X, GitHub, @ subat0mik on Slack
Zach Stein — X, GitHub, @Zach Stein on Slack
Getting Intune with Bugs and Tokens: A Journey Through EPM was originally published in Posts By SpecterOps Team Members on Medium, where people are continuing the conversation by highlighting and responding to this story.