October 30, 2026
CVE-2026–92744 — Unauthenticated SYSTEM RCE in the Delinea Privilege Manager Agent
I recently reported CVE-2026–92744, a remote code execution vulnerability in the Windows agent for Delinea Privilege Manager. The issue…

By Isuka sanuj
9 min read
I recently reported CVE-2026–92744, a remote code execution vulnerability in the Windows agent for Delinea Privilege Manager. The issue comes down to how the agent authenticates its management server. A custom certificate validator accepts certificates without checking them, and that decision carries through to the agent's policy and task execution paths.
I reproduced code execution as NT AUTHORITY\SYSTEM on a clean installation, without authenticating to the agent. The end-to-end test was performed over loopback. The listener and firewall configuration indicate that the same callback service is exposed over the network, but I did not test it from a second machine. I'll separate what I directly observed from what the configuration shows below.
Delinea released fixed Windows agent versions 12.0.5804 and 13.0.2064 on October 28, 2026.
Starting with the callback listener
Privilege Manager's Windows agent runs as SYSTEM. Among other things, it receives registration results, policy changes, and queued tasks from the management server through a WCF callback service.
On my default test installation, I found the callback listener on TCP 5593:
--- listeners ---
:::5593 pid=4 proc=System <- all interfaces (http.sys)
--- firewall ---
[Domain, Private, Public] Arellia Agent (SSTP-In) | dir=Inbound action=Allow port=5593 prog=System--- listeners ---
:::5593 pid=4 proc=System <- all interfaces (http.sys)
--- firewall ---
[Domain, Private, Public] Arellia Agent (SSTP-In) | dir=Inbound action=Allow port=5593 prog=SystemThe listener was bound to all interfaces, and the installer-created inbound firewall rule covered Domain, Private, and Public profiles. I hadn't relaxed any security settings. The registry configuration was:
HKLM\SOFTWARE\Policies\Arellia\AMS
BaseUrl = https://localhost/TMS/
InstallCode = testinstallcode
BaseUrlOverride = https://localhost/TMS/HKLM\SOFTWARE\Policies\Arellia\AMS
BaseUrl = https://localhost/TMS/
InstallCode = testinstallcode
BaseUrlOverride = https://localhost/TMS/The BaseUrl pointed at localhost in this test environment. That did not change how the callback listener was bound. I used 127.0.0.1 for all of my calls because the sandbox didn't provide another host to test from. That distinction matters when discussing the remote attack vector.
The certificate validator
The first thing that stood out in the code was AmsServerCertValidator:
internal class AmsServerCertValidator : X509CertificateValidator
{
public override void Validate(X509Certificate2 certificate)
{
}
}internal class AmsServerCertValidator : X509CertificateValidator
{
public override void Validate(X509Certificate2 certificate)
{
}
}
That's the entire Validate() method. It returns without checking the certificate.
I found this validator used in three places:
+-------------------------------------------------------+-----------------------------+
| Where it's assigned | What it affects |
+-------------------------------------------------------+-----------------------------+
| _serviceHost.Credentials.ClientCertificate. | Incoming callback |
| Authentication.CustomCertificateValidator | connections |
+-------------------------------------------------------+-----------------------------+
| AgentRegistration2Client.CreateClient(...) | Outbound registration |
+-------------------------------------------------------+-----------------------------+
| ClientBaseFactory.CreateClient<ClientItems2Client, | Outbound policy retrieval |
| ...> | |
+-------------------------------------------------------+-----------------------------++-------------------------------------------------------+-----------------------------+
| Where it's assigned | What it affects |
+-------------------------------------------------------+-----------------------------+
| _serviceHost.Credentials.ClientCertificate. | Incoming callback |
| Authentication.CustomCertificateValidator | connections |
+-------------------------------------------------------+-----------------------------+
| AgentRegistration2Client.CreateClient(...) | Outbound registration |
+-------------------------------------------------------+-----------------------------+
| ClientBaseFactory.CreateClient<ClientItems2Client, | Outbound policy retrieval |
| ...> | |
+-------------------------------------------------------+-----------------------------+The callback host explicitly chooses custom certificate validation:
_serviceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode
= X509CertificateValidationMode.Custom;
_serviceHost.Credentials.ClientCertificate.Authentication.CustomCertificateValidator
= new AmsServerCertValidator();_serviceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode
= X509CertificateValidationMode.Custom;
_serviceHost.Credentials.ClientCertificate.Authentication.CustomCertificateValidator
= new AmsServerCertValidator();This isn't an additional check on top of WCF's normal certificate validation. CertificateValidationMode.Custom delegates the decision to that method. Since the method does nothing, a self-signed client certificate is accepted.
The callback contract also specifies ProtectionLevel.None:
[ServiceContract(Namespace = "http://schemas.arellia.com/agent/services/",
Name = "IAgentRegistration",
SessionMode = SessionMode.NotAllowed,
ProtectionLevel = ProtectionLevel.None)]
public interface IAgentRegistrationCallback
{
void OnRegistrationComplete(PlatformRegistration platformDetails);
void OnApplicablePolicyChange(ClientItemDigest[] applicablePolicyDigests);
void OnClientTaskQueued(PendingTaskExecutionInstance[] pendingTasks);
void OnBadRegistration(Guid suggestedNewId);
}[ServiceContract(Namespace = "http://schemas.arellia.com/agent/services/",
Name = "IAgentRegistration",
SessionMode = SessionMode.NotAllowed,
ProtectionLevel = ProtectionLevel.None)]
public interface IAgentRegistrationCallback
{
void OnRegistrationComplete(PlatformRegistration platformDetails);
void OnApplicablePolicyChange(ClientItemDigest[] applicablePolicyDigests);
void OnClientTaskQueued(PendingTaskExecutionInstance[] pendingTasks);
void OnBadRegistration(Guid suggestedNewId);
}That disables message-level signing for these operations. In the configuration I examined, the client certificate was supposed to establish the caller's identity, but the custom validator wasn't establishing anything.
Confirming that the callbacks were reachable
I tested the operations exposed by IAgentRegistrationCallback using a self-signed certificate. The registration and task-queue operations were reachable, and the other two callbacks also accepted the caller:
Already proven reachable: OnRegistrationComplete, OnClientTaskQueued
OnApplicablePolicyChange accepted=True
OnBadRegistration accepted=TrueAlready proven reachable: OnRegistrationComplete, OnClientTaskQueued
OnApplicablePolicyChange accepted=True
OnBadRegistration accepted=TrueA couple of errors during testing were useful for understanding where validation was happening.
The first was Could not establish trust relationship for the SSL/TLS secure channel. That came from my own .NET client rejecting the agent's self-signed server certificate. It wasn't the agent rejecting my client certificate.
I also hit ContractFilter mismatch at the EndpointDispatcher after using the interface name instead of the contract's declared Name in the SOAP action. The request had already passed the message security stage by the time it reached dispatch. Fixing the action let me call the operation.
Changing the agent's trusted certificates
The next question was what an accepted caller could actually do. OnRegistrationComplete turned out to be important because it takes certificates from the registration response and passes them to the certificate manager:
foreach (string serverCertificate in platformDetails.ServerCertificates)
{
byte[] raw = Convert.FromBase64String(serverCertificate);
ICertificate2 certificate2 = _cert2Factory.Create(raw);
list.Add(certificate2);
}
_certMgr.SaveServerCertificates(list);foreach (string serverCertificate in platformDetails.ServerCertificates)
{
byte[] raw = Convert.FromBase64String(serverCertificate);
ICertificate2 certificate2 = _cert2Factory.Create(raw);
list.Add(certificate2);
}
_certMgr.SaveServerCertificates(list);I traced SaveServerCertificates() as well. It opens LocalMachine\Ams for writing, removes certificates that aren't in the supplied list, and then adds the supplied certificates. I couldn't find a chain check, pinning check, or comparison against an already trusted registration authority in this path. The exception handling around the parsing only covered malformed base64.
I tested this twice on separate clean installations. Here's the relevant output from one run:
[16:21:46] LocalMachine\Ams before: 0 cert(s):
[16:21:52] attacker cert: 13E298FDE814B0B34B1BBB83631BF54276FCF2FC CN=attacker.invalid
[16:21:53] --- INVOKING OnRegistrationComplete with our certificate ---
[16:21:54] call returned without exception
[16:22:06] LocalMachine\Ams after: 1 cert(s): 13E298FDE814B0B34B1BBB83631BF54276FCF2FC[16:21:46] LocalMachine\Ams before: 0 cert(s):
[16:21:52] attacker cert: 13E298FDE814B0B34B1BBB83631BF54276FCF2FC CN=attacker.invalid
[16:21:53] --- INVOKING OnRegistrationComplete with our certificate ---
[16:21:54] call returned without exception
[16:22:06] LocalMachine\Ams after: 1 cert(s): 13E298FDE814B0B34B1BBB83631BF54276FCF2FC
Before the call, the Ams store was empty. After the call, it contained the self-signed certificate I had just generated (CN=attacker.invalid).
This is more serious than simply accepting an untrusted caller. The agent uses this store when verifying signed client items. If an unauthenticated registration callback can populate or replace the certificates in that store, subsequent signature checks can trust the caller's key. Enabling RequireSignedClientItems doesn't solve that underlying trust problem.
Following a queued task into RunTask
The other useful callback was OnClientTaskQueued. To understand where it led, I traced execution through AgentController.RunTask:
clientItem = ClientItemManager.GetRawClientItem(taskInstance.CommandId);
...
if (clientItem == null)
throw new Exception($"Failed to get ClientCommand {taskInstance.CommandId}"); // <-- primitive test stopped here
RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
runspace.Open();
...
runspaceDispatcher.SetExecutionPolicy(ExecutionPolicy.Bypass, ExecutionPolicyScope.Process);
runspaceDispatcher.ImportModule("Arellia.Agent");
...
AgentPowershellScriptContract agentPowershellScriptContract = (AgentPowershellScriptContract)clientItem.InnerContract;
pSCommand.AddScript(agentPowershellScriptContract.Script);clientItem = ClientItemManager.GetRawClientItem(taskInstance.CommandId);
...
if (clientItem == null)
throw new Exception($"Failed to get ClientCommand {taskInstance.CommandId}"); // <-- primitive test stopped here
RunspaceConfiguration runspaceConfiguration = RunspaceConfiguration.Create();
Runspace runspace = RunspaceFactory.CreateRunspace(runspaceConfiguration);
runspace.Open();
...
runspaceDispatcher.SetExecutionPolicy(ExecutionPolicy.Bypass, ExecutionPolicyScope.Process);
runspaceDispatcher.ImportModule("Arellia.Agent");
...
AgentPowershellScriptContract agentPowershellScriptContract = (AgentPowershellScriptContract)clientItem.InnerContract;
pSCommand.AddScript(agentPowershellScriptContract.Script);The task contains a CommandId. The agent uses it to look up a client item; if the item is found, the code eventually creates a PowerShell runspace, sets the process execution policy to Bypass, and calls AddScript() with the script from the item.
For an initial test, I queued a task using a newly generated GUID that wasn't present in any item database or configuration. The callback returned, and I observed the marker I had set for the task path:
[18:03:10] --- calling OnClientTaskQueued (unauthenticated) ---
[18:03:12] call returned without exception
[18:03:22] MARKER SEEN after 10s[18:03:10] --- calling OnClientTaskQueued (unauthenticated) ---
[18:03:12] call returned without exception
[18:03:22] MARKER SEEN after 10sThe unresolved command was expected to fail. The agent reached the GetRawClientItem() lookup and threw because the item did not exist. This test showed that the unauthenticated callback could cause the agent to process an attacker-chosen task identifier; it did not, by itself, demonstrate script execution.
Reproducing SYSTEM execution
The remaining piece was the client item. A queued task alone isn't enough: the item has to be available when the agent retrieves it, and it has to pass signature verification.
In the test environment, I used a server endpoint that responded to the agent's normal registration and client-item requests. The sequence I observed was:
- The agent registered and processed a response containing my certificate, which was written into
LocalMachine\Ams. - I sent
OnClientTaskQueuedto the callback listener with the task identifier. - The agent requested the corresponding client item from the configured endpoint.
- The signed item was checked against the certificate now in
Ams. - The task reached the PowerShell execution path, running under the agent's SYSTEM account.
The result:
EXECUTED as: nt authority\system at 2026-09-12T20:30:02
*** UNAUTHENTICATED CODE EXECUTION AS SYSTEM - CONFIRMED ***EXECUTED as: nt authority\system at 2026-09-12T20:30:02
*** UNAUTHENTICATED CODE EXECUTION AS SYSTEM - CONFIRMED ***
That was the point where I could confirm end-to-end code execution, rather than only callback access or task scheduling.
An agent restart didn't fix the underlying issue. In my test, re-registration and subsequent item retrieval caused the trust store and verifier state to be loaded again, so the same chain remained possible.
Two details that mattered during testing
I ran into two things that made the results confusing at first:
taskInstance.Inputneeded to be a non-null, empty collection.RunTask()iterates over it after adding the script. Passingnullcaused aNullReferenceExceptionafter the execution point, making a successful run look like a failure.- The signature verifier could hold an older view of
Ams. Restarting the agent, or allowing its normal cycle to reload the store, made the newly added certificate available to verification.
While investigating the command lookup, I also captured these requests:
POST /TMS/Agent/AgentRegistration4.svc
POST /TMS/Agent/ClientItems2.svc <- went to fetch the command
POST /TMS/Agent/AgentRegistration4.svc <- reported the resultPOST /TMS/Agent/AgentRegistration4.svc
POST /TMS/Agent/ClientItems2.svc <- went to fetch the command
POST /TMS/Agent/AgentRegistration4.svc <- reported the resultThe agent reported the failing command ID back in its own fault response:
<b:ExceptionMessage>Failed to get ClientCommand c68751d5-62d1-46ed-b3e2-c253d6341f06
because of exception Failed to RefreshClientItemType: 00000000-... (GetDigests)</b:ExceptionMessage>
<b:InstanceState>Faulted</b:InstanceState><b:ExceptionMessage>Failed to get ClientCommand c68751d5-62d1-46ed-b3e2-c253d6341f06
because of exception Failed to RefreshClientItemType: 00000000-... (GetDigests)</b:ExceptionMessage>
<b:InstanceState>Faulted</b:InstanceState>That ID had been generated during my test. The fault confirmed the task had made it to the command-resolution path, even before I had the signed client item working.
Remote exposure and severity
I want to be clear about the scope of the reproduction. Every request I sent came from 127.0.0.1. I did not execute a second-host network test. The callback listener was bound to :::5593, and the installation created an inbound allow rule for that port across all three Windows Firewall profiles. Those findings support a network-reachable attack surface, but they aren't a substitute for a direct remote test.
Here are the scores for the two ways of looking at my test, alongside Delinea's published rating:
+--------------------------------------+---------------------------------------------------+------------+
| Scenario | CVSS vector | Score |
+--------------------------------------+---------------------------------------------------+------------+
| Network exposure indicated by | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | 9.8 |
| configuration (CVSS 3.1) | | |
+--------------------------------------+---------------------------------------------------+------------+
| Loopback/local path directly | CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | 7.8 |
| demonstrated (CVSS 3.1) | | |
+--------------------------------------+---------------------------------------------------+------------+
| Published by Delinea (CVSS 4.0) | CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:A/VC:H/VI:H/VA:H/ | 8.7 (High) |
| | SC:H/SI:H/SA:H | |
+--------------------------------------+---------------------------------------------------+------------++--------------------------------------+---------------------------------------------------+------------+
| Scenario | CVSS vector | Score |
+--------------------------------------+---------------------------------------------------+------------+
| Network exposure indicated by | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | 9.8 |
| configuration (CVSS 3.1) | | |
+--------------------------------------+---------------------------------------------------+------------+
| Loopback/local path directly | CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H | 7.8 |
| demonstrated (CVSS 3.1) | | |
+--------------------------------------+---------------------------------------------------+------------+
| Published by Delinea (CVSS 4.0) | CVSS:4.0/AV:N/AC:H/AT:P/PR:H/UI:A/VC:H/VI:H/VA:H/ | 8.7 (High) |
| | SC:H/SI:H/SA:H | |
+--------------------------------------+---------------------------------------------------+------------+Delinea's published vector uses AV:N. It also includes PR:H, despite the CVE being described as unauthenticated. I've kept their published vector here rather than presenting my own estimate as the official score.
For the CVSS 3.1 scenarios above, I used S:U: the vulnerable agent already runs as SYSTEM, and the script executes in that process context.
Other issues I noticed in the same codebase
While tracing registration and client-item verification, I found a few related trust decisions worth mentioning. These were observations from the code review, not separate end-to-end RCE demonstrations.
internal static bool GetRequireSignedClientItems()
{
bool result = true;
if (IsInternetClientUri(GetAmsBaseUrl()))
result = false;
...
}
public static bool IsInternetClientUri(Uri clientUri)
{
return clientUri.Host.EndsWith("servicebus.windows.net", StringComparison.InvariantCultureIgnoreCase);
}internal static bool GetRequireSignedClientItems()
{
bool result = true;
if (IsInternetClientUri(GetAmsBaseUrl()))
result = false;
...
}
public static bool IsInternetClientUri(Uri clientUri)
{
return clientUri.Host.EndsWith("servicebus.windows.net", StringComparison.InvariantCultureIgnoreCase);
}For an agent using a servicebus.windows.net endpoint, GetRequireSignedClientItems() returns false. That weakens the client-item signing requirement on the relay path.
The signature verification path can fail open.
if (failedWithException)
{
if (CoreManagementAgent.GetRequireSignedClientItems()) { ...; return false; }
return true;
}if (failedWithException)
{
if (CoreManagementAgent.GetRequireSignedClientItems()) { ...; return false; }
return true;
}When signature verification throws, the return value depends on GetRequireSignedClientItems(). If signing isn't required, this branch returns true.
Outbound TLS certificate validation is disabled by default in the on-premises configuration I inspected. When ValidateServerCertificate and ValidateCertChain are unset, the ServerCertificateValidationCallback accepts the server certificate. I also found that enabling ValidateCertChain=1 alone did not reject a self-signed certificate where Subject == Issuer and the chain reported UntrustedRoot.
The registration data isn't encrypted at the message level. The registration payload is signed, but InstallCode and the machine SID aren't separately encrypted by that payload protection. This matters particularly when the transport peer's certificate isn't verified.
Patch and mitigation
Delinea fixed the issue in Windows agent 12.0.5804 for v12 and 13.0.2064 for v13, released with Privilege Manager 13.0.2 on October 28, 2026.
If you're running affected agents, updating is the fix. Delinea also lists blocking inbound TCP 5593 as an interim mitigation; the agents can continue using scheduled pulls.
From my review, the broader security lessons are about preserving the trust boundary, not just changing one line. The callback caller should be authenticated against a known certificate, and a registration callback shouldn't be able to replace the verification authority without that authentication. Message signing, a more restrictive listener/firewall configuration, signed-item verification that fails closed, and server certificate validation are also relevant hardening measures.
Disclosure timeline
2026-09-12 Reproduced end to end, clean default install (SYSTEM proof)
2026-09-14 Reported to Delinea (case #01416021)
2026-09-16 CVE-2026-92744 reserved
2026-09-29 Answered the dev team's questions; rebutted the proposed severity downgrade
2026-10-28 Fixed in agent 12.0.5804 / 13.0.2064 (Privilege Manager 13.0.2)
2026-10-29 CVE-2026-92744 published, 8.7 High2026-09-12 Reproduced end to end, clean default install (SYSTEM proof)
2026-09-14 Reported to Delinea (case #01416021)
2026-09-16 CVE-2026-92744 reserved
2026-09-29 Answered the dev team's questions; rebutted the proposed severity downgrade
2026-10-28 Fixed in agent 12.0.5804 / 13.0.2064 (Privilege Manager 13.0.2)
2026-10-29 CVE-2026-92744 published, 8.7 HighResearch credit: Isuka Sanuj (lead), Ishan Nim (contributor), CyberCrew Inc.
Thanks to Ishan Nim for guiding me through the research and disclosure process, and to the CyberCrew team for their support.
Final thoughts
The empty Validate() method was the starting point, but the real impact came from how much trust the agent placed in the result. Once the callback accepted an unverified certificate, the registration path could change the trusted certificates used for later signature checks. Those checks then protected execution with a trust store the untrusted caller had been allowed to control.
That's what made this worth tracing past the first callback. Accepting a self-signed certificate was one bug; following that decision through registration, item verification, and task execution showed its actual impact.
Full technical details and research files: https://github.com/isukasanuj/CVE-2026-92744