NEW

Les pentests automatisés par IA sont arrivés chez Mobeta

CVE-2026-59309 & CVE-2026-59310: Patch-diffing VMware vCenter to auth bypass and RCE

CVE-2026-59309 checker output: the SASL SRP bind completes as administrator@vsphere.local with no password, then lists the members of the Administrators group.

On its face, VMSA-2026-0006 reads like most VMware advisories: a version number, a table of affected products, two CVE identifiers with a CVSS score attached. What made this one worth stopping for is that both entries score 9.8, both are described as reachable without credentials, and one of them was already flagged as under active in-the-wild exploitation before we opened a single file. At the time we started, no public technical write-up existed for either bug; just the advisory text and a patched build number to compare against.

So we did what the advisory couldn’t tell us: we diffed the patch.

The diff pipeline (1/3)

The advisory tells you what, not where

AdvisoryVMSA-2026-0006
CVE-2026-59309VMware Directory Service, authentication bypass · CVSS 9.8
CVE-2026-59310vCenter Syslog receiver, directory traversal → arbitrary code execution · CVSS 9.8
AffectedvCenter Server Appliance (vCSA) 8.0.3.00900 (build 15564605) and earlier 8.0.3.x
Fixed in8.0.3.01000 (build 15566761)

Two critical, pre-auth bugs, fixed in the same release, with a build number as the only visible artifact of the fix. Everything else (the vulnerable function, the missing check, the exact reachable code path) has to be recovered from the patch itself.

Why RPM-diffing alone gets you nowhere

The instinct is to diff package lists between the two patch ISOs and look for anything that changed. That instinct dies fast here.

EXHIBIT AOne build bump, two unrelated fixes 8.0.3.00900 → 8.0.3.01000 build 15564605 → 15566761 · 538 RPMs shipped 63 RPMs VMware-*, rebuilt vmware-directory-* / lwraft / sts identity & auth path LDAP-ish RPC · TCP 389/636/2020 vmware-syslog templates log ingestion path rsyslog dynafile · UDP/TCP 514 CVE-2026-59309 auth bypass · CVSS 9.8 CVE-2026-59310 path traversal · CVSS 9.8 two unrelated bugs, filed under one version number

A single build bump moves 63 of 538 RPMs in lockstep: every VMware-* package gets rebuilt, whether or not it contains a fix. Two unrelated security patches are filed under one version number, and the RPM list alone can’t tell you which of the 63 actually changed for a reason.

8.0.3.01000 is a full product build, not a targeted hotfix. 63 of 538 RPMs differ by filename between 8.0.3.00900 and 8.0.3.01000, every VMware-owned package bumped together, which is normal for a quarterly-style build train and tells us nothing about where either fix actually lives. Filename or version diffing stops at “these 63 packages are different,” which is a list of suspects, not a location.

The one thing it does hand us for free: rsyslog-8.2306.0-3.ph4.x86_64.rpm, the open-source rsyslog daemon that backs the vCenter syslog receiver, has a byte-identical SHA-256 hash in both ISOs. Whatever CVE-2026-59310 turns out to be, it isn’t a bug in upstream rsyslog. It has to be in VMware’s own configuration or code around it.

sha256sum of the rsyslog RPM in both ISOs printing the identical hash 0510041c1c5d88243e440ebe663724130aa2d3b89805d879eaeb6fd194883e6c for the 00900 and 01000 builds

The rsyslog RPM hashes to the same SHA-256 in both ISOs, so whatever CVE-2026-59310 is, it lives in VMware’s configuration around rsyslog, not in the upstream daemon.

Building a pipeline instead of guessing

Since the package list wasn’t going to narrow anything down, we went one level deeper: extract every RPM on both sides into full filesystem trees, and hash-diff every file they contain.

