A GUID is Not a Credential: Unauthenticated RCE in Veeam Service Provider Console

A GUID is Not a Credential: Unauthenticated RCE in Veeam Service Provider Console

Share

TL;DR
CVE-2026-58073 (CVSS 9.5) and CVE-2026-58072 (CVSS 9.0) are critical vulnerabilities in Veeam Service Provider Console, the multi-tenant console that managed service providers use to run backups across all of their customers. The first lets an unauthenticated network peer claim a connected backup agent’s identity and receive that agent’s real certificate. The second lets anything holding an agent certificate write a file anywhere on the server. Chained, they are unauthenticated remote code execution on the console that sits above every tenant’s backups, which we proved end to end against Veeam’s own binaries. Patch to 9.3.0, then check your logs for indicators of compromise. Bishop Fox has published a safe detection tool.

Summary

On August 4, 2026, Veeam published KB4893, covering four vulnerabilities in Veeam Service Provider Console (VSPC). All four affect 9.2.1.33875 and every earlier version 9 build. The fix ships only in 9.3.0.35057, with no 9.2.x backport, so remediation means a version upgrade rather than a hotfix.

This post covers the critical pair:

  • CVE-2026-58073 (CVSS v4.0 9.5), described by Veeam as “a vulnerability in Veeam Service Provider Console allowing an unauthenticated attacker to impersonate a managed agent and obtain that agent’s credentials.”
  • CVE-2026-58072 (CVSS v4.0 9.0), “a vulnerability in Veeam Service Provider Console allowing arbitrary file write on the management server, which can lead to remote code execution.”

Both came in through HackerOne. The advisory does not name the reporter, so credit for the original discovery goes to them. Bishop Fox diffed the two builds, confirmed each root cause, proved the chain against real Veeam binaries, verified the patch closes both halves, and built a scanner defenders can run without exploiting anything.

Two sibling CVEs shipped in the same advisory. Neither is part of this chain, though the same upgrade closes both. CVE-2026-58067 (CVSS 8.7) is an unauthenticated memory exhaustion denial of service, which our diff traces to unbounded inbound packet buffering on the same agent channel. CVE-2026-58071 (CVSS 8.2) allows unauthenticated access to a proxied appliance API as Portal Administrator during a short window after an administrator session begins.

What Defenders Should Do Right Now

  • Patch to 9.3.0 or later. One build fixes all four KB4893 issues.
  • Restrict who can reach TCP/9999. The agent port should only answer the subnets your agents live in. That is a real control for consoles serving only provider infrastructure, and a weaker one for providers running Veeam Cloud Connect, whose tenant agents arrive through an internet-facing gateway by design.
  • Sweep your fleet. Run our detection tool against every console you operate, including internal ones. The probe is unauthenticated and its whole footprint is two TCP connections.
  • Go looking for exploitation, not just exposure. The attack is loud in the console’s own logs on both patched and unpatched builds. Exact strings are below.
  • If you find those signatures, treat the agent certificates as compromised and call Veeam. Patching does not revoke a certificate the console already issued, and rotating one is not a documented procedure. What helps and what does not is below.


Background

Veeam Service Provider Console is the multi-tenant platform that Veeam Cloud & Service Providers and larger enterprises use to run backups for many customers from one place. Every managed machine runs a management agent that connects back to it. That makes the console a high-value target: one server is the control plane for many organizations’ backups, and an agent credential is a foothold in someone else’s estate. Agents reach it two ways:

Path

Port

Exposure

Agent in the provider’s own infrastructure, connecting directly

TCP/9999

usually internal

Agent in a customer environment, connecting through a Veeam Cloud Connect gateway

TCP/6180

internet-facing by design

The protocol on 9999 is a proprietary framed protocol carrying protobuf messages. The ConnectionHub listening there is a router: it reads a short handshake off a raw socket, looks up the named receiver the connection asks for, and multiplexes it through to the Application Server. Neither authentication nor TLS termination happens at the router; both happen at the far end, inside the tunnel.


The Vulnerability

CVE-2026-58073: The Server Asks the Wrong Question

Once a connection reaches the Application Server, it negotiates TLS inside the tunnel and reads the agent’s identity out of the client certificate’s subject, where Veeam packs several GUIDs into the distinguished name:

CN = <agentId GUID> 
O  = "<companyId GUID>[;<clusteredAgentId GUID>]" 
OU = <locationId GUID>

The server then decides what this peer may invoke, in ReadAgentIdProcessorHandshakeV4.Perform:

