CVE-2026-122988: RCE in Windows Admin Center

Windows Admin Center is Microsoft’s browser-based platform for managing Windows servers, clusters, virtual machines, and related infrastructure. Administrators interact with it through a web interface, while extensions running on the gateway perform the underlying management work with the Windows identity associated with the user’s session.

While testing Windows Admin Center 2.7.18.4, Sable, Vulnetic’s autonomous AI pentester, found that the Microsoft-signed msft.sme.vm-conversion 1.13.7 extension combined three separate security flaws in a particularly dangerous way. The extension placed an unescaped route parameter into a PowerShell command, queued the attacker-controlled migration before a later permission check failed, and eventually processed that queued work using a cached impersonation token belonging to another user.

Chained together, those behaviors allowed an ordinary GatewayUser to submit arbitrary PowerShell that later ran on the gateway under a GatewayAdministrator’s Windows identity. The attacker did not need the administrator’s password, cookies, session, or token. In our test environment, the payload ran as EC2AMAZ-NMNBK7S\sablewacadmin, a local administrator, and created a marker file owned by BUILTIN\Administrators.

Microsoft assigned the issue CVE-2026-122988, rated it Important with a CVSS 3.1 score of 8.0, and classified it as CWE-78, OS command injection. Microsoft’s advisory describes the deterministic exploitation path in which a low-privileged user submits a crafted request and a GatewayAdministrator later performs a VM conversion. Sable also reproduced a second path in which the extension’s background worker executed the queued payload without any new administrator activity, provided that a recently cached administrator token was still valid.

Sable found the initial injection while tracing user-controlled input into PowerShell and process-execution sinks across Windows Admin Center extensions. The static analysis quickly showed that the VM Conversion extension was building PowerShell from an attacker-controlled route value, but that alone did not explain which Windows identity would execute the command or why the low-privileged request returned successfully without immediately creating the proof file.

The static sink is confirmed in the IL dump. Now let me verify live environment state: gateway up, extension installed, low-priv user exists.

The answer came from following the request beyond the injection point. The malicious migration was stored before a later status-file write failed, leaving it in a shared in-memory queue. That queue could then be processed by a different request or by a background timer, and the worker used whichever Windows impersonation token the extension had cached most recently.

Background path goes through CacheUtil.RunImpersonated. Let me see what identity CacheUtil impersonates.

That turned a straightforward command injection into a cross-user privilege escalation. The GatewayUser supplied the code, the queue preserved it, and a GatewayAdministrator’s cached token supplied the execution context.

The destination parameter becomes PowerShell

The vulnerable route accepts a destination value that is supposed to identify the target Hyper-V host:

Several migration prechecks insert that value directly into a PowerShell command template:

The extension effectively constructs and executes the command as follows:

PowerShellExecutor then sends the completed string into an in-process PowerShell runspace:

The validation only verifies that destination is present. It does not constrain the value to a valid hostname or escape PowerShell metacharacters before the string reaches String.Format and AddScript. A semicolon can therefore terminate the intended argument and begin a new statement.

Sable used a harmless payload that wrote the current Windows identity to a file:

The final Invoke-Command keeps the resulting script syntactically balanced. After URL encoding the payload, the low-privileged user submitted it through the migration endpoint and received HTTP 202. The marker file did not appear immediately, which led Sable to the second flaw in the chain.

The attacking account was a normal GatewayUser and a member of BUILTIN\Users. It was not a GatewayAdministrator, which was independently confirmed when /api/gateway/access/users returned HTTP 403.

Low-priv user works and is confirmed non-admin (403 on gateway access). Now the core reproduction: Phase A, low-priv plant, admin connect, immediate batch-run detonation.

Inside ConcurrentVMBatchMigrationQueue.Enqueue, the extension adds the migration object to its in-memory queue before attempting to update the migration status:

The status update writes to the following file:

Ordinary users can read and execute within that directory but cannot append to the status file, so the write throws an UnauthorizedAccessException. By then, however, queue.Enqueue(item) has already completed. The exception is swallowed by the surrounding fire-and-forget task, and the attacker-controlled migration remains in memory even though the low-privileged user cannot finish processing it.

The lack of an expected queue log initially suggested that insertion had failed, so Sable went back through the lambda and the exact ordering of Migrate rather than treating the missing log as proof.

The low-priv lambda never logged ‘Added the … to the migration queue.’ Let me see the b\\0 lambda and Migrate’s exact sequence.

That review showed why the HTTP 202 response and missing marker were misleading. The request had failed after modifying shared state, leaving the payload waiting for whichever execution path processed the queue next.

A normal administrator request executes the payload

For the deterministic reproduction, a GatewayAdministrator later submitted an ordinary migration using destination=localhost. The administrator’s request contained no injected PowerShell, and the attacker never accessed the administrator’s account. It simply caused the extension to process the existing queue while running under the administrator’s impersonated Windows token.