EXHIBIT BThe content-hash diffing pipeline ISO 8.0.3.00900 vulnerable ISO 8.0.3.01000 fixed extract 538 RPMs each · rpm → cpio.gz → cpio → filesystem tree fs_00900/ vs fs_01000/ sha256sum every file, both trees diff the two sorted hash listings rsyslog.rpm byte-identical ruled out 1,061 files changed across the 63-RPM set triage · path relevance: */syslog/*, */vmdir/*, */afd/*, *sts* · file type: compiled/bytecode over plain config · diff magnitude: a few changed bytes beats a full rebuild priority target set root-cause isolation → Part 2 / Part 3

The pipeline:

  • extract both ISOs’ full RPM payload into filesystem trees,
  • SHA-256 every file on both sides,
  • diff the hash listings rather than the package manifest.

rsyslog itself comes back byte-identical; one suspect eliminated with certainty before any manual review starts.

Each .rpm has to be unpacked in two hops (RPM → cpio.gzcpio → real files) before it yields a comparable filesystem tree, repeated across all 538 packages on each side. Once both trees exist, sha256sum over every file plus a diff of the two sorted hash listings replaces “63 packages changed” with a real, file-level change set: 1,061 files differ in content between 8.0.3.00900 and 8.0.3.01000.

That’s still too much to review by hand, so we triaged it with three heuristics, in priority order:

  1. Path relevance: anything under */syslog/*, */vmdir/*, */lwraft/*, */afd/*, or matching *sts*, since those paths map directly to the two components named in the advisory.
  2. Artifact type: compiled binaries and JVM bytecode ranked above plain config, since a directory-traversal-to-RCE bug and an auth-bypass bug are both far more likely to live in logic than in a comment-adjacent config value, though config wasn’t ignored, and as it turned out, one of the two fixes lived exactly there.
  3. Diff magnitude: a handful of changed lines or opcodes in an otherwise-identical file is a much stronger security-patch signal than a file that differs everywhere, which is usually just a recompile picking up a new embedded timestamp or build path.

That narrowed 1,061 files down to a short, reviewable list sitting inside the two component families the advisory already named.

Two threads, two write-ups

The triaged set split cleanly along the same line the advisory drew: one cluster of changes inside the identity/directory-service packages, one cluster inside the syslog templates VAMI ships for the built-in log receiver. Different protocols, different listeners, different attacker footholds; the only thing they share is a version number.

EXHIBIT CTwo unauthenticated paths, one appliance ATTACKER no credentials vCenter Server Appliance rsyslog receiver UDP/TCP 514 no auth on any listener Directory Service vmdird / STS RPC 389 / 636 / 2020 CVE-2026-59310 path traversal → Part 2 CVE-2026-59309 auth bypass → Part 3 both paths reachable pre-auth · CVSS 9.8 each

Two independent, unauthenticated paths into the same appliance: the syslog receiver on UDP/TCP 514, and the Directory Service’s vmdir/STS RPC surface. Neither requires a foothold on the other; they just happen to have been fixed in the same build.

We picked the syslog path first: it’s the smaller blast radius to isolate (a config template, not a compiled RPC surface) and it’s the one already reported as being exploited in the wild. Here’s what we found.

CVE-2026-59310: The Syslog Path Traversal (2/3)

CWE-22 (Path Traversal) → pre-authentication arbitrary file write, escalating to remote code execution under the advisory’s CVSS 9.8 rating.

Where the fix lives

vCenter’s built-in rsyslog instance (the receiver behind the appliance’s UDP/514, plaintext TCP/514 (imptcp), and TLS syslog listener) is configured from a single file:

/usr/lib/vmware-visl-integration/config/vmware-syslog.conf.template

In the vulnerable build, four dynafile templates build the on-disk log path by directly interpolating two syslog message header fields %app-name% and %hostname% with no sanitization at all:

$template defaultLoc,"/var/log/vmware/%app-name%/%app-name%-syslog.log"
$template vpxdLoc,"/var/log/vmware/%app-name%/%app-name%-syslog.log"
$template rsyslogadminLoc,"/var/log/vmware/%app-name%/%app-name%-syslog.log"
$template esxLoc,"/var/log/vmware/esx/%hostname%/%hostname%-syslog.log"

All four dynafile templates splice an attacker-controlled header field straight into a filesystem path; no allow-list, no traversal check.

Both APP-NAME and HOSTNAME are plain, attacker-controlled tokens in an RFC 5424 syslog message header; nothing authenticates or validates them before they’re spliced into a filesystem path and handed to rsyslog’s omfile dynafile writer, which auto-creates missing parent directories ($CreateDirs defaults to on) and then opens/creates the final file for append.

EXHIBIT DSyslog traversal: the full kill chain ATTACKER no credentials RFC5424 msg, HOSTNAME = crafted traversal string rsyslog listener UDP/514 · TCP/514 (imptcp) · TLS/514 (imtcp) all three bound to ruleset “all” no programname/app-name match required if ($hostname != $$myhostname) then ?esxLoc;esxFmt last, unconditional rule: matches almost any external host esxLoc template: /var/log/vmware/esx/<H>/<H>-syslog.log H = %hostname%, interpolated twice, unsanitized (pre-patch) see Exhibit F for how the two <H> slots resolve identically omfile dynafile writer auto-creates missing parent dirs ($CreateDirs on) · writes MSG verbatim /opt/vmware/share/htdocs/configurev2/MOBETA-syslog.log written · attacker-controlled content GET https://<vcenter>:5480/configurev2/MOBETA-syslog.log served statically by VAMI · no auth · readable pre-auth

The full kill chain in one picture: an unauthenticated message on any of three listeners falls through to the last, unconditional ruleset rule, which routes it into esxLoc and writes it, with the attacker’s own message body, to a path they chose. That path lands inside VAMI’s static web root, so the write is readable back over HTTPS with no further steps.

No spoofed app-name required

Of the four templates, esxLoc is the simplest to reach. It’s bound by the ruleset’s last, unconditional rule:

# VC syslog server log collection
if ($hostname != $$myhostname) then ?esxLoc;esxFmt

Bound to every listener via $InputPTCPServerBindRuleset all and $InputUDPServerBindRuleset all, this is the last rule any inbound message can hit.

$InputPTCPServerBindRuleset all and $InputUDPServerBindRuleset all bind every listener (UDP, plaintext TCP, and TLS) to this same ruleset. Any inbound message whose HOSTNAME differs from the appliance’s own hostname (true of essentially anything sent from outside) falls through to esxLoc. No specific programname/app-name value is required; the attacker only has to control HOSTNAME, a plain header token present in every syslog message. The content that lands in the file comes from esxFmt:

$template esxFmt,"%timestamp:::date-rfc3339% %syslogseverity-text% %hostname% %app-name% %msg%\n"

so the attacker’s MSG body (arbitrary bytes, if they want) is written verbatim.

The vendor’s own patch confirms the data flow

The fix in 8.0.3.01000 wraps every one of these substitutions with rsyslog’s built-in secpath-replace property replacer, which strips or neutralizes .. and path-separator sequences from the value before it ever reaches the template:

EXHIBIT E The vendor’s patch, as a diff

-$template defaultLoc,"/var/log/vmware/%app-name%/%app-name%-syslog.log"
+$template defaultLoc,"/var/log/vmware/%app-name:::secpath-replace%/%app-name:::secpath-replace%-syslog.log"
-$template vpxdLoc,"/var/log/vmware/%app-name%/%app-name%-syslog.log"
+$template vpxdLoc,"/var/log/vmware/%app-name:::secpath-replace%/%app-name:::secpath-replace%-syslog.log"
-$template rsyslogadminLoc,"/var/log/vmware/%app-name%/%app-name%-syslog.log"
+$template rsyslogadminLoc,"/var/log/vmware/%app-name:::secpath-replace%/%app-name:::secpath-replace%-syslog.log"
-$template esxLoc,"/var/log/vmware/esx/%hostname%/%hostname%-syslog.log"
+$template esxLoc,"/var/log/vmware/esx/%hostname:::secpath-replace%/%hostname:::secpath-replace%-syslog.log"

The vendor’s own remediation confirms the vulnerable data flow as plainly as it gets: an unsanitized syslog header field flows straight into a filesystem path, and the whole fix is one property replacer added at each substitution point. Every one of the four templates gets the same secpath-replace wrapper; nothing else about the ruleset or the writer changed.

We isolated this by content-hash diffing the two patch ISOs’ full filesystem trees (see Part 1); rsyslog itself came back byte-identical between builds, which is what told us to keep looking in VMware’s own template rather than upstream code.

The “used twice” trick

The esxLoc template drops our attacker string %hostname% into /var/log/vmware/esx/<H>/<H>-syslog.log twice: once as a directory, once as the filename prefix. Both <H> get the identical literal value, but they sit at different filesystem depths, so a payload tuned to cancel one depth won’t cancel the other.

The trick is a quirk of the filesystem root: .. at / resolves back to /. Pad the string with more ../ than either expansion needs, and both walk all the way up to /, where the extra ../ are absorbed as no-ops. From there both push the same forward path, and only the last component differs (the second gets -syslog.log appended). One fixed value, sent once, deterministically writes an attacker-chosen file with attacker-controlled content.

EXHIBIT FOne value, two depths, one target 1st slot: directory component /var/log/vmware/esx/<H> starts 4 levels deep 2nd slot: file basename <H>-syslog.log starts 6 levels deep (inside dir just created) 4 of 16 ../ pop to root 6 of 16 ../ pop to root / remaining ../ are no-ops (root is its own parent) opt/vmware/share/htdocs/configurev2/MOBETA identical forward path, pushed from root: both times /opt/…/configurev2/MOBETA created as a directory (1st slot) …/MOBETA-syslog.log written, attacker content (2nd slot) one HOSTNAME value, sent once: two depths, one deterministic target

Different starting depths, same surplus trick: 16 leading ../ segments overshoot both the 4-deep and the 6-deep starting point, so both expansions bottom out at root and push the identical forward path back down. They diverge only at the very last component.

Concretely, with H = "../"*16 + "opt/vmware/share/htdocs/configurev2/MOBETA":

  1. Directory <H> (start depth 4): 4 of the 16 ../ reach /, the remaining 12 are no-ops, then opt/vmware/share/htdocs/configurev2/MOBETA is pushed → /opt/vmware/share/htdocs/configurev2/MOBETA, created as a directory by rsyslog’s mkdir-parents step.
  2. File <H>-syslog.log (start depth 6, inside the directory just created): 6 of the 16 ../ reach /, the rest are no-ops, the same forward path is pushed, and the final component MOBETA-syslog.log lands it at /opt/vmware/share/htdocs/configurev2/MOBETA-syslog.log.

Impact

/opt/vmware/share/htdocs/ is the document root VAMI (the appliance-management web UI on :5480) serves statically, and configurev2/ is a path under it reachable without further traversal from the HTTP client side. A file planted there via the syslog primitive above is immediately retrievable over HTTPS:

https://<vcenter>:5480/configurev2/MOBETA-syslog.log

Terminal output of the CVE-2026-59310 checker running against 192.168.3.150: it sends the path-traversal syslog message, waits for rsyslog to flush, then fetches the planted file and confirms the marker is present

Firefox loading https://192.168.3.150:5480/configurev2/MOBETA-syslog.log and displaying the planted file content, confirming pre-authentication arbitrary write readable back over VAMI HTTPS

Confirmed end-to-end. The checker sends a crafted RFC 5424 syslog message with the path-traversal HOSTNAME value, then fetches the planted file from VAMI’s HTTPS interface. Marker found in the retrieved content confirms unauthenticated arbitrary write and pre-authentication read-back on the target appliance.

At minimum, that’s an unauthenticated, pre-auth arbitrary file write with attacker-controlled content, readable back over HTTPS, a primitive we confirmed end-to-end. Escalation to full remote code execution, consistent with the advisory’s CVSS 9.8 rating and “arbitrary code execution” language, requires steering the same primitive at an executable sink instead of a static htdocs path; a CGI-executable directory or a cron drop-in are the obvious candidates. We successfully achieved remote code execution through this path; we leave the exercise of escalating this arbitrary file write to RCE to the reader and do not publish the full exploitation chain here.

Preconditions

  1. Network reachability to the vCenter appliance’s syslog listener: UDP/514, plaintext TCP/514 (imptcp), or the TLS syslog port (imtcp); none of them require authentication.
  2. No programname/app-name spoofing needed on the esxLoc path; only HOSTNAME != vCenter’s own hostname, which is trivially true for any external sender.
  3. The target directory tree must be writable by the rsyslog process’s user and exist up through configurev2/; it does, shipped by VAMI itself.

Detection & mitigation

  1. Patch to 8.0.3.01000 or later: it adds secpath-replace sanitization to %app-name%/%hostname% on all four templates.
  2. Network-layer: restrict inbound UDP/514, TCP/514, and TCP/6514 to trusted syslog forwarders only.
  3. Detection: alert on any file appearing under /var/log/vmware/** or /opt/vmware/share/htdocs/** whose path contains ..-derived components, or on syslog HOSTNAME/APP-NAME header values containing / or ..; both are invalid per RFC 5424 §6.2.4 and are a strong indicator of this technique.

Detection rules

Two Sigma rules (Sekoia.IO-compatible YAML) cover the network and host dimensions; a YARA rule adds packet-level coverage for Suricata, Zeek, or inline NTA sensors.

title: CVE-2026-59310: vCenter Syslog HOSTNAME Path Traversal Attempt
id: 7f4e2a1c-83b0-4d9e-a5c7-f612e8903421
status: experimental
description: |
  Detects syslog messages sent to vCenter's rsyslog receiver (TCP/UDP 514,
  TLS 6514) whose HOSTNAME header field contains path-traversal sequences.
  In vCenter 8.0.3.00900 and earlier the HOSTNAME field is interpolated
  unsanitized into the esxLoc dynafile path template, enabling unauthenticated
  arbitrary file write (CVE-2026-59310, CVSS 9.8).
  A HOSTNAME value containing ../ is invalid per RFC 5424 s6.2.4 and is not
  produced by any well-behaved syslog sender.
references:
  - https://www.vmware.com/security/advisories/VMSA-2026-0006.html
  - https://www.rfc-editor.org/rfc/rfc5424
author: Raphael Dray, Mobeta
date: 2026/08/21
tags:
  - attack.initial_access
  - attack.t1190
  - attack.persistence
  - attack.t1505.003
logsource:
  category: network_traffic
  product: syslog
detection:
  selection_port:
    dst_port:
      - 514
      - 6514
  selection_payload:
    payload|contains:
      - '../'
      - '%2e%2e%2f'
      - '%2e%2e/'
  condition: selection_port and selection_payload
falsepositives:
  - None; a HOSTNAME containing ../ is RFC-invalid and has no legitimate origin
    from a correctly implemented syslog sender
level: high

Sigma · network traffic: fires on any syslog datagram to ports 514/6514 whose payload carries a traversal sequence, in any encoding. Deploy on a sensor with RFC 5424 visibility or on the receiver itself. A ../ in a syslog HOSTNAME is structurally invalid, so benign matches are near-impossible.

title: CVE-2026-59310: rsyslog File Creation in vCenter VAMI Web Root
id: 3c8a5f2e-94d1-4b7f-b3e6-a017c5284b63
status: experimental
description: |
  Detects file-creation events produced by the rsyslogd process inside
  /opt/vmware/share/htdocs/, the document root served by vCenter's VAMI
  management interface on TCP 5480. rsyslogd has no legitimate reason to
  write to this directory; any file it creates there is a high-confidence
  indicator of successful CVE-2026-59310 exploitation.
references:
  - https://www.vmware.com/security/advisories/VMSA-2026-0006.html
author: Raphael Dray, Mobeta
date: 2026/08/21
tags:
  - attack.initial_access
  - attack.t1190
  - attack.persistence
  - attack.t1505.003
logsource:
  category: file_event
  product: linux
detection:
  selection:
    Image|endswith: '/rsyslogd'
    TargetFilename|startswith:
      - '/opt/vmware/share/htdocs/'
  condition: selection
falsepositives:
  - None; rsyslogd has no legitimate write access to the VAMI web root
level: critical

Sigma · file event: host-level coverage for auditd, Sysmon for Linux, or Falco. It fires on successful exploitation rather than the attempt, making it a post-compromise indicator, and the /rsyslogd Image filter rules out other web-root writers.

rule CVE_2026_59310_vCenter_Syslog_PathTraversal
{
    meta:
        description = "Detects RFC 5424 syslog messages with path-traversal sequences"
                      " in the HOSTNAME field targeting vCenter's rsyslog dynafile"
                      " template (CVE-2026-59310, CVSS 9.8)"
        author      = "Raphael Dray, Mobeta"
        date        = "2026-08-21"
        reference   = "https://www.vmware.com/security/advisories/VMSA-2026-0006.html"
        cve         = "CVE-2026-59310"

    strings:
        // RFC 5424 PRI + VERSION 1 prefix
        $rfc5424_hdr = /^<[0-9]{1,3}>1 /

        // Traversal sequences in the HOSTNAME field (bare and percent-encoded)
        $trav_bare   = "../../"      ascii
        $trav_pct_lo = "%2e%2e%2f"  ascii
        $trav_pct_up = "%2E%2E%2F"  ascii

        // Target path components for the vCenter VAMI htdocs write primitive
        $path_htdocs = "opt/vmware/share/htdocs"  ascii
        $path_conf2  = "configurev2"               ascii

    condition:
        $rfc5424_hdr and
        ( $trav_bare or $trav_pct_lo or $trav_pct_up ) and
        ( $path_htdocs or $path_conf2 )
}

YARA: packet-level detection for live captures or offline pcap (yara -r). The three clauses demand the RFC 5424 PRI and version prefix, an encoded traversal, and a target-path fragment together, so isolated coincidences never trigger it.

That’s the syslog side fully rooted, from advisory to working write primitive. The identity-service side took the same pipeline somewhere different.

CVE-2026-59309: The SRP Auth Bypass (3/3)

Authentication bypass in VMware Directory Service, CVSS 9.8 (per VMSA-2026-0006).

Running the same pipeline on the identity cluster

Part 1’s triage split the 1,061 changed files into two clusters along the lines the advisory drew. The syslog cluster resolved to a single template file: four lines, one property replacer, done. The identity cluster looked, at first, like it had resolved to something narrower still: one changed binary out of five swept. It hadn’t. Chasing that one binary down with a proper section-level diff of the compiled code is what actually cracked this bug open, by ruling the first suspect out completely.

We started from every compiled artifact plausibly in vmdird’s authentication path (the daemon itself, its LDAP-auth library, its client library, its SASL bind handler, and the Likewise LSASS auth-provider plugin vmdird uses for local authentication) and hashed each one on both sides:

vmdird (daemon)usr/lib/vmware-vmdir/sbin/vmdird: byte-identical SHA-256
libvmdirauth.sousr/lib/vmware-vmdir/lib64/libvmdirauth.so: byte-identical SHA-256
libvmdirclient.sousr/lib/vmware-vmdir/lib64/libvmdirclient.so: byte-identical SHA-256
libsaslvmdirdb.sousr/lib/vmware-vmdir/lib64/libsaslvmdirdb.so: byte-identical SHA-256
liblsass_auth_provider_vmdir.soopt/likewise/lib64/liblsass_auth_provider_vmdir.so: SHA-256 differs

vmdird itself (the actual LDAP-speaking directory daemon) never moved. Neither did its own auth or client libraries. The one file that flagged lives one layer down, in the OS-level authentication glue vCenter inherited along with the rest of the Likewise Open codebase. It’s not stripped, either: both builds expose the same three entry points by name: VmDirCheckUserInList, VmDirAuthenticateUserPam, VmDirAuthenticateUserEx, same globals, same symbol count, nothing added or removed from the interface. On a first pass, that’s a promising lead: a changed file, unchanged interface, sitting right in vmdird’s local-auth glue.

It’s also wrong, and a closer look at the compiled sections proves it in about thirty seconds.

The flag that wasn’t a fix

A whole-file SHA-256 only tells you two files differ; it never tells you where. An ELF’s section table lists every section with its file offset and size, so diffing each section’s raw bytes on its own, rather than the file as a whole, turns a vague “changed” into something you can actually pin down:

EXHIBIT GThe flagged binary, zero code changed vmdird daemon · byte-identical SHA-256 = libvmdirauth.so byte-identical SHA-256 = libvmdirclient.so byte-identical SHA-256 = libsaslvmdirdb.so byte-identical SHA-256 = liblsass_auth_provider_vmdir.so SHA-256 differs · flagged for review verified at the section level .text · .rodata · .data · .symtab · .dynsym : SAME .debug_info · .debug_line · .debug_str : differ diff is the embedded compile path only: bora-25333653 → bora-25599006 per-section offset/size diff FALSE POSITIVE function similarity: 143 / 143 match at 1.000000 similarity 0 bytes of code changed : rebuild noise not the fix the one file the SHA-256 sweep flagged turns out to carry zero code changes : the fix is somewhere else

liblsass_auth_provider_vmdir.so’s .text, .rodata, .data, .symtab, and .dynsym are all byte-identical between builds. Only the DWARF debug sections differ, and that difference is entirely the compiler’s embedded build path, bora-25333653bora-25599006. A function-by-function similarity pass confirms it independently: all 143 functions match at effectively 100% similarity. Not one instruction changed.

The symbol table backs this up too: VmDirCheckUserInList, VmDirAuthenticateUserPam, and VmDirAuthenticateUserEx sit at the exact same addresses with the exact same sizes in both builds. The file’s SHA-256 differs because 15,479 bytes of .debug_info/.debug_line/.debug_str differ: DWARF metadata that embeds the compiler’s absolute build directory, which shifted with the version bump the same way it did for every other VMware-owned artifact in this release train (Part 1, Exhibit A). Rebuild the same unchanged source under a new build number and the debug info changes even though the machine code doesn’t. liblsass_auth_provider_vmdir.so is a full false positive: flagged by hash, cleared by section, cleared again by function-level comparison. The fix isn’t here.

That meant re-running the sweep properly: not five hand-picked binaries, but every ELF in the entire 1,061-file changed set, hashing each one’s .text section individually instead of trusting the whole-file hash.

EXHIBIT HOne real change in the identity stack 1,061 files differ by whole-file SHA-256 (Part 1’s full corpus, not just the identity cluster) including a false positive we just ruled out (Exhibit G) .text section hashed individually for every ELF in that set section table offset/size, raw byte range compare: skip everything a recompile touches by default 28 / 1,061 carry a genuine .text difference vpxd (+20B) · vsan libraries (2-175B) · envoy log writers (1B), unrelated components, outside the identity stack 27 of the 28 have nothing to do with vmdird or SASL opt/likewise/lib64/sasl2/libsrp.so.3.0.0 Cyrus SASL SRP mechanism plugin: the SASL mechanism vmdird uses for its LDAP bind path .text grows by 208 bytes · exactly one function resized srp_server_mech_step2.isra.15 · 0x71c → 0x7e9 bytes (symbol table) new PLT import appears: BN_div (not linked in 8.0.3.00900 at all) the only genuine code change anywhere in the identity/auth stack one real hit out of 1,061 flagged files, and it’s a single OpenSSL bignum call added to one function

Only 28 of the 1,061 flagged files carry a real .text difference; almost all of them (vpxd, vSAN’s libraries, Envoy’s log writers) are unrelated components differing by a handful of relocation-noise bytes. Exactly one sits inside the identity/auth stack: opt/likewise/lib64/sasl2/libsrp.so.3.0.0, the Cyrus SASL SRP mechanism plugin. Its .text grows by 208 bytes, one function resizes, and a brand-new PLT import BN_div appears, one 8.0.3.00900 never linked at all.

That last part is the giveaway before a single instruction gets read. The import table on both builds shows BN_div linked only in 8.0.3.01000: a new call to OpenSSL’s big-number division routine, in a SASL plugin whose entire job is modular-exponentiation arithmetic.

diff of the two import tables of libsrp.so, output 10a11 then a single added line BN_div, meaning BN_div is linked only in the 8.0.3.01000 build

Diffing the two import tables says it in one line: BN_div, OpenSSL’s big-number division routine, is a brand-new dependency in the patched libsrp.so, absent from 8.0.3.00900.

The symbol table narrows it further: every function in the file keeps its exact byte size across both builds except one: srp_server_mech_step2.isra.15, which grows from 0x71c to 0x7e9 bytes. A function-similarity pass puts a number on it: every function in the 193-symbol binary matches at effectively 100% similarity except this one, which lands at 0.75, by a wide margin the only real code change anywhere in the file.

nm -S on both builds grepped for srp_server_mech_step2, showing size 0x71c in the vulnerable build and 0x7e9 in the fixed build, and a join over both symbol tables printing only one size mismatch: srp_server_mech_step2.isra.15 0x71c 0x7e9

nm -S puts it beyond doubt: srp_server_mech_step2.isra.15 is the only symbol whose size changed, 0x71c → 0x7e9, and joining the two symbol tables on name confirms nothing else in the file moved.

libsrp.so.3.0.0 is Cyrus SASL’s implementation of SRP (Secure Remote Password), a zero-knowledge password-authenticated key exchange, and the SASL mechanism vmdird uses for LDAP bind. The string cmusaslsecretSRP is still in the binary’s .rodata, the standard attribute name the SRP mechanism reads a user’s password verifier from; this plugin is live, not vendored-but-unused. libsaslvmdirdb.so, the auxprop plugin that supplies those verifiers out of vmdir’s own data store, is byte-identical between builds (see the table above); the bug isn’t in how secrets are stored or fetched. It’s in the protocol arithmetic itself.

The missing check

Decompiling srp_server_mech_step2 in both builds and diffing the two results shows exactly one inserted block, sitting right after an existing check that survives unchanged in both versions:

Ghidra decompiler with the vulnerable build on the left and the patched build on the right, both showing srp_server_mech_step2. The patched pane has a green-highlighted inserted block calling BN_new, BN_CTX_new and BN_div to compute A mod N, BN_is_zero to test the remainder, and returning with the new message Illegal value for A (A mod N == 0) before A is used

Ghidra decompiler, vulnerable build on the left, patched on the right. The entire fix is the green block on the right: BN_div computes A mod N, BN_is_zero tests the remainder, and the bind bails out with the new Illegal value for 'A' (A mod N == 0) message before A is ever used to derive a key.

EXHIBIT IThe missing check and the bypass 8.0.3.00900 · vulnerable UnBuffer(msg) → A, client M1 if (A <= 0) reject (no other check on A) BN_mod_exp(…, A, …, N) CalculateM1(N, g, I, s, A, B, K) compare client M1 == expected M1 match → SASL bind succeeds A is trusted at face value once it’s > 0 8.0.3.01000 · fixed UnBuffer(msg) → A, client M1 if (A <= 0) reject rem = A mod N (BN_div) if (rem == 0) reject ← new "Illegal value for 'A' (A mod N == 0)" BN_mod_exp(…, A, …, N) CalculateM1(…) → compare → bind RFC 5054 §3.1: reject degenerate A/B 208 new bytes, one new BN_div import what the missing check lets an attacker do the bypass, on 8.0.3.00900 attacker opens an SRP SASL bind to vmdird as a known identity (e.g. administrator@vsphere.local), no password needed yet sends A = N (any nonzero multiple of N) as its SRP public value: unvalidated, so it’s accepted server derives S = (A · v^u)^b mod N = 0: independent of the account’s real password verifier v attacker computes the same S = 0 with no secret at all, derives K = H(0), and forges client evidence M1 forged M1 matches → vmdird accepts the bind as authenticated, pre-auth, without the password classic SRP "degenerate public value" attack: RFC 5054 §3.1 exists precisely to block it

Both builds reject a client public value A ≤ 0. Only 8.0.3.01000 goes further: it computes A mod N via the new BN_div call and rejects the bind if the remainder is zero:

  • BN_new/BN_CTX_new for the scratch state
  • BN_is_zero on the remainder
  • bailing out with a brand-new error string, “Illegal value for 'A' (A mod N == 0)”, before A is ever used to derive a session key

That error string is the single cleanest piece of evidence in this whole investigation: it does not exist anywhere in 8.0.3.00900’s string table. A pass over both binaries’ .rodata confirms it: the vulnerable build has a short “Illegal value for 'A'” (guarding the A > 0 check both versions share) and, separately, “Illegal value for 'B'” on the client side, but never the A mod N == 0 variant, and never a BN_div call anywhere in the file. Both the message and the check it belongs to are wholly new in the patch.

rabin2 -z run over both builds and grepped for Illegal value. The vulnerable build lists only three strings: Illegal value for A, Illegal value for B, and SRP: Illegal value for u. The patched build lists the same three plus a fourth, Illegal value for A (A mod N == 0)

rabin2 -z over both builds, in loop order vulnerable then patched: the vulnerable build carries only the three pre-existing Illegal value strings, while the patched build gains a fourth, Illegal value for 'A' (A mod N == 0), the message that guards the new check.

The check being added is A mod N == 0, precisely the safeguard RFC 5054 §3.1 mandates and precisely the one that was missing: “the host MUST abort the authentication attempt if A % N is zero.” Without it, an unauthenticated attacker who knows nothing but a valid identity string (administrator@vsphere.local, for instance) can open an SRP SASL bind against vmdird and send A = N as their public ephemeral value. (Plain A = 0 is the one degenerate value the pre-existing A > 0 guard already rejects; what it misses is every nonzero multiple of N.)

The cryptographic detail (math-heavy). Collapse this if you want to skip the formulas.

The forced collapse of the shared secret

In the SRP protocol, the server derives the shared secret as:

S=(Avu)bmodN

where v is the account’s stored password verifier and u=H(AB) is a deterministic value computed from both public ephemerals. With A0(modN), the product Avu reduces to zero before exponentiation, so the shared secret collapses unconditionally to S=0, regardless of v. The session key is then derived as the SHA-1 hash of the big-endian encoding of S. But BN_bn2bin(0) produces an empty byte string, so:

K=SHA-1(ε)=da39a3ee5e6b4b0d3255bfef95601890afd80709

SHA-1 of the empty string is a public constant. An attacker does not need to know the account’s password, its verifier, or the factorisation of N to arrive at this key.

Forging client evidence

SRP requires the client to prove knowledge of the password by sending M1 alongside A. The Cyrus SASL SRP implementation computes:

M1=H((H(N)H(g))H(I)sABKH(L))

where H is SHA-1, I is the identity string, s is the server-supplied salt, g is the group generator, B is the server’s public ephemeral, and L encodes the offered security-layer options. Every input is either a known constant, a value the attacker chose (A), or a value the server sent openly in step 1 (N, g, s, B). With K=SHA-1(ε) in hand, the attacker computes M1 exactly and sends it in step 2. The server verifies it, finds it correct, and replies with its own evidence value M2 together with the session IVs needed to install the post-bind security layer.

The 3-step bind and mandatory security layers

A Cyrus SASL SRP bind takes three round trips:

  • Step 1: the client sends an identity, and the server responds with N, g, s, and B.
  • Step 2: the client sends A and M1.
  • Step 3: the server replies with M2, and the bind completes.

What the advisory does not surface is that the Cyrus SASL SRP mechanism, as deployed in vmdird, enforces a mandatory security layer: immediately after the step-3 acknowledgement, vmdird installs an AES-OFB and HMAC-SHA1 cipher over the LDAP socket, and any later LDAP PDU sent in plaintext is rejected outright. Full exploitation therefore means operating that post-bind security layer too, under the forged session key.

EXHIBIT J The 3-step SRP bind and mandatory security layer CLIENT SERVER · vmdird 1 2 3 I = administrator@vsphere.local N, g, s (salt), B (server public ephemeral) A (client public ephemeral), M1 (client proof) M2 (server proof), sIV (server IV) ACK → 3-step bind completes MANDATORY SECURITY LAYER · installed over the LDAP socket Cipher: AES-128-OFB (no padding) + HMAC-SHA1 (Encrypt-then-MAC order) Any subsequent plaintext LDAP PDU is rejected. Key material derived from the shared session key K: AES-128 key = K[:16] HMAC-SHA1 key = K (full 20 bytes) enc_iv = sIV dec_iv = cIV (16 zero bytes) ATTACK · SRP zero-key bypass CVE-2026-59309 A = N S = 0 K = SHA-1(empty) = const K known → all layer keys known → attacker issues arbitrary LDAP as the authenticated identity, no password.

The attacker rides this exact exchange. Sending A = N collapses the shared key to a public constant, so once the bind completes, every security-layer key is known as well.

Post-bind: the SASL security layer is also broken

Ghidra analysis of libsrp.so.3.0.0 reveals the security layer parameters directly. The cipher is aes-128-ofb (entry 3 of the cipher_options table at offset 0x0030c3c0); OFB mode requires no padding. The MAC is HMAC-SHA1, applied in Encrypt-then-MAC order: srp_encode at 0x00103810 runs the OFB encryption pass first, then appends the HMAC-SHA1 tag over the resulting ciphertext. On the receive side, _plug_decode at 0x00108280 reads a 4-byte big-endian inner-length field directly from the socket (no outer sockbuf length wrapper) and passes the frame to srp_decode_packet for MAC verification and decryption. Since the attacker controls K, all derived key material is known:

  • AES-128 key: K[:16] (first 16 bytes of K)
  • HMAC-SHA1 key: K (full 20 bytes)
  • Encryption IV (enc_iv): the server-generated sIV, received in the step-2 response
  • Decryption IV (dec_iv): the client-chosen cIV, sent as 16 zero bytes in step 2

An attacker with a valid identity string and TCP access to vmdird’s LDAP port can execute the full chain: forge the 3-step SRP bind with A=N, derive K=SHA-1(ε), compute all security-layer keys from it, and issue arbitrary LDAP requests as the authenticated identity. We confirmed this end-to-end: a subtree search under dc=vsphere,dc=local returns full directory entries including the membership of cn=Administrators, listing Administrator, all domain-joined machine accounts, and every vCenter solution-user service principal. That is unauthenticated read access to the complete vSphere SSO directory, in addition to whatever LDAP write privileges the target identity carries, with no password required.

Terminal output of the CVE-2026-59309 checker running against 192.168.3.150: it completes the 3-step SRP SASL bind as administrator@vsphere.local without sending a password, then queries cn=Administrators returning 9 members including domain machine accounts and solution-user service principals

Confirmed end-to-end. The checker completes the full 3-step SRP SASL bind as administrator@vsphere.local with no password: sends A = N, forges M1 from K=SHA-1(ε), and uses the established AES-OFB security-layer session to query cn=Administrators,cn=Builtin,dc=vsphere,dc=local. The LDAP response lists 9 members, including domain machine accounts and every solution-user service principal.

Detection

Because CVE-2026-59309 turns one crafted bignum inside a valid LDAP message into a bypass, the rules below cover its three observable phases:

  • the connection attempt
  • the post-bind enumeration
  • the packet signature of the degenerate A value
title: CVE-2026-59309: LDAP Connection to vCenter Directory Service from Unexpected Source
id: 9e1b3d7a-c452-4f8e-8b2d-70e3a914c582
status: experimental
description: |
  Detects TCP connections to vCenter Directory Service (vmdird) LDAP ports
  (389, 636, 2020) from hosts outside the designated management network.
  CVE-2026-59309 requires only a TCP connection and a known account identity;
  no credential is needed. A bind attempt from a non-management source is
  a prerequisite for exploitation and warrants investigation.
  Tune filter_mgmt_cidr to match your management-plane CIDR.
references:
  - https://www.vmware.com/security/advisories/VMSA-2026-0006.html
  - https://www.rfc-editor.org/rfc/rfc5054
author: Raphael Dray, Mobeta
date: 2026/08/21
tags:
  - attack.initial_access
  - attack.t1190
  - attack.credential_access
  - attack.t1078.001
logsource:
  category: network_connection
  product: linux
detection:
  selection:
    Initiated: 'true'
    DestinationPort:
      - 389
      - 636
      - 2020
  filter_mgmt_cidr:
    SourceIp|cidr:
      - '10.0.0.0/8'      # adjust to your management-plane CIDR
      - '172.16.0.0/12'
      - '192.168.0.0/16'
  condition: selection and not filter_mgmt_cidr
falsepositives:
  - Legitimate management tooling reaching vmdird from expected subnets;
    tune filter_mgmt_cidr before deployment
level: medium

Sigma · network connection: the earliest signal, firing on any connection from outside the management CIDR to vmdird’s LDAP ports before authentication completes. Scope filter_mgmt_cidr tightly to your legitimate management hosts to keep it quiet.

title: CVE-2026-59309: Successful SASL SRP Bind Followed by Sensitive LDAP Search
id: 2f7c4a9e-d360-4b1c-a8f5-c93b1e075d4a
status: experimental
description: |
  Detects a behavioral pattern consistent with CVE-2026-59309 exploitation:
  a successful SASL SRP bind to vmdird immediately followed by LDAP search
  operations targeting sensitive Directory Information Tree subtrees.
  An attacker exploiting this bug authenticates without knowing the account
  password; the first observable post-bind action is typically enumeration
  of the vSphere SSO directory (cn=Users, cn=Administrators,
  cn=ServicePrincipals). Correlate events within a 5-second window per
  source IP. Requires vmdird access logging at DEBUG or VERBOSE level.
references:
  - https://www.vmware.com/security/advisories/VMSA-2026-0006.html
author: Raphael Dray, Mobeta
date: 2026/08/21
tags:
  - attack.initial_access
  - attack.t1190
  - attack.discovery
  - attack.t1087.002
logsource:
  product: vmware
  service: vmdir
detection:
  bind_success:
    EventType: 'BIND'
    AuthMechanism: 'SASL/SRP'
    Result: 'success'
  sensitive_search:
    EventType: 'SEARCH'
    BaseDN|contains:
      - 'dc=vsphere,dc=local'
      - 'cn=Users'
      - 'cn=Administrators'
      - 'cn=ServicePrincipals'
  condition: bind_success and sensitive_search
falsepositives:
  - Legitimate administrative tooling performing SASL SRP binds followed by
    directory lookups; validate against known management hosts and service accounts
level: high

Sigma · vmdird log: a behavioural rule needing bind and search logging (log_level = VERBOSE in vmdir.cfg). A successful SRP bind followed immediately by a search on a sensitive base DN is the exploit’s signature; service accounts bind once at startup and never enumerate cn=Administrators on the fly.

rule CVE_2026_59309_vCenter_SRP_DegeneratePublicValue
{
    meta:
        description = "Detects LDAP packets containing a Cyrus SASL SRP step-2 bind"
                      " where the client public value A equals the RFC 5054 group"
                      " modulus N, the degenerate value that collapses the shared"
                      " secret to zero (CVE-2026-59309, CVSS 9.8)"
        author      = "Raphael Dray, Mobeta"
        date        = "2026-08-21"
        reference   = "https://www.vmware.com/security/advisories/VMSA-2026-0006.html"
        cve         = "CVE-2026-59309"

    strings:
        // LDAP BindRequest (tag 0x60) with SASL credentials for mechanism "SRP"
        // 60=BindRequest  02 01 03=version 3  04 00=empty DN
        // a3=SaslCredentials  04 03 53 52 50="SRP"
        $ldap_srp_bind = { 60 ?? 02 01 03 04 00 a3 ?? 04 03 53 52 50 }

        // First 32 bytes of RFC 5054 1024-bit group prime N
        // Sending A = N means A starts with exactly these bytes
        $N_1024 = {
            EE AF 0A B9  AD B3 8D D6  9C 33 F8 0A  FA 8F C5 E8
            60 72 61 87  75 FF 3C 0B  9E A2 31 4C  9C 25 65 76
        }

        // First 32 bytes of RFC 5054 2048-bit group prime N
        $N_2048 = {
            AC 6B DB 41  32 4A 9A 9B  F6 06 E8 C3  97 3B E7 36
            29 72 02 24  8B 74 7D 8A  82 35 EF B6  17 F9 C0 AE
        }

    condition:
        $ldap_srp_bind and ( $N_1024 or $N_2048 )
}

YARA: catches A = N by its bytes. The first 32 bytes of the RFC 5054 1024- and 2048-bit primes are fixed public constants that appear verbatim at the head of the A field only in the degenerate case, and $ldap_srp_bind anchors the match to a real LDAP SRP BindRequest. Apply on a 389/636/2020 mirror or to vmdird pcap.

Mitigation

  1. Patch to 8.0.3.01000 or later: it adds the A mod N == 0 rejection (and, per the client-side symmetry visible in the same binary, the equivalent check belongs on any custom SRP client speaking to vmdird too).
  2. Network-layer: vmdird’s LDAP ports (389, 636, 2020) should be reachable only from management networks; this bug requires nothing but a TCP connection and a known account identity, no prior credential. Whether that segmentation actually holds is what an internal network penetration test measures.
  3. Detection: log and alert on SRP SASL bind attempts where the client’s public value A is 0 or a multiple of the advertised N; that’s not a naturally occurring value from a real SRP client and is a strong, specific indicator of this exact technique, well ahead of any generic “anomalous auth success” heuristic.

Closing the series

Three things came out of one build number:

  • a public advisory that told us that two critical bugs existed
  • a diffing pipeline that told us where (down to a single config template for CVE-2026-59310 and a single function, in a single SASL plugin, for CVE-2026-59309)
  • and, for both bugs, a root cause with a data flow an attacker could actually walk

The syslog bug came from following an unsanitized field into a path. The auth bypass came from not trusting the file the initial hash diff pointed at, verifying that lead to destruction at the section level, and widening the net until the one real code change in the entire identity stack turned up in a place a package-name grep would never have suggested: a bundled SASL mechanism plugin, one missing bignum comparison away from a full authentication bypass.

The same patch-diffing and source-level analysis backs our white-box pentest engagements with source access.

Resources

Advisory and standards

  • VMSA-2026-0006: the VMware advisory covering CVE-2026-59309 and CVE-2026-59310.
  • RFC 5424: the syslog protocol. Section 6.2.4 defines the HOSTNAME/APP-NAME fields abused in the traversal.
  • RFC 5054: SRP for TLS. Section 3.1 is the A % N == 0 safeguard that was missing.
  • CWE-22: path traversal, the class behind CVE-2026-59310.

Components and tooling

  • rsyslog property replacer: the secpath-replace replacer the patch adds, and the dynafile mechanics it protects.
  • Cyrus SASL: the SASL library shipping the SRP mechanism plugin (libsrp.so) that carried CVE-2026-59309.
  • Sigma and YARA: the rule formats used for the detection content above.

Related Mobeta work

Ces techniques s’appliquent
à votre périmètre ?

Un échange de 30 min avec nos pentesters certifiés pour cadrer votre exposition. Sans engagement.

Planifier un échange

Quel est votre niveau
d’exposition réel ?

30 minutes avec un expert pour le savoir. Sans engagement.