CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/msf/ui/console/command_dispatcher/developer.rb
Views: 1904
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(path, print_errors: true)
71
full_path = File.expand_path(path)
72
73
unless File.exist?(full_path) && full_path.end_with?('.rb')
74
print_error("#{full_path} must exist and be a .rb file") if print_errors
75
return
76
end
77
78
# The file must exist to reach this, so we try our best here
79
if full_path.start_with?(Msf::Config.module_directory, Msf::Config.user_module_directory)
80
print_error('Reloading Metasploit modules is not supported (try "reload")') if print_errors
81
return
82
end
83
84
print_status("Reloading #{full_path}")
85
load full_path
86
end
87
88
# @return [Array<String>] The list of modified file paths since startup
89
def modified_file_paths(print_errors: true)
90
files, is_success = modified_files
91
92
unless is_success
93
print_error("Git is not available") if print_errors
94
files = []
95
end
96
97
@modified_files ||= []
98
@modified_files |= files.map do |file|
99
next if file.end_with?('_spec.rb') || file.end_with?("spec_helper.rb")
100
File.join(Msf::Config.install_root, file)
101
end.compact
102
@modified_files
103
end
104
105
def cmd_irb_help
106
print_line 'Usage: irb'
107
print_line
108
print_line 'Open an interactive Ruby shell in the current context.'
109
print @@irb_opts.usage
110
end
111
112
#
113
# Open an interactive Ruby shell in the current context
114
#
115
def cmd_irb(*args)
116
expressions = []
117
118
# Parse the command options
119
@@irb_opts.parse(args) do |opt, idx, val|
120
case opt
121
when '-e'
122
expressions << val
123
when '-h'
124
cmd_irb_help
125
return false
126
end
127
end
128
129
if expressions.empty?
130
print_status('Starting IRB shell...')
131
132
framework.history_manager.with_context(name: :irb) do
133
begin
134
reline_autocomplete = Reline.autocompletion
135
if active_module
136
print_status("You are in #{active_module.fullname}\n")
137
Rex::Ui::Text::IrbShell.new(active_module).run
138
else
139
print_status("You are in the \"framework\" object\n")
140
Rex::Ui::Text::IrbShell.new(framework).run
141
end
142
rescue
143
print_error("Error during IRB: #{$!}\n\n#{$@.join("\n")}")
144
ensure
145
Reline.autocompletion = reline_autocomplete if defined? reline_autocomplete
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 here either
155
if framework.datastore['VERBOSE'].to_s == 'true'
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
# XXX: VERBOSE is a string instead of Bool??
247
print_status("Launching #{editor} #{path}") if framework.datastore['VERBOSE'].to_s == 'true'
248
249
unless system(*editor.split, path)
250
print_error("Could not execute #{editor} #{path}")
251
return
252
end
253
254
return if editing_module
255
256
reload_file(path)
257
end
258
259
#
260
# Tab completion for the edit command
261
#
262
def cmd_edit_tabs(str, words)
263
tab_complete_filenames(str, words)
264
end
265
266
def cmd_reload_lib_help
267
cmd_reload_lib('-h')
268
end
269
270
#
271
# Reload Ruby library files from specified paths
272
#
273
def cmd_reload_lib(*args)
274
files = []
275
options = OptionParser.new do |opts|
276
opts.banner = 'Usage: reload_lib lib/to/reload.rb [...]'
277
opts.separator ''
278
opts.separator 'Reload Ruby library files from specified paths.'
279
opts.separator ''
280
281
opts.on '-h', '--help', 'Help banner.' do
282
return print(opts.help)
283
end
284
285
opts.on '-a', '--all', 'Reload all* changed files in your current Git working tree.
286
*Excludes modules and non-Ruby files.' do
287
files.concat(modified_file_paths)
288
end
289
end
290
291
# The remaining unparsed arguments are files
292
files.concat(options.order(args))
293
files.uniq!
294
295
return print(options.help) if files.empty?
296
297
files.each do |file|
298
reload_file(file)
299
rescue ScriptError, StandardError => e
300
print_error("Error while reloading file #{file.inspect}: #{e}:\n#{e.backtrace.to_a.map { |line| " #{line}" }.join("\n")}")
301
end
302
end
303
304
#
305
# Tab completion for the reload_lib command
306
#
307
def cmd_reload_lib_tabs(str, words)
308
tab_complete_filenames(str, words)
309
end
310
311
def cmd_log_help
312
print_line 'Usage: log'
313
print_line
314
print_line 'Display framework.log paged to the end if possible.'
315
print_line 'To change the preferred pager, you can "setg LocalPager".'
316
print_line 'For full effect, "setg LogLevel 3" before running modules.'
317
print_line
318
print_line "Log location: #{File.join(Msf::Config.log_directory, 'framework.log')}"
319
print_line
320
end
321
322
#
323
# Display framework.log paged to the end if possible
324
#
325
def cmd_log(*args)
326
path = File.join(Msf::Config.log_directory, 'framework.log')
327
328
# XXX: +G isn't portable and may hang on large files
329
pager = local_pager.to_s.include?('less') ? "#{local_pager} +G" : local_pager
330
331
unless pager
332
pager = 'tail -n 50'
333
print_warning("LocalPager or $PAGER/$MANPAGER should be set. Falling back on #{pager}.")
334
end
335
336
# XXX: No vprint_status in this context?
337
# XXX: VERBOSE is a string instead of Bool??
338
print_status("Launching #{pager} #{path}") if framework.datastore['VERBOSE'].to_s == 'true'
339
340
unless system(*pager.split, path)
341
print_error("Could not execute #{pager} #{path}")
342
end
343
end
344
345
#
346
# Interact with framework's service manager
347
#
348
def cmd__servicemanager(*args)
349
if args.include?('-h') || args.include?('--help')
350
cmd__servicemanager_help
351
return false
352
end
353
354
opts = {}
355
@@_servicemanager_opts.parse(args) do |opt, idx, val|
356
case opt
357
when '-l', '--list'
358
opts[:list] = true
359
end
360
end
361
362
if opts.empty?
363
opts[:list] = true
364
end
365
366
if opts[:list]
367
table = Rex::Text::Table.new(
368
'Header' => 'Services',
369
'Indent' => 1,
370
'Columns' => ['Id', 'Name', 'References']
371
)
372
Rex::ServiceManager.instance.each.with_index do |(name, instance), id|
373
# TODO: Update rex-core to support querying the reference count
374
table << [id, name, instance.instance_variable_get(:@_references)]
375
end
376
377
if table.rows.empty?
378
print_status("No framework services are currently running.")
379
else
380
print_line(table.to_s)
381
end
382
end
383
end
384
385
#
386
# Tab completion for the _servicemanager command
387
#
388
def cmd__servicemanager_tabs(_str, words)
389
return [] if words.length > 1
390
391
@@_servicemanager_opts.option_keys
392
end
393
394
def cmd__servicemanager_help
395
print_line 'Usage: _servicemanager'
396
print_line
397
print_line 'Manage running framework services'
398
print @@_servicemanager_opts.usage
399
print_line
400
end
401
402
#
403
# Interact with framework's history manager
404
#
405
def cmd__historymanager(*args)
406
if args.include?('-h') || args.include?('--help')
407
cmd__historymanager_help
408
return false
409
end
410
411
opts = {}
412
@@_historymanager_opts.parse(args) do |opt, idx, val|
413
case opt
414
when '-l', '--list'
415
opts[:list] = true
416
when '-d', '--debug'
417
opts[:debug] = val.nil? ? true : val.downcase.start_with?(/t|y/)
418
end
419
end
420
421
if opts.empty?
422
opts[:list] = true
423
end
424
425
if opts.key?(:debug)
426
framework.history_manager._debug = opts[:debug]
427
print_status("HistoryManager debugging is now #{opts[:debug] ? 'on' : 'off'}")
428
end
429
430
if opts[:list]
431
table = Rex::Text::Table.new(
432
'Header' => 'History contexts',
433
'Indent' => 1,
434
'Columns' => ['Id', 'File', 'Name']
435
)
436
framework.history_manager._contexts.each.with_index do |context, id|
437
table << [id, context[:history_file], context[:name]]
438
end
439
440
if table.rows.empty?
441
print_status("No history contexts present.")
442
else
443
print_line(table.to_s)
444
end
445
end
446
end
447
448
#
449
# Tab completion for the _historymanager command
450
#
451
def cmd__historymanager_tabs(_str, words)
452
return [] if words.length > 1
453
454
@@_historymanager_opts.option_keys
455
end
456
457
def cmd__historymanager_help
458
print_line 'Usage: _historymanager'
459
print_line
460
print_line 'Manage the history manager'
461
print @@_historymanager_opts.usage
462
print_line
463
end
464
465
#
466
# Time how long in seconds a command takes to execute
467
#
468
def cmd_time(*args)
469
if args.empty? || args.first == '-h' || args.first == '--help'
470
cmd_time_help
471
return true
472
end
473
474
profiler = nil
475
while args.first == '--cpu' || args.first == '--memory'
476
profiler = args.shift
477
end
478
479
begin
480
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
481
command = Shellwords.shelljoin(args)
482
483
case profiler
484
when '--cpu'
485
Metasploit::Framework::Profiler.record_cpu do
486
driver.run_single(command)
487
end
488
when '--memory'
489
Metasploit::Framework::Profiler.record_memory do
490
driver.run_single(command)
491
end
492
else
493
driver.run_single(command)
494
end
495
ensure
496
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
497
elapsed_time = end_time - start_time
498
print_good("Command #{command.inspect} completed in #{elapsed_time} seconds")
499
end
500
end
501
502
def cmd_time_help
503
print_line 'Usage: time [options] [command]'
504
print_line
505
print_line 'Time how long a command takes to execute in seconds. Also supports profiling options.'
506
print_line
507
print_line ' Usage:'
508
print_line ' * time db_import ./db_import.html'
509
print_line ' * time show exploits'
510
print_line ' * time reload_all'
511
print_line ' * time missing_command'
512
print_line ' * time --cpu db_import ./db_import.html'
513
print_line ' * time --memory db_import ./db_import.html'
514
print @@time_opts.usage
515
print_line
516
end
517
518
private
519
520
def modified_files
521
# Temporary work-around until Open3 gets fixed on Windows 11:
522
# https://github.com/ruby/open3/issues/9
523
return [] if Rex::Compat.is_cygwin || Rex::Compat.is_windows
524
525
# Using an array avoids shelling out, so we avoid escaping/quoting
526
changed_files = %w[git diff --name-only]
527
begin
528
output, status = Open3.capture2e(*changed_files, chdir: Msf::Config.install_root)
529
is_success = status.success?
530
output = output.split("\n")
531
rescue => e
532
elog(e)
533
output = []
534
is_success = false
535
end
536
return output, is_success
537
end
538
end
539
540