
Safely detect Veeam Service Provider Console auth bypass CVE-2026-58073
A safe, unauthenticated detector for the KB4893 vulnerabilities in Veeam Service Provider Console (published 2026-08-04). It answers one question per target, before any authentication or TLS: are the KB4893 fixes present on this console?
The headline pair is a chain. CVE-2026-58073 (CVSS 9.5) lets an unauthenticated network peer impersonate a connected management agent and be issued that agent's real certificate, because the agent handshake decides authorization from the GUID the peer wrote into its own certificate. CVE-2026-58072 (CVSS 9.0) is an arbitrary file write reachable once you hold an agent identity. Chained, they are unauthenticated remote code execution on the console that manages every tenant's backups. The same advisory also fixes CVE-2026-58071 (CVSS 8.2, proxied appliance API as Portal Administrator) and CVE-2026-58067 (CVSS 8.7, unauthenticated memory-exhaustion DoS). CVE-2026-58073 and CVE-2026-58072 were reported to Veeam through HackerOne; the advisory does not name the reporter.
This script does not attempt impersonation, request a certificate, or write a file. It reads the router's advertised protocol generation and nothing else.
Yes. The detector is designed for production and assessment use:
Connector handshake naming a receiver that will not exist. No TLS session is negotiated, no
certificate is presented, and is never called.SaveFilesChannelHostProxy.m_multiplexers. No receiver is registered, no channel or multiplexer is built,
no agent record is touched. A Receiver-type handshake would register a name; this tool never
sends one.ConnectionHub.log, each carrying the receiver name bf-probe-<uuid4> so a defender can tell a
scan from an attack. The exact lines are below.VULNERABLE after it proves it is a VSPC
ConnectionHub (see below), so a quiet TCP service cannot be mistaken for an unpatched console.Per target the tool opens two TCP connections and sends one ConnectionHub handshake on each, naming
a receiver bf-probe-<uuid4> that will not exist.
Under the default --transport auto, a target whose transport is not the one implied by its port
costs one extra connection: the wrong-transport probe is rejected during the handshake, before any
receiver name is read, and the right transport is then used for both real probes. Pin
--transport direct or --transport gateway to hold it to exactly two connections — worth doing if
you have quoted a connection count in a change request.
State changed on the server: none. The Connector code path performs a dictionary lookup in
ChannelHostProxy.m_multiplexers, misses, and returns an error. No receiver is registered, no
multiplexer or channel is created, no TLS session is negotiated, no agent record is touched. This
tool never sends a Receiver-type handshake, which is the one that would register a name.
Log entries are written to
%ProgramData%\Veeam\Veeam Availability Console\Log\Server\ConnectionHub.log. Verbatim from a live
9.2.1.33875 ConnectionHub, with timestamps and scope JSON trimmed:
probe 1 (version 6), both patched and unpatched builds:
[INFO] ChannelHostProxy: Accept connection begin {"RemoteEndPoint":"<ip>:<port>","Line":"1"}
[INFO] ChannelHostProxy: Accept connection end {"RemoteEndPoint":"<ip>:<port>","Line":"2",...}
[WARN] ChannelHostProxy: Cannot connect transmitter. Requested receiver not found
(receiver name:bf-probe-<uuid>) {"RemoteEndPoint":"<ip>:<port>","Line":"3",...}
probe 2 (version 7), patched builds only:
the same three lines
probe 2 (version 7), unpatched builds:
[INFO] ChannelHostProxy: Accept connection begin
[WARN] ChannelHostProxy: Handshake failed. Reason:Unsupported client version "7"
[INFO] ChannelHostProxy: Accept connection end
Two connections, six lines, no other entries and no state change — confirmed on a live host.
The literal string bf-probe- in ConnectionHub.log identifies this tool's traffic, so a defender
can attribute it and a scanning team can prove what they sent. Change RECEIVER_PREFIX in the
source if you need a different marker.
The ConnectionHub management-agent router reads a client handshake before any authentication or TLS,
and Request.Read validates the client-advertised protocol version against a hardcoded range. The
fix widened that range in the same build that fixed the CVEs:
| Build | Check | Accepts |
|---|---|---|
<= 9.2.1.33875 (vulnerable) | (uint)(versionByte - 3) <= 3 | 3, 4, 5, 6 |
>= 9.3.0.35057 (patched) | (uint)(versionByte - 3) <= 4 | 3, 4, 5, 6, 7 |
So a handshake advertising version 7 is a clean binary discriminator. The detector sends two probes per target, in this order for a reason (transport detection can add a third — see below):
| Probe | Advertises | Purpose |
|---|---|---|
| 1 | version 6 | Must return Requested receiver not found, proving the target really is a VSPC ConnectionHub |
| 2 | version 7 | A reply means PATCHED; silence means VULNERABLE |
Without the stage-one gate, silence in probe 2 also matches any quiet TCP service on the internet, and firewalls would be reported as vulnerable Veeam consoles.
It works over both paths a management agent uses:
| Transport | Port | Exposure |
|---|---|---|
| Direct to the ConnectionHub | 9999 | usually internal |
| Through a Veeam Cloud Connect gateway | 6180 | internet-facing by design |
The gateway path needs a relay prologue that the direct path must not have, so probe 1 doubles as
transport detection. Under the default --transport auto it tries one transport, and if the
fingerprint gate does not pass, tries the other. Whichever passes is latched, and probe 2 reuses
it — a mixed target file needs no per-host annotation.
The latch is load-bearing. If probe 2 could retry on the other transport, silence would no longer be
attributable to the version check, only to "one of two byte paths did not answer", which is how a
false VULNERABLE gets manufactured.
Which transport is tried first is decided by the port, and that is not cosmetic — the two mismatches fail at very different speeds:
| Mismatch | How the far end reads it | Cost |
|---|---|---|
| Relay prologue → direct hub | int16 meta of hostType 44 / versionByte 0, fails Request.Read's range check | disposed in one round trip |
| Direct handshake → gateway | int32 frame length of 1,012,729,346 | gateway waits for bytes that never arrive; burns the full timeout |
So auto leads with the service that owns the port: gateway first on 6180, direct everywhere
else. That keeps the common case at a single attempt and the expensive mismatch off the fast path.
A probe that fails to open TCP at all short-circuits without trying the second transport, so dead
hosts in a wide sweep cost one timeout, not two.
VULNERABLE means "the KB4893 fixes are not present", not "this is 9.2.1.33875". Builds older than
9.2.1 share the same version check, so they should report protocol 6 and be reported vulnerable too
(inferred from the code, not measured — see Limitations), but the tool cannot separate 9.2.1 from
9.1 or 8.1. Confirm the exact build in the console UI if you need it.
# single host (default TCP/9999)
./cve_2026_58073_check.py vspc.example.com
# explicit port, several hosts
./cve_2026_58073_check.py vspc.example.com:9999 10.0.0.5
# scan a list, one target per line ('#' comments allowed), compact output
./cve_2026_58073_check.py -f targets.txt --brief
# machine-readable output for pipelines
./cve_2026_58073_check.py -f targets.txt --json > results.json
# a Veeam Cloud Connect gateway — the relay transport is detected automatically
./cve_2026_58073_check.py cc-gw.example.com:6180
# pin the transport to skip detection (port then defaults to 6180)
./cve_2026_58073_check.py --transport gateway cc-gw.example.com
# validate the wire codecs with no network access
./cve_2026_58073_check.py --self-test
| Flag | Description |
|---|---|
targets | One or more HOST[:PORT] (port defaults to 9999, or 6180 with --transport gateway) |
-f, --targets-file FILE | Read targets from a file (one per line; # comments) |
--transport {auto,direct,gateway} | How to reach the ConnectionHub. auto (default) detects it per target; gateway prepends the Cloud Connect relay prologue and defaults the port to 6180 |
-p, --port PORT | Override the default port |
--timeout SECS | Per-probe timeout (default: 8) |
--workers N | Concurrent targets (default: 16); output stays in input order |
-b, --brief | Single aligned line per target — ideal for scanning many hosts |
--json | Emit structured JSON, including every probe sent per target |
--no-color | Disable coloured output (also honours NO_COLOR and non-TTY) |
--self-test | Validate the .NET wire codecs and exit; no network access |
An unpatched console (the default two-line output). The [!] marker and VULNERABLE render red
on a TTY:
$ ./cve_2026_58073_check.py vspc.example.com
[!] vspc.example.com:9999: VULNERABLE [protocol-7-rejected]
ConnectionHub rejects protocol 7 but accepts 6, so the KB4893 fixes are absent (<= 9.2.1.33875)
A patched console:
$ ./cve_2026_58073_check.py patched.example.com
[+] patched.example.com:9999: PATCHED [protocol-7-accepted]
ConnectionHub accepts protocol 7, so the KB4893 fixes are present (>= 9.3.0.35057)
A gateway target, with the relay transport auto-detected. The (gateway) suffix names the
transport the verdict was reached over:
$ ./cve_2026_58073_check.py cc-gw.example.com:6180
[!] cc-gw.example.com:6180 (gateway): VULNERABLE [protocol-7-rejected]
ConnectionHub rejects protocol 7 but accepts 6, so the KB4893 fixes are absent (<= 9.2.1.33875)
Sweeping an estate, one aligned line per host (--brief). Exit status is 1 if any host is
VULNERABLE, else 0 — handy in scripts:
$ ./cve_2026_58073_check.py -f targets.txt --brief; echo "exit: $?"
VULNERABLE vspc.example.com:9999 protocol-7-rejected
PATCHED patched.example.com:9999 protocol-7-accepted
UNAFFECTED fileserver.example.com:9999 not-vspc
ERROR unused.example.com:9999 unreachable
exit: 1
Machine-readable output for pipelines (--json). Every probe is included per target, so a
finding can be re-derived from the evidence rather than trusted. transport is the one the verdict
was reached over, and each probe carries the transport it used — so an auto-detected target shows
the rejected attempt too:
$ ./cve_2026_58073_check.py vspc.example.com --json
[
{
"target": "vspc.example.com:9999",
"host": "vspc.example.com",
"port": 9999,
"transport": "direct",
"verdict": "VULNERABLE",
"reason": "protocol-7-rejected",
"detail": "ConnectionHub rejects protocol 7 but accepts 6, so the KB4893 fixes are absent (<= 9.2.1.33875)",
"protocol_version": 6,
"affected_cves": [
"CVE-2026-58073",
"CVE-2026-58072",
"CVE-2026-58071",
"CVE-2026-58067"
],
"probes": [
{
"version_byte": 6,
"transport": "direct",
"connected": true,
"responded": true,
"status": "Error",
"message": "Cannot connect transmitter. Requested receiver not found (receiver name:bf-probe-b7c40be0-e2e8-41dc-9fc3-cbbd7345954a)",
"error": ""
},
{
"version_byte": 7,
"transport": "direct",
"connected": true,
"responded": false,
"status": "",
"message": "",
"error": ""
}
]
}
]
| Verdict | Reason tag | Meaning |
|---|---|---|
VULNERABLE | protocol-7-rejected | Confirmed VSPC ConnectionHub that accepts protocol 6 and rejects 7. The KB4893 fixes are absent (<= 9.2.1.33875). |
PATCHED | protocol-7-accepted | Confirmed VSPC ConnectionHub that accepts protocol 7. The KB4893 fixes are present (>= 9.3.0.35057). |
UNAFFECTED | not-vspc | Accepted TCP but did not answer a valid ConnectionHub handshake on any transport tried, so it is not a VSPC ConnectionHub. |
INCONCLUSIVE | unexpected-reply | Answered the fingerprint probe with something other than Requested receiver not found. |
INCONCLUSIVE | inconclusive-discriminator | Passed the fingerprint gate, then answered the version-7 probe in a way that is neither a pass nor a fail — or that second connection failed outright. Retry. |
ERROR | unreachable | Could not connect, or the Cloud Connect gateway refused the relay prologue on the first transport tried (which under auto means any target on port 6180). |
| Code | Meaning |
|---|---|
0 | No target was VULNERABLE |
1 | At least one target is VULNERABLE |
2 | Usage error (bad arguments / unreadable targets file) |
--transport auto too; pin --transport direct to
keep the untested path off a sweep entirely.VULNERABLE verdict speaks to patch state, not to whether anyone exploited
the console. Exploitation leaves its own traces in the console's logs on both patched and unpatched
builds; hunt those separately.Upgrade to Veeam Service Provider Console 9.3.0.35057 or later (KB4893). All four issues are fixed in that one build, and there is no 9.2.x backport, so remediation is a version upgrade rather than a hotfix.
Two things the upgrade does not do. It does not restrict who can reach TCP/9999, which should only answer the subnets your management agents live in. And it does not revoke an agent certificate the console already issued, including one issued to an attacker while it was unpatched. If you find evidence of exploitation, open a Veeam Support case for guidance on compromised agent certificates: rotating them is not a documented procedure, and the certificates you can manage in the portal are not the CA that signs agent certificates.
This code is distributed under an MIT license.
Usage of this tool for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state, and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program.