if (!_agentLoginManager.IsLoggedIn(clusteredAgentId) || _agentLoginManager.IsRejectedAgent(agentId)) 
{ 
    channelEndpoint.IncomingCallInterceptor = new AnonymousInterceptor(); 
} 
else if (val.ClusteredAgentMode == ClusteredAgentMode.Secondary) 
{ 
    channelEndpoint.IncomingCallInterceptor = new SecondaryAgentInterceptor(); 
} 
// no else: a peer that passes the check gets no interceptor at all

The interceptor is the authorization layer, so an attacker wants to fall past both branches. A channel with no interceptor installed carries no restrictions at all: RemoteTypeProvider.InvokeMethod falls back to a default that simply invokes the call. Everything therefore rides on IsLoggedIn, which in 9.2.1 is a bare dictionary lookup:

public bool IsLoggedIn(Guid clusteredAgentId, out AgentLoginInfo loginInfo) 
    => _logins.TryGetValue(clusteredAgentId, out loginInfo);

_logins is a process-global table populated whenever any agent authenticates successfully, and the clusteredAgentId being looked up is the one the peer wrote into its own certificate. The server answers “is this connection authenticated?” by asking “has somebody with this GUID logged in at some point?” The answer is never tied to the certificate presented on this connection.

That would be latent if the TLS handshake rejected unknown certificates, but it does not. AgentManagement_StrongCertificateAuthorization defaults to false, and in that weak mode the login routine tries to validate the certificate chain, fails, creates a guest session, and returns true anyway. Any self-signed certificate with a parsable subject completes the handshake. There is a reason for the leniency: legitimate agents really do present self-signed certificates the first time they enroll, before the server has issued them anything, so at the TLS layer impersonation and first-time enrollment look the same.

So an unauthenticated peer presenting a self-signed certificate with a connected agent’s GUID reaches the full agent surface, certificate dispatchers included: IServerCertificateDispatcher.IssueCertificateForAgent hands back a PKCS#12 bundle and the password to open it. That is the advisory’s “obtain that agent’s credentials.” The credential is durable because it chains to the console’s real agent root CA. The Linux path (NixSslHandshakeV2) carries the same defect and received the same fix.

The whole patch is one function signature. 9.3.0 replaces the lookup with:

public bool IsLoggedIn(Guid clusteredAgentId, string connectionCertificateThumbprint, out AgentLoginInfo loginInfo) 
    => _logins.TryGetValue(clusteredAgentId, out loginInfo) 
       && string.Equals(loginInfo.ConnectionCertificateThumbprint, connectionCertificateThumbprint);

AgentLoginInfo gains a ConnectionCertificateThumbprint field, set only on a genuine login after the certificate chain validates against the VSPC root, and all six IsLoggedIn call sites now pass the thumbprint from the current connection. The question changes from “has this GUID ever logged in” to “is this the connection that logged in.”

CVE-2026-58072: Two Attacker-Controlled Path Components

The second bug is EndpointDownloadAcceptor.SaveFiles, on the IDownloadAcceptorPb contract, registered on the same agent-facing receiver:

public async Task SaveFiles(DownloadPackagePb package, RpcUserStream stream) 
{ 
    ... 
    FromVmbpDownloader.SaveFileOnDisk( 
        Path.Combine(_vmbpShareFolder[Convert(package.PathFor)], 
                     package.DefinedPathSubfolder ?? string.Empty),   // attacker-controlled 
        package.Files, stream, CancellationToken.None); 
}

and downstream in FromVmbpDownloader.SaveFileOnDisk:

string text = Path.Combine(targetPath, item.FileName);                 // also attacker-controlled 
if (!Directory.Exists(targetPath)) Directory.CreateDirectory(targetPath); 
using FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.ReadWrite, FileShare.None);

SaveFiles exists so an agent can upload files to the console, into the share roots it keeps for agent packages and deployment staging. Three caller-supplied fields decide where those files land. package.PathFor picks the share root, DefinedPathSubfolder names a folder beneath it, and each FileDataPb.FileName names a file to create there. Only PathFor is a server-side lookup, into a fixed table. The other two are strings off the wire, and nothing validates either.

That is cleaner to abuse than a typical traversal, thanks to .NET Path.Combine semantics: a rooted second argument discards the first entirely. An attacker who sets DefinedPathSubfolder to C:\inetpub\wwwroot throws the share root away without a single ..\, which is also why naive traversal signatures miss it. From there, Directory.CreateDirectory creates the destination if it is absent, FileMode.Create truncates or creates the file, the content streams straight off the RPC call, and the write runs as the console’s service account.

Chaining the Two

