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

VSPC's login check keyed only on agent GUID, letting a self-signed cert steal an agent's certificate, then SaveFiles path abuse wrote to IIS webroot for unauthenticated RCE.

- Payout: Research
- Severity: critical
- Program: Veeam Service Provider Console
- Disclosed: 2026-08-26
- CVE: CVE-2026-58073, CVE-2026-58072
- Write-up quality: 9/10
- Original: https://bishopfox.com/blog/a-guid-is-not-a-credential-unauthenticated-rce-in-veeam-service-provider-console
- Library page: https://bugbountylibrary.com/r/blog%3Abishopfox-com-blog-a-guid-is-not-a-credential-unauthenticated-rce-in-veeam-service-provider-console
- Tags: area:cloud domain:infra impact:internal-access impact:privesc impact:rce kind:research platform:research sev:critical tech:windows technique:chained technique:source-review vuln:auth-bypass vuln:path-traversal vuln:rce

**Target:** VSPC ConnectionHub agent channel on TCP/9999 (protobuf RPC over framed protocol)
**Root cause:** IsLoggedIn checked only whether any agent with the attacker-supplied clusteredAgentId had ever logged in, not whether this connection's certificate matched; weak cert auth accepted self-signed certs. Separately, SaveFiles combined attacker-controlled PathFor/DefinedPathSubfolder/FileName without validation, and .NET Path.Combine discards the base on a rooted second argument.
**Steps:**
1. Obtain a connected agent's GUID (cleartext in TLS 1.2 client Certificate message, or world-readable HKLM\SOFTWARE\Veeam\VAC\Agent\AgentId on managed hosts).
2. Generate a self-signed certificate with subject `CN=<agentId GUID>`, `O = "<companyId GUID>[;<clusteredAgentId GUID>]"`, `OU=<locationId GUID>` and connect to TCP/9999.
3. Complete handshake; IsLoggedIn returns true from the global _logins table, no interceptor is installed, channel is unrestricted.
4. Call `IServerCertificateDispatcher.IssueCertificateForAgent` to receive a PKCS#12 bundle plus password signed by the console's agent root CA.
5. Reconnect with the stolen certificate and call `IDownloadAcceptorPb.SaveFiles` with `DefinedPathSubfolder` set to a rooted path like `C:\inetpub\wwwroot` and a FileName for the payload.
6. Request the dropped page over IIS to execute as IIS APPPOOL\DefaultAppPool.
**How found:** Diffed 9.2.1 vs 9.3.0 builds; noticed the patch was a single function signature change adding a certificate thumbprint to IsLoggedIn, revealing the original check was a bare dictionary lookup on attacker-supplied GUID. Traced weak-mode cert auth default and the missing else branch leaving no interceptor. Separately spotted Path.Combine with two wire-controlled components in SaveFiles.
**Exploitation:** Attacker needs network reach to 9999 and a currently-logged-in agent GUID. Forge self-signed cert with victim GUIDs, pass the GUID-only login check, get unrestricted channel, pull a root-CA-signed PKCS#12 via IssueCertificateForAgent, then use that genuine credential on a fresh connection to call SaveFiles with a rooted destination, writing a webshell into the IIS webroot for code execution as the app pool identity (which also holds SeImpersonatePrivilege).
**Escalation:** Chained auth bypass (58073) into arbitrary file write (58072) into IIS webroot page for RCE; on provider consoles this is control-plane access over all tenants' backups and agent task pushing.
**Insight:** When a patch adds a parameter to an auth check, the pre-patch check was trusting something the peer controls — here a GUID in the cert subject. Also: .NET Path.Combine with a rooted second argument escapes without any `..\`, so traversal signatures miss it; and a missing `else` that leaves no interceptor is a silent full-authorization path.

## Proof of concept (copied exactly from the original report)

**Vulnerable IsLoggedIn GUID-only dictionary lookup**

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

**SaveFiles path combine with attacker subfolder**

```
FromVmbpDownloader.SaveFileOnDisk( 
 Path.Combine(_vmbpShareFolder[Convert(package.PathFor)], 
 package.DefinedPathSubfolder ?? string.Empty), // attacker-controlled 
 package.Files, stream, CancellationToken.None);
```

**SaveFileOnDisk writes attacker-controlled filename**

```
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);
```

