
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 SaveFiles is never called.ChannelHostProxy.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.