Neither half is enough alone. A peer that fails the login check is handed the anonymous interceptor, which does not allow IDownloadAcceptorPb, so SaveFiles stays out of reach without an agent identity. CVE-2026-58073 supplies exactly that: pass the GUID check, take the unrestricted channel, then call SaveFiles on it with a rooted destination. An authentication flaw becomes an arbitrary file write on the management server. With IIS serving the portal, a page dropped into the web root turns that write into code execution, which is how we finished the chain in the lab. 

Preconditions

A vulnerable version is necessary but not sufficient, and some of these cut against the worst case.

  1. Exposure depends on configuration.

    The attack needs a TCP path to the agent channel on 9999, which is normally internal. Reaching it means a misconfiguration, an insider, or an existing foothold that can route there. Internet exposure requires Cloud Connect, whose gateway on 6180 appears to forward connections to 9999 untouched. We traced that in the gateway code but had no live gateway to test against.
  2. An agent must be currently logged in.

    _logins
    is populated only by a genuine agent login behind a certificate chaining to the console’s root. The weak-mode guest path never writes to it, and no anonymous contract can enroll an agent, so on a console where no agent has ever connected the bug is inert. It is also why a console is not automatically its own victim: Veeam installs management agents onmanaged machines, and the console installer offers only the Server and Web UI components.
  3. The attacker must know a connected agent’s GUID.

    This is a genuine requirement, but a cheap one to meet, because the GUID is not treated as a secret in either place it lives: unencrypted on the wire, and world-readable on disk on every managed endpoint. KB4893 changes neither, so model the GUID as a step rather than a barrier. Both routes are below.

Detecting It Safely

The ConnectionHub gives away its patch state before a client authenticates or negotiates TLS. It validates the client-advertised protocol version at the start of the handshake, where the patch widened the accepted range:

Build

Check

Accepts

9.2.1 and earlier (vulnerable)

(uint)(versionByte - 3) <= 3

3, 4, 5, 6

9.3.0 and later (patched)

(uint)(versionByte - 3) <= 4

3, 4, 5, 6, 7

A handshake advertising version 7 is a clean binary discriminator. A patched hub parses the request, fails to find the named receiver, and returns an XML error. A vulnerable one throws and disposes the socket, returning zero bytes.

Our scanner sends two probes in a specific order. Probe one advertises version 6 and must return Requested receiver not found, which proves the target is a VSPC ConnectionHub. Without that gate, silence in probe two also matches any quiet TCP service on the internet, and the scan would report firewalls as vulnerable consoles. Probe two advertises version 7.

$ ./cve_2026_58073_check.py vspc.example.com patched.example.com --brief 
VULNERABLE    vspc.example.com:9999            protocol-7-rejected 
PATCHED       patched.example.com:9999         protocol-7-accepted

Neither probe changes server state. Each handshake names a receiver bf-probe-<uuid4> that will not exist, so the lookup misses and returns an error. No receiver is registered, no channel is built, no TLS session starts, no agent record is touched. The tool never sends the handshake type that would register a name. It leaves six lines in ConnectionHub.log carrying the bf-probe- marker, so a defender who finds them can tell our scan from an attack.

Detecting Exploitation, Not Just Exposure

A scan result speaks only to exposure. Whether anyone came through an unpatched console is answerable too, because Veeam logs the relevant events on both builds. The console is Windows-only, so the files below always sit under %ProgramData%\Veeam\Veeam Availability Console\Log\, even where the impersonated agent is a Linux one.

The File Write on Patched Servers

The 9.3.0 acceptor adds two guards, and both log at error level from ServerEndpointDownloadAcceptor. A vendor typo separates them: Veeam misspelled “Suspicious” in exactly one of the two.

Suspicios call from channel Id: <channel-guid> 
Path '<path>' is outside of the allowed service folders. Suspicious call from channel Id: <channel-guid>

The misspelled line is the CVE-2026-58072 signature. It fires only when a caller invokes the method the vendor deleted, and choosing deletion over validation is a strong statement that no legitimate caller existed, so treat one occurrence as an attempted exploit until you can show otherwise. The correctly spelled line is weaker: a write path 9.3.0 still supports, aimed outside the allowlist. Chase it down, but do not read it as evidence of this CVE. A rule that fires identically on both lines buries the one worth waking someone up for.

First, grep Suspicio to catch both, then split on the spelling to triage. Anchor on the token ServerEndpointDownloadAcceptor, and deduplicate, since error-level lines are copied into Server_error.log. Only the misspelled line appeared on our 9.3.0 rig; we read the other in the binary.

