Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/ui/console/command_dispatcher/developer.rb
56371 views
1
# -*- coding: binary -*-
2
3
class Msf::Ui::Console::CommandDispatcher::Developer
4
5
include Msf::Ui::Console::CommandDispatcher
6
7
@@irb_opts = Rex::Parser::Arguments.new(
8
'-h' => [false, 'Help menu.' ],
9
'-e' => [true, 'Expression to evaluate.']
10
)
11
12
@@time_opts = Rex::Parser::Arguments.new(
13
['-h', '--help'] => [ false, 'Help banner.' ],
14
'--cpu' => [false, 'Profile the CPU usage.'],
15
'--memory' => [false, 'Profile the memory usage.']
16
)
17
18
@@_servicemanager_opts = Rex::Parser::Arguments.new(
19
['-l', '--list'] => [false, 'View the currently running services' ]
20
)
21
22
@@_historymanager_opts = Rex::Parser::Arguments.new(
23
'-h' => [false, 'Help menu.' ],
24
['-l', '--list'] => [true, 'View the current history manager contexts.'],
25
['-d', '--debug'] => [true, 'Debug the current history manager contexts.']
26
)
27
28
def initialize(driver)
29
super
30
@modified_files = modified_file_paths(print_errors: false)
31
end
32
33
def name
34
'Developer'
35
end
36
37
def commands
38
commands = {
39
'irb' => 'Open an interactive Ruby shell in the current context',
40
'pry' => 'Open the Pry debugger on the current module or Framework',
41
'edit' => 'Edit the current module or a file with the preferred editor',
42
'reload_lib' => 'Reload Ruby library files from specified paths',
43
'log' => 'Display framework.log paged to the end if possible',
44
'time' => 'Time how long it takes to run a particular command'
45
}
46
if framework.features.enabled?(Msf::FeatureManager::MANAGER_COMMANDS)
47
commands['_servicemanager'] = 'Interact with the Rex::ServiceManager'
48
commands['_historymanager'] = 'Interact with the Rex::Ui::Text::Shell::HistoryManager'
49
end
50
commands
51
end
52
53
def local_editor
54
framework.datastore['LocalEditor'] ||
55
Rex::Compat.getenv('VISUAL') ||
56
Rex::Compat.getenv('EDITOR') ||
57
Msf::Util::Helper.which('vim') ||
58
Msf::Util::Helper.which('vi')
59
end
60
61
def local_pager
62
framework.datastore['LocalPager'] ||
63
Rex::Compat.getenv('PAGER') ||
64
Rex::Compat.getenv('MANPAGER') ||
65
Msf::Util::Helper.which('less') ||
66
Msf::Util::Helper.which('more')
67
end
68
69
# XXX: This will try to reload *any* .rb and break on modules
70
def reload_file(full_path, print_errors: true)
71
unless File.exist?(full_path) && full_path.end_with?('.rb')
72
print_error("#{full_path} must exist and be a .rb file") if print_errors
73
return
74
end
75
76
# The file must exist to reach this, so we try our best here
77
if full_path.start_with?(Msf::Config.module_directory, Msf::Config.user_module_directory)
78
print_error('Reloading Metasploit modules is not supported (try "reload")') if print_errors
79
return
80
end
81
82
print_status("Reloading #{full_path}")
83
load full_path
84
end
85
86
# @return [Array<String>] The list of modified file paths since startup
87
def modified_file_paths(print_errors: true)
88
files, is_success = modified_files
89
90
unless is_success
91
print_error("Git is not available") if print_errors
92
files = []
93
end
94
95
ignored_patterns = %w[
96
**/Gemfile
97
**/Gemfile.lock
98
**/*_spec.rb
99
**/spec_helper.rb
100
]
101
@modified_files ||= []
102
@modified_files |= files.reject do |file|
103
ignored_patterns.any? { |pattern| File.fnmatch(pattern, file) }
104
end
105
@modified_files
106
end
107
108
def cmd_irb_help
109
print_line 'Usage: irb'
110
print_line
111
print_line 'Open an interactive Ruby shell in the current context.'
112
print @@irb_opts.usage
113
end
114
115
#
116
# Open an interactive Ruby shell in the current context
117
#
118
def cmd_irb(*args)
119
expressions = []
120
121
# Parse the command options
122
@@irb_opts.parse(args) do |opt, idx, val|
123
case opt
124
when '-e'
125
expressions << val
126
when '-h'
127
cmd_irb_help
128
return false
129
end
130
end
131
132
if expressions.empty?
133
print_status('Starting IRB shell...')
134
135
framework.history_manager.with_context(name: :irb) do
136
begin
137
if active_module
138
print_status("You are in #{active_module.fullname}\n")
139
Rex::Ui::Text::IrbShell.new(active_module).run
140
else
141
print_status("You are in the \"framework\" object\n")
142
Rex::Ui::Text::IrbShell.new(framework).run
143
end
144
rescue
145
print_error("Error during IRB: #{$!}\n\n#{$@.join("\n")}")
146
end
147
end
148
149
# Reset tab completion
150
if (driver.input.supports_readline)
151
driver.input.reset_tab_completion
152
end
153
else
154
# XXX: No vprint_status in this context
155
if framework.datastore['VERBOSE']
156
print_status("You are executing expressions in #{binding.receiver}")
157
end
158
159
expressions.each { |expression| eval(expression, binding) }
160
end
161
end
162
163
#
164
# Tab completion for the irb command
165
#
166
def cmd_irb_tabs(_str, words)
167
return [] if words.length > 1
168
169
@@irb_opts.option_keys
170
end
171
172
def cmd_pry_help
173
print_line 'Usage: pry'
174
print_line
175
print_line 'Open the Pry debugger on the current module or Framework.'
176
print_line
177
end
178
179
#
180
# Open the Pry debugger on the current module or Framework
181
#
182
def cmd_pry(*args)
183
if args.include?('-h')
184
cmd_pry_help
185
return
186
end
187
188
begin
189
require 'pry'
190
rescue LoadError
191
print_error('Failed to load Pry, try "gem install pry"')
192
return
193
end
194
195
print_status('Starting Pry shell...')
196
197
Pry.config.history_load = false
198
framework.history_manager.with_context(history_file: Msf::Config.pry_history, name: :pry) do
199
if active_module
200
print_status("You are in the \"#{active_module.fullname}\" module object\n")
201
active_module.pry
202
else
203
print_status("You are in the \"framework\" object\n")
204
framework.pry
205
end
206
end
207
end
208
209
def cmd_edit_help
210
print_line 'Usage: edit [file/to/edit]'
211
print_line
212
print_line "Edit the currently active module or a local file with #{local_editor}."
213
print_line 'To change the preferred editor, you can "setg LocalEditor".'
214
print_line 'If a library file is specified, it will automatically be reloaded after editing.'
215
print_line 'Otherwise, you can reload the active module with "reload" or "rerun".'
216
print_line
217
end
218
219
#
220
# Edit the current module or a file with the preferred editor
221
#
222
def cmd_edit(*args)
223
editing_module = false
224
225
if args.length > 0
226
path = File.expand_path(args[0])
227
elsif active_module
228
editing_module = true
229
path = active_module.file_path
230
end
231
232
unless path
233
print_error('Nothing to edit. Try using a module first or specifying a library file to edit.')
234
return
235
end
236
237
editor = local_editor
238
239
unless editor
240
# ed(1) is the standard editor
241
editor = 'ed'
242
print_warning("LocalEditor or $VISUAL/$EDITOR should be set. Falling back on #{editor}.")
243
end
244
245
# XXX: No vprint_status in this context
246
print_status("Launching #{editor} #{path}") if framework.datastore['VERBOSE']
247
248
unless system(*editor.split, path)
249
print_error("Could not execute #{editor} #{path}")
250
return
251
end
252
253
return if editing_module
254
255
reload_file(path)
256
end
257
258
#
259
# Tab completion for the edit command
260
#
261
def cmd_edit_tabs(str, words)
262
tab_complete_filenames(str, words)
263
end
264
265
def cmd_reload_lib_help
266
cmd_reload_lib('-h')
267
end
268
269
#
270
# Reload Ruby library files from specified paths
271
#
272
def cmd_reload_lib(*args)
273
files = []
274
options = OptionParser.new do |opts|
275
opts.banner = 'Usage: reload_lib lib/to/reload.rb [...]'
276
opts.separator ''
277
opts.separator 'Reload Ruby library files from specified paths.'
278
opts.separator ''
279
280
opts.on '-h', '--help', 'Help banner.' do
281
return print(opts.help)
282
end
283
284
opts.on '-a', '--all', 'Reload all* changed files in your current Git working tree.
285
*Excludes modules and non-Ruby files.' do
286
files.concat(modified_file_paths)
287
end
288
end
289
290
# The remaining unparsed arguments are files
291
files.concat(options.order(args))
292
files.uniq!
293
294
return print(options.help) if files.empty?
295
296
files.each do |file|
297
reload_file(file)
298
rescue ScriptError, StandardError => e
299
print_error("Error while reloading file #{file.inspect}: #{e}:\n#{e.backtrace.to_a.map { |line| " #{line}" }.join("\n")}")
300
end
301
end
302
303
#
304
# Tab completion for the reload_lib command
305
#
306
def cmd_reload_lib_tabs(str, words)
307
tab_complete_filenames(str, words)
308
end
309
310
def cmd_log_help
311
print_line 'Usage: log'
312
print_line
313
print_line 'Display framework.log paged to the end if possible.'
314
print_line 'To change the preferred pager, you can "setg LocalPager".'
315
print_line 'For full effect, "setg LogLevel 3" before running modules.'
316
print_line
317
print_line "Log location: #{File.join(Msf::Config.log_directory, 'framework.log')}"
318
print_line
319
end
320
321
#
322
# Display framework.log paged to the end if possible
323
#
324
def cmd_log(*args)
325
path = File.join(Msf::Config.log_directory, 'framework.log')
326
327
# XXX: +G isn't portable and may hang on large files
328
pager = local_pager.to_s.include?('less') ? "#{local_pager} +G" : local_pager
329
330
unless pager
331
pager = 'tail -n 50'
332
print_warning("LocalPager or $PAGER/$MANPAGER should be set. Falling back on #{pager}.")
333
end
334
335
# XXX: No vprint_status in this context
336
print_status("Launching #{pager} #{path}") if framework.datastore['VERBOSE']
337
338
unless system(*pager.split, path)
339
print_error("Could not execute #{pager} #{path}")
340
end
341
end
342
343
#
344
# Interact with framework's service manager
345
#
346
def cmd__servicemanager(*args)
347
if args.include?('-h') || args.include?('--help')
348
cmd__servicemanager_help
349
return false
350
end
351
352
opts = {}
353
@@_servicemanager_opts.parse(args) do |opt, idx, val|
354
case opt
355
when '-l', '--list'
356
opts[:list] = true
357
end
358
end
359
360
if opts.empty?
361
opts[:list] = true
362
end
363
364
if opts[:list]
365
table = Rex::Text::Table.new(
366
'Header' => 'Services',
367
'Indent' => 1,
368
'Columns' => ['Id', 'Name', 'References']
369
)
370
Rex::ServiceManager.instance.each.with_index do |(name, instance), id|
371
# TODO: Update rex-core to support querying the reference count
372
table << [id, name, instance.instance_variable_get(:@_references)]
373
end
374
375
if table.rows.empty?
376
print_status("No framework services are currently running.")
377
else
378
print_line(table.to_s)
379
end
380
end
381
end
382
383
#
384
# Tab completion for the _servicemanager command
385
#
386
def cmd__servicemanager_tabs(_str, words)
387
return [] if words.length > 1
388
389
@@_servicemanager_opts.option_keys
390
end
391
392
def cmd__servicemanager_help
393
print_line 'Usage: _servicemanager'
394
print_line
395
print_line 'Manage running framework services'
396
print @@_servicemanager_opts.usage
397
print_line
398
end
399
400
#
401
# Interact with framework's history manager
402
#
403
def cmd__historymanager(*args)
404
if args.include?('-h') || args.include?('--help')
405
cmd__historymanager_help
406
return false
407
end
408
409
opts = {}
410
@@_historymanager_opts.parse(args) do |opt, idx, val|
411
case opt
412
when '-l', '--list'
413
opts[:list] = true
414
when '-d', '--debug'
415
opts[:debug] = val.nil? ? true : val.downcase.start_with?(/t|y/)
416
end
417
end
418
419
if opts.empty?
420
opts[:list] = true
421
end
422
423
if opts.key?(:debug)
424
framework.history_manager._debug = opts[:debug]
425
print_status("HistoryManager debugging is now #{opts[:debug] ? 'on' : 'off'}")
426
end
427
428
if opts[:list]
429
table = Rex::Text::Table.new(
430
'Header' => 'History contexts',
431
'Indent' => 1,
432
'Columns' => ['Id', 'File', 'Name']
433
)
434
framework.history_manager._contexts.each.with_index do |context, id|
435
table << [id, context[:history_file], context[:name]]
436
end
437
438
if table.rows.empty?
439
print_status("No history contexts present.")
440
else
441
print_line(table.to_s)
442
end
443
end
444
end
445
446
#
447
# Tab completion for the _historymanager command
448
#
449
def cmd__historymanager_tabs(_str, words)
450
return [] if words.length > 1
451
452
@@_historymanager_opts.option_keys
453
end
454
455
def cmd__historymanager_help
456
print_line 'Usage: _historymanager'
457
print_line
458
print_line 'Manage the history manager'
459
print @@_historymanager_opts.usage
460
print_line
461
end
462
463
#
464
# Time how long in seconds a command takes to execute
465
#
466
def cmd_time(*args)
467
if args.empty? || args.first == '-h' || args.first == '--help'
468
cmd_time_help
469
return true
470
end
471
472
profiler = nil
473
while args.first == '--cpu' || args.first == '--memory'
474
profiler = args.shift
475
end
476
477
begin
478
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
479
command = Shellwords.shelljoin(args)
480
481
case profiler
482
when '--cpu'
483
Metasploit::Framework::Profiler.record_cpu do
484
driver.run_single(command)
485
end
486
when '--memory'
487
Metasploit::Framework::Profiler.record_memory do
488
driver.run_single(command)
489
end
490
else
491
driver.run_single(command)
492
end
493
ensure
494
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
495
elapsed_time = end_time - start_time
496
print_good("Command #{command.inspect} completed in #{elapsed_time} seconds")
497
end
498
end
499
500
def cmd_time_help
501
print_line 'Usage: time [options] [command]'
502
print_line
503
print_line 'Time how long a command takes to execute in seconds. Also supports profiling options.'
504
print_line
505
print_line ' Usage:'
506
print_line ' * time db_import ./db_import.html'
507
print_line ' * time show exploits'
508
print_line ' * time reload_all'
509
print_line ' * time missing_command'
510
print_line ' * time --cpu db_import ./db_import.html'
511
print_line ' * time --memory db_import ./db_import.html'
512
print @@time_opts.usage
513
print_line
514
end
515
516
protected
517
518
def source_directories
519
[Msf::Config.install_root]
520
end
521
522
def modified_files
523
# Using an array avoids shelling out, so we avoid escaping/quoting
524
changed_files = %w[git diff --name-only]
525
526
is_success = true
527
files = []
528
source_directories.each do |directory|
529
begin
530
output, status = Open3.capture2e(*changed_files, chdir: directory)
531
is_success = status.success?
532
break unless is_success
533
534
files += output.split("\n").map do |path|
535
realpath = Pathname.new(directory).join(path).realpath
536
raise "invalid path" unless realpath.to_s.start_with?(directory)
537
realpath.to_s
538
end
539
rescue => e
540
elog(e)
541
is_success = false
542
break
543
end
544
end
545
[files, is_success]
546
end
547
end
548
549