The primary PoC matches the validated deferred cross-user chain. Let me see the omitted middle section, CLI args, low-priv user creation, and the timer PoC.

The attacker’s request did not execute as the GatewayUser, and the administrator’s request contained no malicious input. The extension took code from one user’s queued migration and executed it under another user’s Windows identity.

Marker file appeared ~10s after the POST. Execution confirmed. Content looked empty on first read, possible flush race. Re-reading and pulling the migrate response plus event-log output.

After the file contents and ownership were verified, Sable recorded the result plainly:

All validation steps complete. The finding is fully reproduced and confirmed.

Executing without a new administrator request

The deterministic path established the cross-user privilege escalation, but Sable continued testing to determine whether exploitation always required an administrator to perform a new VM conversion. A successful VM Conversion session starts a batch timer that runs approximately every ten minutes, while the extension retains the requester’s impersonation token for 90 minutes and calls CacheUtil.RunImpersonated when processing queued work.

To test that path, an administrator first used VM Conversion normally, which initialized the background worker and populated the token cache. Once the immediate batch run finished, the low-privileged user submitted the poisoned migration. No further administrator VM Conversion requests were made.

At the next timer tick, the marker appeared:

This means an administrator does not have to interact with VM Conversion at the moment the attack is planted or executed. If the worker has already been initialized and its cached administrator token is still valid, the GatewayUser can submit the payload and wait for the next background cycle.

The critical positive claims are already fully reproduced: low-priv plant (HTTP 202), zero admin interaction, 10-minute timer tick executed the payload as EC2AMAZ-NMNBK7S\sablewacadmin at exactly connect+10min.

Sable also tested what happened after the cached administrator token expired. The extension retains the token for 90 minutes, so the test stopped all VM Conversion activity, allowed the cached identity to cross that threshold, and then waited for the next batch cycle.

Cache age just crossed the 90-minute threshold (90.9 min). Plant#3 fires in ~2 minutes; the next 10-minute tick will process it with an expired or absent cached token.

Once the token expired, the extension fell back to running without impersonation as NT AUTHORITY\NetworkService, but the queued payload did not reach execution. The autonomous path therefore depends on a valid cached privileged token and should not be described as a standalone NetworkService RCE.

Sable also checked whether sibling precheck or VM-listing routes could provide immediate execution without the queue and cached-token conditions. Version 1.13.7 did not expose a path that removed those prerequisites. An older 1.6.0 build contained a synchronous precheck route with the same injection pattern, but it ran as the caller, and the status-file ACL stopped a low-privileged user before the PowerShell sink.

The hypothesized precondition-removing path does NOT exist. Immediate in-request execution exists only in the older 1.6.0 plugin, runs as the caller, never cached admin or NetworkService, and the status-file ACL still blocks low-priv callers before the sink.

These tests narrowed the finding to what was actually demonstrated: deferred, cross-user PowerShell execution through the VM Conversion queue, with an autonomous variant available while a cached GatewayAdministrator token remains valid.

An authenticated GatewayUser can execute arbitrary PowerShell on the Windows Admin Center gateway under another user’s GatewayAdministrator Windows identity. The attacker does not need that user’s password, session, cookies, token, or participation in a malicious request.

The resulting access depends on the privileges assigned to the borrowed Windows identity. A successful attacker may be able to read credentials available to that user, modify files and Windows Admin Center state, access managed infrastructure, or establish persistence on the gateway. In our environment, the cached identity belonged to a local administrator, and the proof file was owned by BUILTIN\Administrators.

- Windows Admin Center build: 2.7.18.4 - Display version: 2606 - Extension: msft.sme.vm-conversion - Extension version: 1.13.7 - Attacker role: GatewayUser - Execution identity: GatewayAdministrator Windows user

Microsoft lists version 2.1.36 of the VM Conversion extension (msft.sme.vm-conversion) as the security update for CVE-2026-122988. We tested extension version 1.13.7. The injection had been in the extension since its first public release in August 2025, about 14 months before Microsoft shipped the fix. Administrators should install the current security update and confirm that their deployment is no longer listed as affected in Microsoft’s Security Update Guide. Windows Admin Center exposes separate build, display, and extension versions, so deployments should be checked against Microsoft’s affected-product table rather than comparing only the 2606 display version used in our test.

At the code level, destination should be parsed and validated as a hostname or address, then passed to PowerShell as typed data instead of being concatenated into a script. Queue insertion and status persistence should also be atomic so that a failed status update cannot leave untracked work behind.

Most importantly, queued migrations should remain bound to the immutable identity and authorization context of the user who submitted them. A shared background worker should never execute one user’s queued input under whichever impersonation token happens to be cached from another session. Background work should use a narrowly scoped service identity or perform a fresh authorization decision tied to the original submitter before execution.