Either line pairs with an Incoming method call 'SaveFiles' record in Agent_Communication.log carrying the same channel GUID, so correlating them gives attempt and outcome as one event.

The File Write on Unpatched Servers

No guard here, but still a trail, since the writer logs every destination at information level:

FromVmbpDownloader: Saving file <full path> 

Hunt for that line with a path outside the console’s own share roots. A write into C:\inetpub\wwwroot or next to a service binary is not something a backup workflow does.

The Impersonation on Unpatched Servers

The 58073 half is just as loud. The server evicts the real agent’s channel and rejects the attacker’s certificate before honoring the call anyway, so a successful attack leaves this shape across AgentAuthorization.log and Agent_Communication.log:

The agent clusteredAgentId <guid> has successfully logged in.          <- the real agent, earlier 
certificate <thumbprint> did not pass validation.                      <- the attacker's cert 
Channel <old> used by agent <guid> is being replaced with channel <new> 
Incoming method call 'IssueCertificateForAgent' 
Channel <new> released for agent <guid> 
The agent clusteredAgentId <guid> has logged out.

Build the rule around a certificate validation failure immediately followed by a channel replacement for the same agent GUID, then a certificate issuance call on the new channel, all inside a second or two. A Saving file line to an unexpected path after that sequence is the full chain in two events. While you are in that file, grep for Agent certificate authorization mode: weak, which confirms the default at runtime. Do not build on rate alone: agents log out and reconnect normally, so the certificate validation failure is what separates an attack from that noise. Log strings are also not a stable interface, so match distinctive tokens rather than whole lines and confirm the format against your own build.

What to Do With a Hit

Any of these signatures means a certificate may have been issued to the wrong party, and rotating one is not a documented procedure, so this is a support conversation rather than a runbook. Three things look like they should help and do not. The certificates you can manage are not the CA that signs agent certificates, resetting a company security token explicitly leaves already-connected agents alone, and reinstalling an agent issues a new certificate without invalidating the old one. Open a Veeam Support case for guidance on compromised agent certificates. In the meantime, rejecting the affected agents is the one documented lever with any effect: the handshake shown earlier installs AnonymousInterceptor on a rejected agent’s channel, dropping a stolen certificate to the anonymous surface. It costs that agent’s SLA statistics, and a rejected agent cannot be uninstalled.

Weaponization

We built a client that speaks the agent protocol and ran the full chain on isolated labs of 9.2.0 and 9.2.1, both stood up by hand rather than through the vendor’s installer, then repeated the runs against 9.3.0. Here we describe the path rather than the recipe, omitting the certificate forging and wire-protocol specifics.

  1. Learn a connected agent’s GUID.

    Nothing on the anonymous surface enumerates agents: no contract there returns a collection, and the closest thing, 9.2.1’s Heartbeat, only confirms a guessed GUID against 122 bits of entropy, and 9.3.0 stops answering it anonymously at all. So the GUID has to come from somewhere else, by one of two routes. On the wire, the agent channel defaults to TLS 1.2 on both builds, and TLS 1.2 sends the client’s Certificate message before ChangeCipherSpec, so the subject carrying those GUIDs crosses in the clear to anyone capturing between a managed machine and the console. On disk, BUILTIN\Users on any machine running an agent can read the GUID from HKLM\SOFTWARE\Veeam\VAC\Agent\AgentId, the agent’s log directory, and its own certificate; we confirmed those permissions on both rigs. Either route tends to satisfy the logged-in precondition for free, since agents hold a persistent channel. Our own runs skipped this step, since we enrolled the victim agent ourselves and took its GUID from the portal API, so both routes are analysis, not something we exercised as an attacker would.
  2. Get a real credential.

    The attacker generates a self-signed certificate carrying that agent’s identifiers, connects to 9999, and completes the handshake. Because the login check consults only the GUID, the channel comes up with no authorization layer and the certificate dispatcher becomes callable. Against our 9.2.1 lab the server returned a 2,931-byte PKCS#12 bundle including a private key, issued by CN=Veeam Software, the console’s own agent root CA. A control run with a random GUID was denied with InvokeDeniedException, which proves the GUID is doing the work. There is no session for the server to expire here, only an ordinary root-signed certificate, which is why the patch section returns to it.
  3. Turn agent access into a file write.

    With the stolen certificate the attacker opens a new connection, authenticates as a legitimate agent, and calls SaveFiles with a rooted destination. That works as a separate connection precisely because the credential is genuine, so it does not depend on still holding the hijacked channel or on the victim’s login still being live.
  4. Turn the write into execution.

    The console runs on Windows with IIS hosting the web portal, so the shortest path to execution is a page in the web root. We wrote a marker page into C:\inetpub\wwwroot, requested it, and got HTTP 200 with our marker string back, executing as IIS APPPOOL\DefaultAppPool. That is end-to-end unauthenticated remote code execution, reproduced on 9.2.0 and 9.2.1 across two separately built labs. A hardened deployment may serve the portal from a different root or identity, which changes the sink without changing the underlying write. That identity also holds SeImpersonatePrivilege, the usual precondition for local privilege escalation, though we did not chain that step.

