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 = O = "[;]" OU =
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.
- 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. - An agent must be currently logged in.
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.
_logins - 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) |
| 3, 4, 5, 6 |
9.3.0 and later (patched) |
| 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-
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: Path '' is outside of the allowed service folders. Suspicious call from channel Id:
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
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 has successfully logged in. <- the real agent, earlier certificate did not pass validation. <- the attacker's cert Channel used by agent is being replaced with channel Incoming method call 'IssueCertificateForAgent' Channel released for agent The agent clusteredAgentId 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.
- 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’sHeartbeat
, 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’sCertificate
message beforeChangeCipherSpec
, 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 fromHKLM\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. - 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 byCN=Veeam Software
, the console’s own agent root CA. A control run with a random GUID was denied withInvokeDeniedException
, 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. - Turn agent access into a file write.
With the stolen certificate the attacker opens a new connection, authenticates as a legitimate agent, and callsSaveFiles
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. - 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 intoC:\inetpub\wwwroot
, requested it, and got HTTP 200 with our marker string back, executing asIIS 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 holdsSeImpersonatePrivilege
, 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 certificate did not pass validation. AcceptorFacade: CreateGuest:
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.
Subscribe to our blog
Be first to learn about latest tools, advisories, and findings.
Thank You! You have been subscribed.
Recommended Posts
