Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/recon/persistence_suggester.rb
27907 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
8
include Msf::Auxiliary::Report
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Persistence Exploit Suggester',
15
'Description' => %q{
16
This module suggests persistence modules that can be used.
17
The modules are suggested based on the architecture and platform
18
that the user has a shell opened as well as the available exploits
19
in meterpreter.
20
It's important to note that not all modules will be checked.
21
Exploits are chosen based on these conditions: session type,
22
platform, architecture, and required default options.
23
},
24
'License' => MSF_LICENSE,
25
'Author' => [ 'h00die' ],
26
'Platform' => all_platforms,
27
'SessionTypes' => [ 'meterpreter', 'shell' ],
28
'Notes' => {
29
'Stability' => [],
30
'Reliability' => [],
31
'SideEffects' => []
32
}
33
)
34
)
35
register_options [
36
Msf::OptInt.new('SESSION', [ true, 'The session to run this module on' ]),
37
Msf::OptBool.new('SHOWDESCRIPTION', [true, 'Displays a detailed description for the available exploits', false])
38
]
39
40
register_advanced_options(
41
[
42
# most linux persistence modules are arch-cmd but for payload purposes only
43
# but usually we end up with a meterpreter session, thus making these invalid
44
# so disable this check by default
45
Msf::OptBool.new('ValidateArch', [true, 'Validate architecture', false]),
46
Msf::OptBool.new('ValidatePlatform', [true, 'Validate platform', true]),
47
Msf::OptBool.new('ValidateMeterpreterCommands', [true, 'Validate Meterpreter commands', false]),
48
# https://github.com/rapid7/rex-text/blob/a72151d409cd812978f63ad0c330efbc8f44b977/lib/rex/text/color.rb#L13
49
Msf::OptString.new('Colors', [false, 'Valid, Invalid and Ignored colors for module checks (unset to disable)', 'grn/red/blu'])
50
]
51
)
52
end
53
54
def valid_colors?(color_str = datastore['Colors'])
55
tokens = color_str.split('/')
56
tokens.each do |tok|
57
print_warning "#{tok} is unlikely to have any functionality for printing colors." if tok == 'clr'
58
59
unless Rex::Text::Color::SUPPORTED_FORMAT_CODES.include?("%#{tok}")
60
print_error "#{tok} is NOT valid color. Please see https://github.com/rapid7/rex-text/blob/a72151d409cd812978f63ad0c330efbc8f44b977/lib/rex/text/color.rb#L13 for valid color options"
61
return false
62
end
63
end
64
true
65
end
66
67
def all_platforms
68
Msf::Module::Platform.subclasses.collect { |c| c.realname.downcase }
69
end
70
71
def session_arch
72
# Prefer calling native arch when available, as most LPEs will require this (e.g. x86, x64) as opposed to Java/Python Meterpreter's values (e.g. Java, Python)
73
session.respond_to?(:native_arch) ? session.native_arch : session.arch
74
end
75
76
def is_module_arch?(mod)
77
mod_arch = mod.target.arch || mod.arch
78
mod_arch.include?(session_arch)
79
rescue StandardError => e
80
print_error "Failed to check module arch for #{mod.fullname} => #{e}"
81
end
82
83
def is_module_wanted?(mod)
84
mod[:result][:incompatibility_reasons].empty?
85
end
86
87
def is_session_type?(mod)
88
# There are some modules that do not define any compatible session types.
89
# We could assume that means the module can run on all session types,
90
# Or we could consider that as incorrect module metadata.
91
mod.session_types.include?(session.type)
92
end
93
94
def is_module_platform?(mod)
95
return false if mod.target.nil?
96
97
platform_obj = Msf::Module::Platform.find_platform session.platform
98
99
module_platforms = mod.target.platform ? mod.target.platform.platforms : mod.platform.platforms
100
module_platforms.include? platform_obj
101
rescue ArgumentError => e
102
# When not found, find_platform raises an ArgumentError
103
elog('Could not find a platform', error: e)
104
return false
105
end
106
107
def has_required_module_options?(mod)
108
get_all_missing_module_options(mod).empty?
109
end
110
111
def get_all_missing_module_options(mod)
112
missing_options = []
113
mod.options.each_pair do |option_name, option|
114
missing_options << option_name if option.required && option.default.nil? && mod.datastore[option_name].blank?
115
end
116
missing_options
117
end
118
119
def valid_incompatibility_reasons(mod, verify_reasons)
120
# As we can potentially ignore some `reasons` (e.g. accepting arch values which are, on paper, not compatible),
121
# this keeps track of valid reasons why we will not consider the module that we are evaluating to be valid.
122
valid_reasons = []
123
valid_reasons << "Missing required module options (#{get_all_missing_module_options(mod).join('. ')})" unless verify_reasons[:has_required_module_options]
124
125
incompatible_opts = []
126
incompatible_opts << 'architecture' unless verify_reasons[:is_module_arch]
127
incompatible_opts << 'platform' unless verify_reasons[:is_module_platform]
128
incompatible_opts << 'session type' unless verify_reasons[:is_session_type]
129
valid_reasons << "Not Compatible (#{incompatible_opts.join(', ')})" if incompatible_opts.any?
130
131
valid_reasons << 'Missing/unloadable Meterpreter commands' if verify_reasons[:missing_meterpreter_commands].any?
132
valid_reasons
133
end
134
135
def set_module_options(mod)
136
ignore_list = ['ACTION', 'TARGET'].freeze
137
datastore.each_pair do |k, v|
138
mod.datastore[k] = v unless ignore_list.include?(k.upcase)
139
end
140
if !mod.datastore['SESSION'] && session.present?
141
mod.datastore['SESSION'] = session.sid
142
end
143
end
144
145
def set_module_target(mod)
146
session_platform = Msf::Module::Platform.find_platform(session.platform)
147
target_index = mod.targets.find_index do |target|
148
# If the target doesn't define its own compatible platforms or architectures, default to the parent (module) values.
149
target_platforms = target.platform&.platforms || mod.platform.platforms
150
target_architectures = target.arch || mod.arch
151
152
target_platforms.include?(session_platform) && target_architectures.include?(session_arch)
153
end
154
mod.datastore['Target'] = target_index if target_index
155
end
156
157
def setup
158
return unless session
159
160
print_status "Collecting persistence modules for #{session.session_type}..."
161
162
setup_validation_options
163
if valid_colors?
164
setup_color_options
165
else
166
fail_with(Failure::BadConfig, 'Colors options set incorrectly')
167
end
168
169
# Collects persistence modules into an array
170
@persistence_modules = []
171
exploit_refnames = framework.exploits.module_refnames
172
exploit_refnames.each_with_index do |name, index|
173
print "%bld%blu[*]%clr Collecting exploit #{index + 1} / #{exploit_refnames.count}\r"
174
next unless name.include? '/persistence/'
175
176
mod = framework.exploits.create name
177
next unless mod
178
179
set_module_options mod
180
set_module_target mod
181
verify_result = verify_mod(mod)
182
@persistence_modules << { module: mod, result: verify_result } if verify_result[:has_check]
183
end
184
end
185
186
def verify_mod(mod)
187
return { has_check: false } unless mod.is_a?(Msf::Exploit::Local) && mod.has_check?
188
189
result = {
190
has_check: true,
191
is_module_platform: (@validate_platform ? is_module_platform?(mod) : true),
192
is_module_arch: (@validate_arch ? is_module_arch?(mod) : true),
193
has_required_module_options: has_required_module_options?(mod),
194
missing_meterpreter_commands: (@validate_meterpreter_commands && session.type == 'meterpreter') ? meterpreter_session_incompatibility_reasons(session) : [],
195
is_session_type: is_session_type?(mod)
196
}
197
result[:incompatibility_reasons] = valid_incompatibility_reasons(mod, result)
198
result
199
end
200
201
def setup_validation_options
202
@validate_arch = datastore['ValidateArch']
203
@validate_platform = datastore['ValidatePlatform']
204
@validate_meterpreter_commands = datastore['ValidateMeterpreterCommands']
205
end
206
207
def setup_color_options
208
@valid_color, @invalid_color, @ignored_color =
209
(datastore['Colors'] || '').split('/')
210
211
@valid_color = "%#{@valid_color}" unless @valid_color.blank?
212
@invalid_color = "%#{@invalid_color}" unless @invalid_color.blank?
213
@ignored_color = "%#{@ignored_color}" unless @ignored_color.blank?
214
end
215
216
def show_found_exploits
217
unless datastore['VERBOSE']
218
print_status "#{@persistence_modules.length} exploit checks are being tried..."
219
return
220
end
221
222
vprint_status "The following #{@persistence_modules.length} exploit checks are being tried:"
223
@persistence_modules.each do |x|
224
vprint_status x[:module].fullname
225
end
226
end
227
228
def run
229
runnable_exploits = @persistence_modules.select { |mod| is_module_wanted?(mod) }
230
if runnable_exploits.empty?
231
print_error 'No suggestions available.'
232
vprint_line
233
vprint_session_info
234
vprint_status unwanted_modules_table(@persistence_modules.reject { |mod| is_module_wanted?(mod) })
235
return
236
end
237
238
show_found_exploits
239
results = runnable_exploits.map.with_index do |mod, index|
240
print "%bld%blu[*]%clr Running check method for exploit #{index + 1} / #{runnable_exploits.count}\r"
241
begin
242
checkcode = mod[:module].check
243
rescue StandardError => e
244
elog("#Local Persistence Suggester failed with: #{e.class} when using #{mod[:module].shortname}", error: e)
245
vprint_error "Check with module #{mod[:module].fullname} failed with error #{e.class}"
246
next { module: mod[:module], errors: ['The check raised an exception.'] }
247
end
248
249
if checkcode.nil?
250
vprint_error "Check failed with #{mod[:module].fullname} for unknown reasons"
251
next { module: mod[:module], errors: ['The check failed for unknown reasons.'] }
252
end
253
254
# See def is_check_interesting?
255
unless is_check_interesting? checkcode
256
vprint_status "#{mod[:module].fullname}: #{checkcode.message}"
257
next { module: mod[:module], errors: [checkcode.message] }
258
end
259
260
# Prints the full name and the checkcode message for the exploit
261
print_good "#{mod[:module].fullname}: #{checkcode.message}"
262
263
# If the datastore option is true, a detailed description will show
264
if datastore['SHOWDESCRIPTION']
265
# Formatting for the description text
266
Rex::Text.wordwrap(Rex::Text.compress(mod[:module].description), 2, 70).split(/\n/).each do |line|
267
print_line line
268
end
269
end
270
271
next { module: mod[:module], checkcode: checkcode.message }
272
end
273
274
print_line
275
print_status valid_modules_table(results)
276
277
vprint_line
278
vprint_session_info
279
vprint_status unwanted_modules_table(@persistence_modules.reject { |mod| is_module_wanted?(mod) })
280
281
report_data = {}
282
results.each do |result|
283
report_data[result[:module].fullname] = result[:checkcode] if result[:checkcode]
284
end
285
286
report_note({
287
host: session.session_host,
288
type: 'persistence.suggested_module',
289
data: report_data
290
})
291
end
292
293
def valid_modules_table(results)
294
name_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new
295
check_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new
296
297
# Split all the results by their checkcode.
298
# We want the modules that returned a checkcode to be at the top.
299
checkcode_rows, without_checkcode_rows = results.partition { |result| result[:checkcode] }
300
rows = (checkcode_rows + without_checkcode_rows).map.with_index do |result, index|
301
color = result[:checkcode] ? @valid_color : @invalid_color
302
check_res = result.fetch(:checkcode) { result[:errors].join('. ') }
303
name_styler.merge!({ result[:module].fullname => color })
304
check_styler.merge!({ check_res => color })
305
306
[
307
index + 1,
308
result[:module].fullname,
309
result[:checkcode] ? 'Yes' : 'No',
310
check_res
311
]
312
end
313
314
Rex::Text::Table.new(
315
'Header' => "Valid modules for session #{session.sid}:",
316
'Indent' => 1,
317
'Columns' => [ '#', 'Name', 'Potentially Vulnerable?', 'Check Result' ],
318
'SortIndex' => -1,
319
'WordWrap' => false, # Don't wordwrap as it messes up coloured output when it is broken up into more than one line
320
'ColProps' => {
321
'Name' => {
322
'Stylers' => [name_styler]
323
},
324
'Potentially Vulnerable?' => {
325
'Stylers' => [::Msf::Ui::Console::TablePrint::CustomColorStyler.new({ 'Yes' => @valid_color, 'No' => @invalid_color })]
326
},
327
'Check Result' => {
328
'Stylers' => [check_styler]
329
}
330
},
331
'Rows' => rows
332
)
333
end
334
335
def unwanted_modules_table(unwanted_modules)
336
arch_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new
337
platform_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new
338
session_type_styler = ::Msf::Ui::Console::TablePrint::CustomColorStyler.new
339
340
rows = unwanted_modules.map.with_index do |mod, index|
341
begin
342
platforms = mod[:module].target.platform&.platforms&.any? ? mod[:module].target.platform.platforms : mod[:module].platform.platforms
343
rescue NoMethodError
344
platforms = nil
345
end
346
platforms ||= []
347
begin
348
arch = mod[:module].target.arch&.any? ? mod[:module].target.arch : mod[:module].arch
349
rescue NoMethodError
350
arch = nil
351
end
352
arch ||= []
353
354
arch.each do |a|
355
if a != session_arch
356
if @validate_arch
357
color = @invalid_color
358
else
359
color = @ignored_color
360
end
361
else
362
color = @valid_color
363
end
364
365
arch_styler.merge!({ a.to_s => color })
366
end
367
368
platforms.each do |module_platform|
369
if module_platform != ::Msf::Module::Platform.find_platform(session.platform)
370
if @validate_platform
371
color = @invalid_color
372
else
373
color = @ignored_color
374
end
375
else
376
color = @valid_color
377
end
378
379
platform_styler.merge!({ module_platform.realname => color })
380
end
381
382
mod[:module].session_types.each do |session_type|
383
color = session_type == session.type ? @valid_color : @invalid_color
384
session_type_styler.merge!(session_type.to_s => color)
385
end
386
387
[
388
index + 1,
389
mod[:module].fullname,
390
mod[:result][:incompatibility_reasons].join('. '),
391
platforms.any? ? platforms.map(&:realname).sort.join(', ') : 'No defined platforms',
392
arch.any? ? arch.sort.join(', ') : 'No defined architectures',
393
mod[:module].session_types.any? ? mod[:module].session_types.sort.join(', ') : 'No defined session types'
394
]
395
end
396
397
Rex::Text::Table.new(
398
'Header' => "Incompatible modules for session #{session.sid}:",
399
'Indent' => 1,
400
'Columns' => [ '#', 'Name', 'Reasons', 'Platform', 'Architecture', 'Session Type' ],
401
'WordWrap' => false,
402
'ColProps' => {
403
'Architecture' => {
404
'Stylers' => [arch_styler]
405
},
406
'Platform' => {
407
'Stylers' => [platform_styler]
408
},
409
'Session Type' => {
410
'Stylers' => [session_type_styler]
411
}
412
},
413
'Rows' => rows
414
)
415
end
416
417
def vprint_session_info
418
vprint_status 'Current Session Info:'
419
vprint_status " Session Type: #{session.type}"
420
vprint_status " Architecture: #{session_arch}"
421
vprint_status " Platform: #{session.platform}"
422
end
423
424
def is_check_interesting?(checkcode)
425
[
426
Msf::Exploit::CheckCode::Vulnerable,
427
Msf::Exploit::CheckCode::Appears,
428
Msf::Exploit::CheckCode::Detected
429
].include? checkcode
430
end
431
432
def print_status(msg = '')
433
super(session ? "#{session.session_host} - #{msg}" : msg)
434
end
435
436
def print_good(msg = '')
437
super(session ? "#{session.session_host} - #{msg}" : msg)
438
end
439
440
def print_error(msg = '')
441
super(session ? "#{session.session_host} - #{msg}" : msg)
442
end
443
end
444
445