Impact scales with what the console manages. On a service provider’s deployment, code execution on the management server is code execution on the control plane for every tenant it serves, with database access to its configuration and the ability to push tasks to agents in customer environments. Step two is separately useful without the RCE, because an agent certificate is an authenticated position in the backup infrastructure of whichever organization that agent belongs to, which on a provider’s console is one of its customers rather than the provider itself.

The Patch

9.3.0 fixes both vulnerabilities at the source rather than at the perimeter: the login check now requires the connection's own certificate to match, and SaveFiles is removed outright.

We tested the fix on our 9.3.0 lab console with a victim agent enrolled through the portal and logged in. Both the control run and the impersonation run were denied with no certificate issued, where 9.2.1 hands back a credential for the same input. The self-signed peer is now demoted to guest no matter whose GUID it claims:

AgentLoginManager: The agent clusteredAgentId <guid> certificate <thumbprint> did not pass validation. 
AcceptorFacade: CreateGuest: <guid>

The patch also trims what a guest peer can invoke, which closes the Heartbeat GUID oracle from weaponization step one. (KB4893 does not mention that change.)

For CVE-2026-58072, SaveFiles is gone rather than guarded, and the write paths that remain are confined to six allowlisted folders under the service’s own directory. We called it on a properly authenticated channel with a real agent’s certificate, and it was still refused with nothing written, so the primitive is gone rather than merely out of reach.

What the patch does not do is take back a certificate the console already issued. It binds a login to the connection that made it, and revokes nothing, so on the patched rig a legitimately signed agent certificate authenticated normally. We never ran the literal sequence of stealing a certificate, patching that same console, and reusing it, because our vulnerable and patched rigs were separate hosts with separate agent CAs, so a certificate from one was never valid on the other. So take that one as read from the code rather than proven in the lab, and plan around it regardless. Note too that the CA signing those certificates is not one of the certificates the portal lets you replace.

Conclusion

CVE-2026-58073 shows how an authentication bug can be two lines long and still rate a 9.5. The server had all the information it needed and asked a question one degree off from the right one: whether some agent with this GUID had logged in, rather than whether this connection belonged to that agent. Everything downstream, including a second critical vulnerability that is not exploitable without it, follows from that one missing binding.

Patching to 9.3.0 closes both halves: the impersonation is denied, and the file write is refused even for a caller holding a genuine agent certificate. What it does not undo is a certificate already issued to the wrong party, which is why the log hunt matters as much as the upgrade: a hit is what turns an abstract credential problem into a support case. The agent GUID stays exposed either way. It still travels unencrypted in the client certificate on a TLS 1.2 channel, and every managed endpoint still stores it world-readable on disk.

Cosmos customers were notified about this vulnerability research shortly after the vendor advisory published. If you are interested in learning more about managed services delivered through our Cosmos platform, visit https://bishopfox.com/services/continuous-threat-exposure-management.

For more vulnerability intelligence insights, visit the Bishop Fox Blog.




Jon Williams

By Jon Williams

Staff Security Engineer

As a researcher for the Bishop Fox Threat Enablement & Analysis team, Jon spends his time hunting for vulnerabilities and writing exploits for software on our customers' attack surface. Jon has written and presented research on various topics including enterprise wireless network attacks, bypassing network access controls, and reverse-engineering edge security device firmware.


Ronan

By Ronan Kervella

Sr. Security Engineer

Ronan Kervella is a Senior Security Engineer at Bishop Fox, where he focuses on vulnerability research and exploit development. He is the author of multiple open source-tools and is an active contributor to the Sliver framework. He has advised Fortune 500 brands and startups in industries such as media, healthcare, and software development.


Banksy Fox exploder1

By Threat Enablement & Analysis Team

The Bishop Fox Threat Enablement & Analysis team researches emerging vulnerabilities, exploits, and attacker techniques to understand how new threats translate into real-world risk. The team combines vulnerability research, exploit development, threat intelligence, and offensive security expertise to analyze new disclosures, validate exploitability, and develop methods for identifying affected systems at scale.

Subscribe to our blog

Be first to learn about latest tools, advisories, and findings.