From d5d9d56081cf0c190c432071004c5de43ec8a1d4 Mon Sep 17 00:00:00 2001 From: Mike Smith Date: Thu, 7 Apr 2011 21:59:32 +0000 Subject: [PATCH] Create a dedicated db table to track sessions & session events. * Add new db tables for session & session_events * Migrate existing session data from events db table * Modify session report methods to log to the new tables git-svn-id: file:///home/svn/framework3/trunk@12273 4d416f70-5f16-0410-b530-b9f4589650da --- .../20110317144932_add_session_table.rb | 110 +++ lib/msf/base/sessions/command_shell.rb | 100 +- lib/msf/base/sessions/meterpreter.rb | 13 + lib/msf/core/db.rb | 300 +++++- lib/msf/core/db_export.rb | 10 +- lib/msf/core/framework.rb | 152 +-- lib/msf/core/model.rb | 3 + lib/msf/core/model/host.rb | 871 ++++++++++++++++++ lib/msf/core/model/note.rb | 7 + lib/msf/core/model/service.rb | 7 + lib/msf/core/model/session.rb | 11 + lib/msf/core/model/session_event.rb | 11 + lib/msf/core/rpc/session.rb | 4 +- lib/msf/core/session.rb | 35 +- lib/msf/core/session/basic.rb | 6 +- lib/msf/core/session/interactive.rb | 28 +- lib/msf/core/session_manager.rb | 60 +- lib/msf/core/task_manager.rb | 2 +- lib/msf/core/thread_manager.rb | 3 +- lib/msf/ui/console/command_dispatcher/core.rb | 67 +- lib/rex/io/ring_buffer.rb | 364 ++++++++ lib/rex/text.rb | 14 + lib/rex/ui/interactive.rb | 44 + modules/auxiliary/server/browser_autopwn.rb | 20 +- 24 files changed, 2028 insertions(+), 214 deletions(-) create mode 100644 data/sql/migrate/20110317144932_add_session_table.rb create mode 100644 lib/msf/core/model/session.rb create mode 100644 lib/msf/core/model/session_event.rb create mode 100644 lib/rex/io/ring_buffer.rb diff --git a/data/sql/migrate/20110317144932_add_session_table.rb b/data/sql/migrate/20110317144932_add_session_table.rb new file mode 100644 index 0000000000..15ac8852bb --- /dev/null +++ b/data/sql/migrate/20110317144932_add_session_table.rb @@ -0,0 +1,110 @@ +class AddSessionTable < ActiveRecord::Migration + + class Event < ActiveRecord::Base + serialize :info + end + + class SessionEvent < ActiveRecord::Base + belongs_to :session + end + + class Session < ActiveRecord::Base + has_many :events, :class_name => 'AddSessionTable::SessionEvent' + serialize :datastore + end + + def self.up + + create_table :sessions do |t| + t.integer :host_id + + t.string :stype # session type: meterpreter, shell, etc + t.string :via_exploit # module name + t.string :via_payload # payload name + t.string :desc # session description + t.integer :port + t.string :platform # platform type of the remote system + t.string :routes + + t.text :datastore # module's datastore + + t.timestamp :opened_at, :null => false + t.timestamp :closed_at + + t.string :close_reason + end + + create_table :session_events do |t| + t.integer :session_id + + t.string :etype # event type: command, output, upload, download, filedelete + t.binary :command + t.binary :output + t.string :remote_path + t.string :local_path + + t.timestamp :created_at + end + + # + # Migrate session data from events table + # + + close_events = Event.find_all_by_name("session_close") + open_events = Event.find_all_by_name("session_open") + + command_events = Event.find_all_by_name("session_command") + output_events = Event.find_all_by_name("session_output") + upload_events = Event.find_all_by_name("session_upload") + download_events = Event.find_all_by_name("session_download") + + open_events.each do |o| + c = close_events.find { |e| e.info[:session_uuid] == o.info[:session_uuid] } + + s = Session.new( + :host_id => o.host_id, + :stype => o.info[:session_type], + :via_exploit => o.info[:via_exploit], + :via_payload => o.info[:via_payload], + :datastore => o.info[:datastore], + :opened_at => o.created_at + ) + + if c + s.closed_at = c.created_at + s.desc = c.info[:session_info] + else + # couldn't find the corresponding close event + s.closed_at = s.opened_at + s.desc = "?" + end + + uuid = o.info[:session_uuid] + + command_events.select { |e| e.info[:session_uuid] == uuid }.each do |e| + s.events.build(:created_at => e.created_at, :etype => "command", :command => e.info[:command] ) + end + + output_events.select { |e| e.info[:session_uuid] == uuid }.each do |e| + s.events.build(:created_at => e.created_at, :etype => "output", :output => e.info[:output] ) + end + + upload_events.select { |e| e.info[:session_uuid] == uuid }.each do |e| + s.events.build(:created_at => e.created_at, :etype => "upload", :local_path => e.info[:local_path], :remote_path => e.info[:remote_path] ) + end + + download_events.select { |e| e.info[:session_uuid] == uuid }.each do |e| + s.events.build(:created_at => e.created_at, :etype => "download", :local_path => e.info[:local_path], :remote_path => e.info[:remote_path] ) + end + + s.events.sort_by(&:created_at) + + s.save! + end + end + + def self.down + drop_table :sessions + drop_table :session_events + end +end diff --git a/lib/msf/base/sessions/command_shell.rb b/lib/msf/base/sessions/command_shell.rb index a923e97533..9f9de29e73 100644 --- a/lib/msf/base/sessions/command_shell.rb +++ b/lib/msf/base/sessions/command_shell.rb @@ -90,7 +90,7 @@ class CommandShell # Keep reading data until no more data is available or the timeout is # reached. - while (::Time.now.to_f < etime and ::IO.select([rstream], nil, nil, timeo)) + while (::Time.now.to_f < etime and (self.respond_to?(:ring) or ::IO.select([rstream], nil, nil, timeo))) res = shell_read(-1, 0.01) buff << res if res timeo = etime - ::Time.now.to_f @@ -103,6 +103,8 @@ class CommandShell # Read from the command shell. # def shell_read(length=-1, timeout=1) + return shell_read_ring(length,timeout) if self.respond_to?(:ring) + begin rv = rstream.get_once(length, timeout) framework.events.on_session_output(self, rv) if rv @@ -114,6 +116,50 @@ class CommandShell end end + # + # Read from the command shell. + # + def shell_read_ring(length=-1, timeout=1) + self.ring_buff ||= "" + + # Short-circuit bad length values + return "" if length == 0 + + # Return data from the stored buffer if available + if self.ring_buff.length >= length and length > 0 + buff = self.ring_buff.slice!(0,length) + return buff + end + + buff = self.ring_buff + self.ring_buff = "" + + begin + ::Timeout.timeout(timeout) do + while( (length > 0 and buff.length < length) or (length == -1 and buff.length == 0)) + ring.select + nseq,data = ring.read_data(self.ring_seq) + if data + self.ring_seq = nseq + buff << data + end + end + end + rescue ::Timeout::Error + rescue ::Exception => e + shell_close + raise e + end + + # Store any leftovers in the ring buffer backlog + if length > 0 and buff.length > length + self.ring_buff = buff[length, buff.length - length] + buff = buff[0,length] + end + + buff + end + # # Writes to the command shell. # @@ -178,7 +224,14 @@ protected # shell_write instead of operating on rstream directly. def _interact framework.events.on_session_interact(self) - + if self.respond_to?(:ring) + _interact_ring + else + _interact_stream + end + end + + def _interact_stream fds = [rstream.fd, user_input.fd] while self.interacting sd = Rex::ThreadSafe.select(fds, nil, fds, 0.5) @@ -190,8 +243,51 @@ protected if sd[0].include? user_input.fd shell_write(user_input.gets) end + Thread.pass end end + + def _interact_ring + + begin + + rdr = Rex::ThreadFactory.spawn("RingMonitor", false) do + seq = nil + while self.interacting + + # Look for any pending data from the remote ring + nseq,data = ring.read_data(seq) + + # Update the sequence number if necessary + seq = nseq || seq + + # Write output to the local stream if successful + user_output.print(data) if data + + begin + # Wait for new data to arrive on this session + ring.wait(seq) + rescue EOFError => e + break + end + end + end + + while self.interacting + # Look for any pending input or errors from the local stream + sd = Rex::ThreadSafe.select([ _local_fd ], nil, [_local_fd], 5.0) + + # Write input to the ring's input mechanism + shell_write(user_input.gets) if sd + end + + ensure + rdr.kill + end + end + + attr_accessor :ring_seq # This tracks the last seen ring buffer sequence (for shell_read) + attr_accessor :ring_buff # This tracks left over read data to maintain a compatible API end class CommandShellWindows < CommandShell diff --git a/lib/msf/base/sessions/meterpreter.rb b/lib/msf/base/sessions/meterpreter.rb index af9f048ce1..554deabd83 100644 --- a/lib/msf/base/sessions/meterpreter.rb +++ b/lib/msf/base/sessions/meterpreter.rb @@ -281,8 +281,21 @@ class Meterpreter < Rex::Post::Meterpreter::Client ::Timeout.timeout(60) do username = self.sys.config.getuid sysinfo = self.sys.config.sysinfo + framework.db.report_note({ + :type => "host.os.session_fingerprint", + :host => self, + :workspace => workspace, + :data => { + :name => sysinfo["Computer"], + :os => sysinfo["OS"], + :arch => sysinfo["Architecture"], + } + }) safe_info = "#{username} @ #{sysinfo['Computer']}" safe_info.force_encoding("ASCII-8BIT") if safe_info.respond_to?(:force_encoding) + # Should probably be using Rex::Text.ascii_safe_hex but leave + # this as is for now since "\xNN" is arguably uglier than "_" + # showing up in various places in the UI. safe_info.gsub!(/[\x00-\x08\x0b\x0c\x0e-\x19\x7f-\xff]+/n,"_") self.info = safe_info end diff --git a/lib/msf/core/db.rb b/lib/msf/core/db.rb index c0963cd03c..45b1147642 100644 --- a/lib/msf/core/db.rb +++ b/lib/msf/core/db.rb @@ -420,6 +420,173 @@ class DBManager wspace.services.all(:include => :host, :conditions => conditions, :order => "hosts.address, port") end + # Returns a session based on opened_time, host address, and workspace + # (or returns nil) + def get_session(opts) + return if not active + wspace = opts[:workspace] || opts[:wspace] || workspace + addr = opts[:addr] || opts[:address] || opts[:host] || return + host = get_host(:workspace => wspace, :host => addr) + time = opts[:opened_at] || opts[:created_at] || opts[:time] || return + Msf::DBManager::Session.find_by_host_id_and_opened_at(host.id, time) + end + + # Record a new session in the database + # + # opts must contain either + # :session - the Msf::Session object we are reporting + # :host - the Host object we are reporting a session on. + # + def report_session(opts) + return if not active + if opts[:session] + raise ArgumentError.new("Invalid :session, expected Msf::Session") unless opts[:session].kind_of? Msf::Session + session = opts[:session] + wspace = opts[:workspace] || find_workspace(session.workspace) + h_opts = { } + h_opts[:host] = normalize_host(session) + h_opts[:arch] = session.arch if session.arch + h_opts[:workspace] = wspace + host = find_or_create_host(h_opts) + sess_data = { + :host_id => host.id, + :stype => session.type, + :desc => session.info, + :platform => session.platform, + :via_payload => session.via_payload, + :via_exploit => session.via_exploit, + :routes => [], + :datastore => session.exploit_datastore.to_h, + :opened_at => Time.now + } + elsif opts[:host] + raise ArgumentError.new("Invalid :host, expected Host object") unless opts[:host].kind_of? Host + host = opts[:host] + sess_data = { + :host_id => host.id, + :stype => opts[:stype], + :desc => opts[:desc], + :platform => opts[:platform], + :via_payload => opts[:via_payload], + :via_exploit => opts[:via_exploit], + :routes => opts[:routes], + :datastore => opts[:datastore], + :opened_at => opts[:opened_at], + :closed_at => opts[:closed_at], + :close_reason => opts[:close_reason], + } + else + raise ArgumentError.new("Missing option :session or :host") + end + ret = {} + + task = queue(Proc.new { + s = Msf::DBManager::Session.create(sess_data) + if opts[:session] + session.db_record = s + else + myret = s.save! + end + ret[:session] = s + }) + + # If this is a live session, we know the host is vulnerable to something. + # If the exploit used was multi/handler, though, we don't know what + # it's vulnerable to, so it isn't really useful to save it. + if opts[:session] + if session.via_exploit and session.via_exploit != "exploit/multi/handler" + return unless host + port = session.exploit_datastore["RPORT"] + service = (port ? host.services.find_by_port(port) : nil) + mod = framework.modules.create(session.via_exploit) + vuln_info = { + :host => host.address, + :name => session.via_exploit, + :refs => mod.references, + :workspace => wspace + } + framework.db.report_vuln(vuln_info) + # Exploit info is like vuln info, except it's /just/ for storing + # successful exploits in an unserialized way. Yes, there is + # duplication, but it makes exporting a score card about a + # million times easier. TODO: See if vuln/exploit can get fixed up + # to one useful table. + exploit_info = { + :name => session.via_exploit, + :payload => session.via_payload, + :workspace => wspace, + :host => host, + :service => service, + :session_uuid => session.uuid + } + framework.db.report_exploit(exploit_info) + end + end + + # Always wait for the task so that session.db_record gets stored + # properly. This allows us to have session events immediately after + # reporting the new session without having to worry about race + # conditions. + return nil if task.wait() != :done + return ret[:session] + end + + # + # Record a session event in the database + # + # opts must contain one of: + # :session -- the Msf::Session OR the Msf::DBManager::Session we are reporting + # :etype -- event type, enum: command, output, upload, download, filedelete + # + # opts may contain + # :output -- the data for an output event + # :command -- the data for an command event + # :remote_path -- path to the associated file for upload, download, and filedelete events + # :local_path -- path to the associated file for upload, and download + # + def report_session_event(opts) + return if not active + raise ArgumentError.new("Missing required option :session") if opts[:session].nil? + raise ArgumentError.new("Expected an :etype") unless opts[:etype] + if opts[:session].respond_to? :db_record + session = opts[:session].db_record + event_data = { :created_at => Time.now } + else + session = opts[:session] + event_data = { :created_at => opts[:created_at] } + end + unless session.kind_of? Msf::DBManager::Session + raise ArgumentError.new("Invalid :session, expected Session object got #{session.class}") + end + event_data[:session_id] = session.id + [:remote_path, :local_path, :output, :command, :etype].each do |attr| + event_data[attr] = opts[attr] if opts[attr] + end + task = queue(Proc.new { + s = Msf::DBManager::SessionEvent.create(event_data) + }) + return task + end + + def report_session_route(session, route) + return if not active + + task = queue(Proc.new { + session.db_record.routes << route + session.db_record.save! + }) + + end + + def report_session_route_remove(session, route) + return if not active + + task = queue(Proc.new { + session.db_record.routes.delete(route) + session.db_record.save! + }) + + end def get_client(opts) wspace = opts.delete(:workspace) || workspace @@ -567,6 +734,9 @@ class DBManager return if not active wait = opts.delete(:wait) wspace = opts.delete(:workspace) || workspace + if wspace.kind_of? String + wspace = find_workspace(wspace) + end seen = opts.delete(:seen) || false crit = opts.delete(:critical) || false host = nil @@ -576,8 +746,8 @@ class DBManager if opts[:host].kind_of? Host host = opts[:host] else - report_host({:workspace => wspace, :host => opts[:host]}) addr = normalize_host(opts[:host]) + report_host({:workspace => wspace, :host => addr}) end # Do the same for a service if that's also included. if (opts[:port]) @@ -634,10 +804,10 @@ class DBManager conditions[:host_id] = host[:id] if host conditions[:service_id] = service[:id] if service - notes = wspace.notes.find(:all, :conditions => conditions) - case mode when :unique + notes = wspace.notes.find(:all, :conditions => conditions) + # Only one note of this type should exist, make a new one if it # isn't there. If it is, grab it and overwrite its data. if notes.empty? @@ -647,6 +817,8 @@ class DBManager end note.data = data when :unique_data + notes = wspace.notes.find(:all, :conditions => conditions) + # Don't make a new Note with the same data as one that already # exists for the given: type and (host or service) notes.each do |n| @@ -2742,7 +2914,7 @@ class DBManager end } host_address = host_data[:host].dup # Preserve after report_host() deletes - report_host(host_data) + report_host(host_data.merge(:wait => true)) host.elements.each('services/service') do |service| service_data = {} service_data[:workspace] = wspace @@ -2832,32 +3004,77 @@ class DBManager end report_cred(cred_data.merge(:wait => true)) end + + host.elements.each('sessions/session') do |sess| + sess_id = nils_for_nulls(sess.elements["id"].text.to_s.strip.to_i) + sess_data = {} + sess_data[:host] = get_host(:workspace => wspace, :address => host_address) + %W{desc platform port stype}.each {|datum| + if sess.elements[datum].respond_to? :text + sess_data[datum.intern] = nils_for_nulls(sess.elements[datum].text.to_s.strip) + end + } + %W{opened-at close-reason closed-at via-exploit via-payload}.each {|datum| + if sess.elements[datum].respond_to? :text + sess_data[datum.gsub("-","_").intern] = nils_for_nulls(sess.elements[datum].text.to_s.strip) + end + } + sess_data[:datastore] = nils_for_nulls(unserialize_object(sess.elements["datastore"], allow_yaml)) + sess_data[:routes] = nils_for_nulls(unserialize_object(sess.elements["routes"], allow_yaml)) + if not sess_data[:closed_at] # Fake a close if we don't already have one + sess_data[:closed_at] = Time.now + sess_data[:close_reason] = "Imported at #{Time.now}" + end + + existing_session = get_session( + :workspace => sess_data[:host].workspace, + :addr => sess_data[:host].address, + :time => sess_data[:opened_at] + ) + this_session = existing_session || report_session(sess_data.merge(:wait => true)) + next if existing_session + sess.elements.each('events/event') do |sess_event| + sess_event_data = {} + sess_event_data[:session] = this_session + %W{created-at etype local-path remote-path}.each {|datum| + if sess_event.elements[datum].respond_to? :text + sess_event_data[datum.gsub("-","_").intern] = nils_for_nulls(sess_event.elements[datum].text.to_s.strip) + end + } + %W{command output}.each {|datum| + if sess_event.elements[datum].respond_to? :text + sess_event_data[datum.gsub("-","_").intern] = nils_for_nulls(unserialize_object(sess_event.elements[datum], allow_yaml)) + end + } + report_session_event(sess_event_data) + end + end end - + # Import web sites doc.elements.each("/#{btag}/web_sites/web_site") do |web| info = {} info[:workspace] = wspace - + %W{host port vhost ssl comments}.each do |datum| if web.elements[datum].respond_to? :text info[datum.intern] = nils_for_nulls(web.elements[datum].text.to_s.strip) end end - + info[:options] = nils_for_nulls(unserialize_object(web.elements["options"], allow_yaml)) if web.elements["options"].respond_to?(:text) info[:ssl] = (info[:ssl] and info[:ssl].to_s.strip.downcase == "true") ? true : false - + %W{created-at updated-at}.each { |datum| if web.elements[datum].text info[datum.gsub("-","_")] = nils_for_nulls(web.elements[datum].text.to_s.strip) end } - + report_web_site(info) yield(:web_site, "#{info[:host]}:#{info[:port]} (#{info[:vhost]})") if block end - + %W{page form vuln}.each do |wtype| doc.elements.each("/#{btag}/web_#{wtype}s/web_#{wtype}") do |web| info = {} @@ -2866,9 +3083,9 @@ class DBManager info[:port] = nils_for_nulls(web.elements["port"].text.to_s.strip) if web.elements["port"].respond_to?(:text) info[:ssl] = nils_for_nulls(web.elements["ssl"].text.to_s.strip) if web.elements["ssl"].respond_to?(:text) info[:vhost] = nils_for_nulls(web.elements["vhost"].text.to_s.strip) if web.elements["vhost"].respond_to?(:text) - + info[:ssl] = (info[:ssl] and info[:ssl].to_s.strip.downcase == "true") ? true : false - + case wtype when "page" %W{path code body query cookie auth ctype mtime location}.each do |datum| @@ -2894,14 +3111,14 @@ class DBManager info[:risk] = info[:risk].to_i info[:confidence] = info[:confidence].to_i end - + %W{created-at updated-at}.each { |datum| if web.elements[datum].text info[datum.gsub("-","_")] = nils_for_nulls(web.elements[datum].text.to_s.strip) end } self.send("report_web_#{wtype}", info) - + yield("web_#{wtype}".intern, info[:path]) if block end end @@ -4468,17 +4685,56 @@ class DBManager end end + # + # Returns something suitable for the +:host+ parameter to the various report_* methods + # + # Takes a Host object, a Session object, an Msf::Session object or a String + # address + # def normalize_host(host) - # If the host parameter is a Session, try to extract its address - if host.respond_to?('target_host') - thost = host.target_host - tpeer = host.tunnel_peer - if tpeer and (!thost or thost.empty?) - thost = tpeer.split(":")[0] + return host if host.kind_of? Host + norm_host = nil + + if (host.kind_of? String) + # If it's an IPv4 addr with a host on the end, strip the port + if host =~ /((\d{1,3}\.){3}\d{1,3}):\d+/ + norm_host = $1 + else + norm_host = host end - host = thost + elsif host.kind_of? Session + norm_host = host.host + elsif host.respond_to?(:target_host) + # Then it's an Msf::Session object with a target but target_host + # won't be set in some cases, so try tunnel_peer as well + thost = host.target_host + if host.tunnel_peer and (!thost or thost.empty?) + # tunnel_peer is of the form ip:port, so strip off the port to + # get the addr by itself + thost = host.tunnel_peer.split(":")[0] + end + norm_host = thost end - host + + # If we got here and don't have a norm_host yet, it could be a + # Msf::Session object with an empty or nil tunnel_host and tunnel_peer; + # see if it has a socket and use its peerhost if so. + if ( + norm_host.nil? and + host.respond_to?(:sock) and + host.sock.respond_to?(:peerhost) and + host.sock.peerhost.to_s.length > 0 + ) + norm_host = session.sock.peerhost + end + # If We got here and still don't have a real host, there's nothing left + # to try, just log it and return what we were given + if not norm_host + dlog("Host could not be normalized: #{host.inspect}") + norm_host = host + end + + norm_host end protected diff --git a/lib/msf/core/db_export.rb b/lib/msf/core/db_export.rb index 8ad1a03929..fa785a8a47 100644 --- a/lib/msf/core/db_export.rb +++ b/lib/msf/core/db_export.rb @@ -75,12 +75,6 @@ class Export return sz end - # Converts binary and whitespace characters to a hex notation. - def ascii_safe_hex(str) - str.gsub!(/([\x00-\x20\x80-\xFF])/){ |x| "\\x%.2x" % x.unpack("C*")[0] } - return str - end - # Formats credentials according to their type, and writes it out to the # supplied report file. Note for reimporting: Blank values are def write_credentials(ptype,creds,report_file) @@ -112,8 +106,8 @@ class Export end else "text" data.each do |c| - user = (c.user.nil? || c.user.empty?) ? "" : ascii_safe_hex(c.user) - pass = (c.pass.nil? || c.pass.empty?) ? "" : ascii_safe_hex(c.pass) + user = (c.user.nil? || c.user.empty?) ? "" : Rex::Text.ascii_safe_hex(c.user, true) + pass = (c.pass.nil? || c.pass.empty?) ? "" : Rex::Text.ascii_safe_hex(c.pass, true) report_file.write "%s %s\n" % [user,pass] end end diff --git a/lib/msf/core/framework.rb b/lib/msf/core/framework.rb index 4baf33c81f..db0240fa8d 100644 --- a/lib/msf/core/framework.rb +++ b/lib/msf/core/framework.rb @@ -277,127 +277,60 @@ class FrameworkEventSubscriber #report_event(:name => "ui_start", :info => info) end - # - # Generic handler for session events - # - def session_event(name, session, opts={}) - address = nil - - if session.respond_to? :peerhost and session.peerhost.to_s.length > 0 - address = session.peerhost - elsif session.respond_to? :tunnel_peer and session.tunnel_peer.to_s.length > 0 - address = session.tunnel_peer[0, session.tunnel_peer.rindex(":") || session.tunnel_peer.length ] - elsif session.respond_to? :target_host and session.target_host.to_s.length > 0 - address = session.target_host - else - elog("Session with no peerhost/tunnel_peer") - dlog("#{session.inspect}", LEV_3) - return - end - - if framework.db.active - ws = framework.db.find_workspace(session.workspace) - event = { - :workspace => ws, - :username => session.username, - :name => name, - :host => address, - :info => { - :session_id => session.sid, - :session_info => session.info, - :session_uuid => session.uuid, - :session_type => session.type, - :username => session.username, - :target_host => address, - :via_exploit => session.via_exploit, - :via_payload => session.via_payload, - :tunnel_peer => session.tunnel_peer, - :exploit_uuid => session.exploit_uuid - }.merge(opts) - } - report_event(event) - end - end - require 'msf/core/session' include ::Msf::SessionEvent def on_session_open(session) - opts = { :datastore => session.exploit_datastore.to_h, :critical => true } - session_event('session_open', session, opts) - if framework.db.active - framework.db.sync - - address = nil - - if session.respond_to? :peerhost and session.peerhost.to_s.length > 0 - address = session.peerhost - elsif session.respond_to? :tunnel_peer and session.tunnel_peer.to_s.length > 0 - address = session.tunnel_peer[0, session.tunnel_peer.rindex(":") || session.tunnel_peer.length ] - elsif session.respond_to? :target_host and session.target_host.to_s.length > 0 - address = session.target_host - else - elog("Session with no peerhost/tunnel_peer") - dlog("#{session.inspect}", LEV_3) - return - end - - # Since we got a session, we know the host is vulnerable to something. - # If the exploit used was multi/handler, though, we don't know what - # it's vulnerable to, so it isn't really useful to save it. - if session.via_exploit and session.via_exploit != "exploit/multi/handler" - wspace = framework.db.find_workspace(session.workspace) - host = wspace.hosts.find_by_address(address) - return unless host - port = session.exploit_datastore["RPORT"] - service = (port ? host.services.find_by_port(port) : nil) - mod = framework.modules.create(session.via_exploit) - vuln_info = { - :host => host.address, - :name => session.via_exploit, - :refs => mod.references, - :workspace => wspace - } - framework.db.report_vuln(vuln_info) - # Exploit info is like vuln info, except it's /just/ for storing - # successful exploits in an unserialized way. Yes, there is - # duplication, but it makes exporting a score card about a - # million times easier. TODO: See if vuln/exploit can get fixed up - # to one useful table. - exploit_info = { - :name => session.via_exploit, - :payload => session.via_payload, - :workspace => wspace, - :host => host, - :service => service, - :session_uuid => session.uuid - } - ret = framework.db.report_exploit(exploit_info) - end - end + framework.db.report_session(:session => session) end def on_session_upload(session, lpath, rpath) - session_event('session_upload', session, :local_path => lpath, :remote_path => rpath) + framework.db.report_session_event({ + :etype => 'upload', + :session => session, + :local_path => lpath, + :remote_path => rpath + }) end def on_session_download(session, rpath, lpath) - session_event('session_download', session, :local_path => lpath, :remote_path => rpath) + #session_event('session_download', session, :local_path => lpath, :remote_path => rpath) + framework.db.report_session_event({ + :etype => 'download', + :session => session, + :local_path => lpath, + :remote_path => rpath + }) end def on_session_filedelete(session, path) - session_event('session_filedelete', session, :path => path) + #session_event('session_filedelete', session, :path => path) + framework.db.report_session_event({ + :etype => 'filedelete', + :session => session, + :local_path => lpath, + :remote_path => rpath + }) end def on_session_close(session, reason='') - session_event('session_close', session) + if session.db_record + # Don't bother saving here, the session's cleanup method will take + # care of that later. + session.db_record.close_reason = reason + end end - def on_session_interact(session) - session_event('session_interact', session) - end + #def on_session_interact(session) + # $stdout.puts('session_interact', session.inspect) + #end def on_session_command(session, command) - session_event('session_command', session, :command => command) + #session_event('session_command', session, :command => command) + framework.db.report_session_event({ + :etype => 'command', + :session => session, + :command => command + }) end def on_session_output(session, output) @@ -412,10 +345,23 @@ class FrameworkEventSubscriber chunks << buff end chunks.each { |chunk| - session_event('session_output', session, :output => chunk) + #session_event('session_output', session, :output => chunk) + framework.db.report_session_event({ + :etype => 'output', + :session => session, + :output => chunk + }) } end + def on_session_route(session, route) + framework.db.report_session_route(session, route) + end + + def on_session_route_remove(session, route) + framework.db.report_session_route_remove(session, route) + end + # # This is covered by on_module_run and on_session_open, so don't bother diff --git a/lib/msf/core/model.rb b/lib/msf/core/model.rb index d2693c1a52..ab96fa7d15 100644 --- a/lib/msf/core/model.rb +++ b/lib/msf/core/model.rb @@ -18,6 +18,8 @@ require 'msf/core/model/workspace' require 'msf/core/model/vuln' require 'msf/core/model/cred' require 'msf/core/model/exploited_host' +require 'msf/core/model/session' +require 'msf/core/model/session_event' require 'msf/core/model/wmap_target' require 'msf/core/model/wmap_request' @@ -35,4 +37,5 @@ require 'msf/core/model/web_vuln' require 'msf/core/model/imported_cred' require 'msf/core/model/tag' +require 'msf/core/model/session_event' diff --git a/lib/msf/core/model/host.rb b/lib/msf/core/model/host.rb index 0edf20dbb9..b2ebea2b61 100644 --- a/lib/msf/core/model/host.rb +++ b/lib/msf/core/model/host.rb @@ -25,6 +25,877 @@ class Host < ActiveRecord::Base n && n.data[:locked] end + # Determine if the fingerprint data is readable. If not, it nearly always + # means that there was a problem with the YAML or the Marshal'ed data, + # so let's log that for later investigation. + def validate_fingerprint_data(fp) + if fp.data.kind_of?(Hash) and !fp.data.empty? + return true + else + dlog("Could not validate fingerprint data: #{fp.inspect}") + return false + end + end + + def normalize_os + host = self + + wname = {} # os_name == Linux, Windows, Mac OS X, VxWorks + wtype = {} # purpose == server, client, device + wflav = {} # os_flavor == Ubuntu, Debian, 2003, 10.5, JetDirect + wvers = {} # os_sp == 9.10, SP2, 10.5.3, 3.05 + warch = {} # arch == x86, PPC, SPARC, MIPS, '' + wlang = {} # os_lang == English, '' + whost = {} # hostname + + # Normalize the operating system fingerprints provided by various + # scanners (nmap, nexpose, retina, nessus, etc). These are stored as + # notes (instead of directly in the os_* fields) specifically for this + # purpose. Note that we're already restricting to this host by using + # host.notes instead of Note, so don't need a host_id in the conditions. + fingers = host.notes.find(:all, + :conditions => [ "ntype like '%%fingerprint'" ] + ) + fingers.each do |fp| + next if not validate_fingerprint_data(fp) + norm = normalize_scanner_fp(fp) + wvers[norm[:os_sp]] = wvers[norm[:os_sp]].to_i + (100 * norm[:certainty]) + wname[norm[:os_name]] = wname[norm[:os_name]].to_i + (100 * norm[:certainty]) + wflav[norm[:os_flavor]] = wflav[norm[:os_flavor]].to_i + (100 * norm[:certainty]) + warch[norm[:arch]] = warch[norm[:arch]].to_i + (100 * norm[:certainty]) + end + + # Grab service information and assign scores. Some services are + # more trustworthy than others. If more services agree than not, + # than that should be considered as well. + # Each service has a starting number of points. Services that + # are more difficult to fake are awarded more points. The points + # represent a running total, not a fixed score. + # XXX: This needs to be refactored in a big way. Tie-breaking is + # pretty arbitrary, it would be nice to explicitly believe some + # services over others, but that means recording which service + # has an opinion and which doesn't. It would also be nice to + # identify "impossible" combinations of services and alert that + # something funny is going on. + host.services.find(:all).each do |s| + next if not s.info + points = 0 + case s.name + when 'smb' + points = 210 + case s.info + when /(ubuntu|debian|fedora|red ?hat|rhel)/i + wname['Linux'] = wname['Linux'].to_i + points + wflav[$1.capitalize] = wflav[$1.capitalize].to_i + points + wtype['server'] = wtype['server'].to_i + points + when /^Windows/ + win_sp = nil + win_flav = nil + win_lang = nil + + ninfo = s.info + ninfo.gsub!('(R)', '') + ninfo.gsub!('(TM)', '') + ninfo.gsub!(/\s+/, ' ') + + # Windows (R) Web Server 2008 6001 Service Pack 1 (language: Unknown) (name:PG-WIN2008WEB) (domain:WORKGROUP) + # Windows XP Service Pack 3 (language: English) (name:EGYPT-B3E55BF3C) (domain:EGYPT-B3E55BF3C) + # Windows 7 Ultimate (Build 7600) (language: Unknown) (name:WIN7) (domain:WORKGROUP) + + #if ninfo =~ /^Windows ([^\s]+)(.*)(Service Pack |\(Build )([^\(]+)\(/ + if ninfo =~ /^Windows (.*)(Service Pack [^\s]+|\(Build [^\)]+\))/ + win_flav = $1.strip + win_sp = ($2).strip + win_sp.gsub!(/with.*/, '') + win_sp.gsub!('Service Pack', 'SP') + win_sp.gsub!('Build', 'b') + win_sp.gsub!(/\s+/, '') + win_sp.tr!("()", '') + else + if ninfo =~ /^Windows ([^\s+]+)([^\(]+)\(/ + win_flav = $2.strip + end + end + + + if ninfo =~ /name: ([^\)]+)\)/ + hostname = $1.strip + end + + if ninfo =~ /language: ([^\)]+)\)/ + win_lang = $1.strip + end + + win_lang = nil if win_lang =~ /unknown/i + win_vers = win_sp + + wname['Microsoft Windows'] = wname['Microsoft Windows'].to_i + points + wlang[win_lang] = wlang[win_lang].to_i + points if win_lang + wflav[win_flav] = wflav[win_flav].to_i + points if win_flav + wvers[win_vers] = wvers[win_vers].to_i + points if win_vers + whost[hostname] = whost[hostname].to_i + points if hostname + + case win_flav + when /NT|2003|2008/ + win_type = 'server' + else + win_type = 'client' + end + wtype[win_type] = wtype[win_type].to_i + points + end + + when 'ssh' + points = 104 + case s.info + when /honeypot/i # Never trust this + nil + when /ubuntu/i + # This needs to be above /debian/ becuase the ubuntu banner contains both, e.g.: + # SSH-2.0-OpenSSH_5.3p1 Debian-3ubuntu6 + wname['Linux'] = wname['Linux'].to_i + points + wflav['Ubuntu'] = wflav['Ubuntu'].to_i + points + wtype['server'] = wtype['server'].to_i + points + when /debian/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Debian'] = wflav['Debian'].to_i + points + wtype['server'] = wtype['server'].to_i + points + when /FreeBSD/ + wname['FreeBSD'] = wname['FreeBSD'].to_i + points + wtype['server'] = wtype['server'].to_i + points + when /sun_ssh/i + wname['Sun Solaris'] = wname['Sun Solaris'].to_i + points + wtype['server'] = wtype['server'].to_i + points + when /vshell|remotelyanywhere|freessh/i + wname['Microsoft Windows'] = wname['Microsoft Windows'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /radware/i + wname['RadWare'] = wname['RadWare'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /dropbear/i + wname['Linux'] = wname['Linux'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /netscreen/i + wname['NetScreen'] = wname['NetScreen'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /vpn3/ + wname['Cisco VPN 3000'] = wname['Cisco VPN 3000'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /cisco/i + wname['Cisco IOS'] = wname['Cisco IOS'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /mpSSH/ + wname['HP iLO'] = wname['HP iLO'].to_i + points + wtype['server'] = wtype['server'].to_i + points + end + when 'http' + points = 99 + case s.info + when /iSeries/ + wname['IBM iSeries'] = wname['IBM iSeries'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Mandrake/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Mandrake'] = wflav['Mandrake'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Mandriva/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Mandrake'] = wflav['Mandrake'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Ubuntu/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Ubuntu'] = wflav['Ubuntu'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Debian/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Debian'] = wflav['Debian'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Fedora/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Fedora'] = wflav['Fedora'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /CentOS/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['CentOS'] = wflav['CentOS'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /RHEL/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['RHEL'] = wflav['RHEL'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Red.?Hat/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Red Hat'] = wflav['Red Hat'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /SuSE/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['SUSE'] = wflav['SUSE'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /TurboLinux/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['TurboLinux'] = wflav['TurboLinux'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Gentoo/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Gentoo'] = wflav['Gentoo'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Conectiva/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Conectiva'] = wflav['Conectiva'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Asianux/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Asianux'] = wflav['Asianux'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Trustix/i + wname['Linux'] = wname['Linux'].to_i + points + wflav['Trustix'] = wflav['Trustix'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /White Box/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['White Box'] = wflav['White Box'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /UnitedLinux/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['UnitedLinux'] = wflav['UnitedLinux'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /PLD\/Linux/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['PLD/Linux'] = wflav['PLD/Linux'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Vine\/Linux/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['Vine/Linux'] = wflav['Vine/Linux'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /rPath/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['rPath'] = wflav['rPath'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /StartCom/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['StartCom'] = wflav['StartCom'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /linux/i + wname['Linux'] = wname['Linux'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /PalmOS/ + wname['PalmOS'] = wname['PalmOS'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /Microsoft[\x20\x2d]IIS\/[234]\.0/ + wname['Microsoft Windows NT 4.0'] = wname['Microsoft Windows NT 4.0'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Microsoft[\x20\x2d]IIS\/5\.0/ + wname['Microsoft Windows 2000'] = wname['Microsoft Windows 2000'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Microsoft[\x20\x2d]IIS\/5\.1/ + wname['Microsoft Windows XP'] = wname['Microsoft Windows XP'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Microsoft[\x20\x2d]IIS\/6\.0/ + wname['Microsoft Windows 2003'] = wname['Microsoft Windows 2003'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Microsoft[\x20\x2d]IIS\/7\.0/ + wname['Microsoft Windows 2008'] = wname['Microsoft Windows 2008'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Win32/i + wname['Microsoft Windows'] = wname['Microsoft Windows'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /DD\-WRT ([^\s]+) /i + wname['Linux'] = wname['Linux'].to_i + points + wflav['DD-WRT'] = wflav['DD-WRT'].to_i + points + wvers[$1.strip] = wvers[$1.strip].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /Darwin/ + wname['Apple Mac OS X'] = wname['Apple Mac OS X'].to_i + points + + when /FreeBSD/i + wname['FreeBSD'] = wname['FreeBSD'].to_i + points + + when /OpenBSD/i + wname['OpenBSD'] = wname['OpenBSD'].to_i + points + + when /NetBSD/i + wname['NetBSD'] = wname['NetBSD'].to_i + points + + when /NetWare/i + wname['Novell NetWare'] = wname['Novell NetWare'].to_i + points + + when /OpenVMS/i + wname['OpenVMS'] = wname['OpenVMS'].to_i + points + + when /SunOS|Solaris/i + wname['Sun Solaris'] = wname['Sun Solaris'].to_i + points + + when /HP.?UX/i + wname['HP-UX'] = wname['HP-UX'].to_i + points + end + when 'snmp' + points = 103 + case s.info + when /^Sun SNMP Agent/ + wname['Sun Solaris'] = wname['Sun Solaris'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^SunOS ([^\s]+) ([^\s]+) / + # XXX 1/2 XXX what does this comment mean i wonder + wname['Sun Solaris'] = wname['Sun Solaris'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Linux ([^\s]+) ([^\s]+) / + whost[$1] = whost[$1].to_i + points + wname['Linux ' + $2] = wname['Linux ' + $2].to_i + points + wvers[$2] = wvers[$2].to_i + points + arch = get_arch_from_string(s.info) + warch[arch] = warch[arch].to_i + points if arch + wtype['server'] = wtype['server'].to_i + points + + when /^Novell NetWare ([^\s]+)/ + wname['Novell NetWare ' + $1] = wname['Novell NetWare ' + $1].to_i + points + wvers[$1] = wvers[$1].to_i + points + arch = "x86" + warch[arch] = warch[arch].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Novell UnixWare ([^\s]+)/ + wname['Novell UnixWare ' + $1] = wname['Novell UnixWare ' + $1].to_i + points + wvers[$1] = wvers[$1].to_i + points + arch = "x86" + warch[arch] = warch[arch].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^HP-UX ([^\s]+) ([^\s]+) / + # XXX + wname['HP-UX ' + $2] = wname['HP-UX ' + $2].to_i + points + wvers[$1] = wvers[$1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^IBM PowerPC.*Base Operating System Runtime AIX version: (\d+\.\d+)/ + wname['IBM AIX ' + $1] = wname['IBM AIX ' + $1].to_i + points + wvers[$1] = wvers[$1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^SCO TCP\/IP Runtime Release ([^\s]+)/ + wname['SCO UnixWare ' + $1] = wname['SCO UnixWare ' + $1].to_i + points + wvers[$1] = wvers[$1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /.* IRIX version ([^\s]+)/ + wname['SGI IRIX ' + $1] = wname['SGI IRIX ' + $1].to_i + points + wvers[$1] = wvers[$1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Unisys ([^\s]+) version ([^\s]+) kernel/ + wname['Unisys ' + $2] = wname['Unisys ' + $2].to_i + points + wvers[$2] = wvers[$2].to_i + points + whost[$1] = whost[$1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /.*OpenVMS V([^\s]+) / + # XXX + wname['OpenVMS ' + $1] = wname['OpenVMS ' + $1].to_i + points + wvers[$1] = wvers[$1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Hardware:.*Software: Windows NT Version ([^\s]+) / + wname['Microsoft Windows NT ' + $1] = wname['Microsoft Windows NT ' + $1].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Hardware:.*Software: Windows 2000 Version 5\.0/ + wname['Microsoft Windows 2000'] = wname['Microsoft Windows 2000'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Hardware:.*Software: Windows 2000 Version 5\.1/ + wname['Microsoft Windows XP'] = wname['Microsoft Windows XP'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + when /^Hardware:.*Software: Windows Version 5\.2/ + wname['Microsoft Windows 2003'] = wname['Microsoft Windows 2003'].to_i + points + wtype['server'] = wtype['server'].to_i + points + + # XXX: TODO 2008, Vista, Windows 7 + + when /^Microsoft Windows CE Version ([^\s]+)+/ + wname['Microsoft Windows CE ' + $1] = wname['Microsoft Windows CE ' + $1].to_i + points + wtype['client'] = wtype['client'].to_i + points + + when /^IPSO ([^\s]+) ([^\s]+) / + whost[$1] = whost[$1].to_i + points + wname['Nokia IPSO ' + $2] = wname['Nokia IPSO ' + $2].to_i + points + wvers[$2] = wvers[$2].to_i + points + arch = get_arch_from_string(s.info) + warch[arch] = warch[arch].to_s + points if arch + wtype['device'] = wtype['device'].to_i + points + + when /^Sun StorEdge/ + wname['Sun StorEdge'] = wname['Sun StorEdge'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /^HP StorageWorks/ + wname['HP StorageWorks'] = wname['HP StorageWorks'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /^Network Storage/ + # XXX + wname['Network Storage Router'] = wname['Network Storage Router'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /Cisco Internetwork Operating System.*Version ([^\s]+)/ + vers = $1.split(/[,^\s]/)[0] + wname['Cisco IOS ' + vers] = wname['Cisco IOS ' + vers].to_i + points + wvers[vers] = wvers[vers].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /Cisco Catalyst.*Version ([^\s]+)/ + vers = $1.split(/[,^\s]/)[0] + wname['Cisco CatOS ' + vers] = wname['Cisco CatOS ' + vers].to_i + points + wvers[vers] = wvers[vers].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /Cisco 761.*Version ([^\s]+)/ + vers = $1.split(/[,^\s]/)[0] + wname['Cisco 761 ' + vers] = wname['Cisco 761 ' + vers].to_i + points + wvers[vers] = wvers[vers].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /Network Analysis Module.*Version ([^\s]+)/ + vers = $1.split(/[,^\s]/)[0] + wname['Cisco NAM ' + vers] = wname['Cisco NAM ' + vers].to_i + points + wvers[vers] = wvers[vers].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /VPN 3000 Concentrator Series Version ([^\s]+)/ + vers = $1.split(/[,^\s]/)[0] + wname['Cisco VPN 3000 ' + vers] = wname['Cisco VPN 3000 ' + vers].to_i + points + wvers[vers] = wvers[vers].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /ProCurve.*Switch/ + wname['3Com ProCurve Switch'] = wname['3Com ProCurve Switch'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /ProCurve.*Access Point/ + wname['3Com Access Point'] = wname['3Com Access Point'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /3Com.*Access Point/i + wname['3Com Access Point'] = wname['3Com Access Point'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /ShoreGear/ + wname['ShoreTel Appliance'] = wname['ShoreTel Appliance'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /firewall/i + wname['Unknown Firewall'] = wname['Unknown Firewall'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /phone/i + wname['Unknown Phone'] = wname['Unknown Phone'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /router/i + wname['Unknown Router'] = wname['Unknown Router'].to_i + points + wtype['device'] = wtype['device'].to_i + points + + when /switch/i + wname['Unknown Switch'] = wname['Unknown Switch'].to_i + points + wtype['device'] = wtype['device'].to_i + points + # + # Printer Signatures + # + when /^HP ETHERNET MULTI-ENVIRONMENT/ + wname['HP Printer'] = wname['HP Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Canon/i + wname['Canon Printer'] = wname['Canon Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Epson/i + wname['Epson Printer'] = wname['Epson Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /ExtendNet/i + wname['ExtendNet Printer'] = wname['ExtendNet Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Fiery/i + wname['Fiery Printer'] = wname['Fiery Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Konica/i + wname['Konica Printer'] = wname['Konica Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Lanier/i + wname['Lanier Printer'] = wname['Lanier Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Lantronix/i + wname['Lantronix Printer'] = wname['Lantronix Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Lexmark/i + wname['Lexmark Printer'] = wname['Lexmark Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Magicolor/i + wname['Magicolor Printer'] = wname['Magicolor Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Minolta/i + wname['Minolta Printer'] = wname['Minolta Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /NetJET/i + wname['NetJET Printer'] = wname['NetJET Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /OKILAN/i + wname['OKILAN Printer'] = wname['OKILAN Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Phaser/i + wname['Phaser Printer'] = wname['Phaser Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /PocketPro/i + wname['PocketPro Printer'] = wname['PocketPro Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Ricoh/i + wname['Ricoh Printer'] = wname['Ricoh Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Savin/i + wname['Savin Printer'] = wname['Savin Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /SHARP AR/i + wname['SHARP Printer'] = wname['SHARP Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Star Micronix/i + wname['Star Micronix Printer'] = wname['Star Micronix Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Source Tech/i + wname['Source Tech Printer'] = wname['Source Tech Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Xerox/i + wname['Xerox Printer'] = wname['Xerox Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /^Brother/i + wname['Brother Printer'] = wname['Brother Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /^Axis.*Network Print/i + wname['Axis Printer'] = wname['Axis Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /^Prestige/i + wname['Prestige Printer'] = wname['Prestige Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /^ZebraNet/i + wname['ZebraNet Printer'] = wname['ZebraNet Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /e\-STUDIO/i + wname['eStudio Printer'] = wname['eStudio Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /^Gestetner/i + wname['Gestetner Printer'] = wname['Gestetner Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /IBM.*Print/i + wname['IBM Printer'] = wname['IBM Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /HP (Color|LaserJet|InkJet)/i + wname['HP Printer'] = wname['HP Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Dell (Color|Laser|Ink)/i + wname['Dell Printer'] = wname['Dell Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + when /Print/i + wname['Unknown Printer'] = wname['Unknown Printer'].to_i + points + wtype['printer'] = wtype['printer'].to_i + points + end # End of s.info for SNMP + + when 'telnet' + points = 105 + case s.info + when /IRIX/ + wname['SGI IRIX'] = wname['SGI IRIX'].to_i + points + when /AIX/ + wname['IBM AIX'] = wname['IBM AIX'].to_i + points + when /(FreeBSD|OpenBSD|NetBSD)\/(.*) / + wname[$1] = wname[$1].to_i + points + arch = get_arch_from_string($2) + warch[arch] = warch[arch].to_i + points + when /Ubuntu (\d+(\.\d+)+)/ + wname['Linux'] = wname['Linux'].to_i + points + wflav['Ubuntu'] = wflav['Ubuntu'].to_i + points + wvers[$1] = wvers[$1].to_i + points + when /User Access Verification/ + wname['Cisco IOS'] = wname['Cisco IOS'].to_i + points + when /Microsoft/ + wname['Microsoft Windows'] = wname['Microsoft Windows'].to_i + points + end # End of s.info for TELNET + wtype['server'] = wtype['server'].to_i + points + + when 'smtp' + points = 103 + case s.info + when /ESMTP.*SGI\.8/ + wname['SGI IRIX'] = wname['SGI IRIX'].to_i + points + wtype['server'] = wtype['server'].to_i + points + end # End of s.info for SMTP + + when 'netbios' + points = 201 + case s.info + when /W2K3/i + wname['Microsoft Windows 2003'] = wname['Microsoft Windows 2003'].to_i + points + wtype['server'] = wtype['server'].to_i + points + when /W2K8/i + wname['Microsoft Windows 2008'] = wname['Microsoft Windows 2008'].to_i + points + wtype['server'] = wtype['server'].to_i + points + end # End of s.info for NETBIOS + + when 'dns' + points = 101 + case s.info + when 'Microsoft DNS' + wname['Microsoft Windows'] = wname['Microsoft Windows'].to_i + points + wtype['server'] = wtype['server'].to_i + points + end # End of s.info for DNS + end # End of s.name case + # End of Services + end + + # + # Report the best match here + # + best_match = {} + best_match[:os_name] = wname.keys.sort{|a,b| wname[b] <=> wname[a]}[0] + best_match[:purpose] = wtype.keys.sort{|a,b| wtype[b] <=> wtype[a]}[0] + best_match[:os_flavor] = wflav.keys.sort{|a,b| wflav[b] <=> wflav[a]}[0] + best_match[:os_sp] = wvers.keys.sort{|a,b| wvers[b] <=> wvers[a]}[0] + best_match[:arch] = warch.keys.sort{|a,b| warch[b] <=> warch[a]}[0] + best_match[:name] = whost.keys.sort{|a,b| whost[b] <=> whost[a]}[0] + best_match[:os_lang] = wlang.keys.sort{|a,b| wlang[b] <=> wlang[a]}[0] + + best_match[:os_flavor] ||= "" + if best_match[:os_name] + # Handle cases where the flavor contains the base name + best_match[:os_flavor].gsub!(best_match[:os_name], '') + end + + best_match[:os_name] ||= 'Unknown' + best_match[:purpose] ||= 'device' + + [:os_name, :purpose, :os_flavor, :os_sp, :arch, :name, :os_lang].each do |host_attr| + next if host.attribute_locked? host_attr + if best_match[host_attr] + host[host_attr] = Rex::Text.ascii_safe_hex(best_match[host_attr]) + end + end + + host.save! + p host + end + +protected + + # + # Convert a host.os.*_fingerprint Note into a hash containing the standard os_* fields + # + # Also includes a :certainty which is a float from 0 - 1.00 indicating the + # scanner's confidence in its fingerprint. If the particular scanner does + # not provide such information, defaults to 0.80. + # + def normalize_scanner_fp(fp) + return {} if not validate_fingerprint_data(fp) + ret = {} + data = fp.data + case fp.ntype + when 'host.os.session_fingerprint' + # These come from meterpreter sessions' client.sys.config.sysinfo + if data[:os] =~ /Windows/ + ret.update(parse_windows_os_str(data[:os])) + else + ret[:os_name] = data[:os] + end + ret[:arch] = data[:arch] if data[:arch] + + when 'host.os.nmap_fingerprint' + # :os_vendor=>"Microsoft" :os_family=>"Windows" :os_version=>"2000" :os_accuracy=>"94" + # + # :os_match=>"Microsoft Windows Vista SP0 or SP1, Server 2008, or Windows 7 Ultimate (build 7000)" + # :os_vendor=>"Microsoft" :os_family=>"Windows" :os_version=>"7" :os_accuracy=>"100" + ret[:certainty] = data[:os_accuracy].to_f / 100.0 + if (data[:os_vendor] == data[:os_family]) + ret[:os_name] = data[:os_family] + else + ret[:os_name] = data[:os_vendor] + " " + data[:os_family] + end + + when 'host.os.nexpose_fingerprint' + # :family=>"Windows" :certainty=>"0.85" :vendor=>"Microsoft" :product=>"Windows 7 Ultimate Edition" + # :family=>"Linux" :certainty=>"0.64" :vendor=>"Linux" :product=>"Linux" + # :family=>"Linux" :certainty=>"0.80" :vendor=>"Ubuntu" :product=>"Linux" + # :family=>"IOS" :certainty=>"0.80" :vendor=>"Cisco" :product=>"IOS" + # :family=>"embedded" :certainty=>"0.61" :vendor=>"Linksys" :product=>"embedded" + ret[:certainty] = data[:certainty].to_f + case data[:family] + when /AIX|ESX|Mac OS X|OpenSolaris|Solaris|IOS|Linux/ + if data[:vendor] == data[:family] + ret[:os_name] = data[:vendor] + else + # family often contains the vendor string, so rip it out to + # avoid useless duplication + ret[:os_name] = data[:vendor] + " " + data[:family].gsub(data[:vendor], '').strip + end + when "Windows" + ret[:os_name] = "Microsoft Windows" + ret[:os_flavor] = data[:product].gsub("Windows", '').strip if data[:product] + when "embedded" + ret[:os_name] = data[:vendor] + else + ret[:os_name] = data[:vendor] + end + ret[:arch] = get_arch_from_string(data[:arch]) if data[:arch] + ret[:arch] ||= get_arch_from_string(data[:desc]) if data[:desc] + + when 'host.os.retina_fingerprint' + # :os=>"Windows Server 2003 (X64), Service Pack 2" + case data[:os] + when /Windows/ + ret.merge(parse_windows_os_str(data[:os])) + else + # No idea what this looks like if it isn't windows. Just store + # the whole thing and hope for the best. XXX: Ghetto. =/ + ret[:os_name] = data[:os] + end + when 'host.os.nessus_fingerprint' + # :os=>"Microsoft Windows 2000 Advanced Server (English)" + # :os=>"Microsoft Windows 2000\nMicrosoft Windows XP" + # :os=>"Linux Kernel 2.6" + # :os=>"Sun Solaris 8" + # :os=>"IRIX 6.5" + + # Nessus sometimes jams multiple OS names together with a newline. + oses = data[:os].split(/\n/) + if oses.length > 1 + # Multiple fingerprints means Nessus wasn't really sure, reduce + # the certainty accordingly + ret[:certainty] = 0.5 + else + ret[:certainty] = 0.8 + end + + # Since there is no confidence associated with them, the best we + # can do is just take the first one. + case oses.first + when /Windows/ + ret.merge(parse_windows_os_str(os)) + when /(.*)?((\d+\.)+\d+)$/ + # Then this fingerprint has some version information at the + # end, pull it off. + ret[:os_name] = $1.gsub("Kernel", '').strip + ret[:os_sp] = $2 + else + ret[:os_name] = oses.first + end + + when 'host.os.qualys_fingerprint' + # :os=>"Microsoft Windows 2000" + # :os=>"Windows 2003" + # :os=>"Microsoft Windows XP Professional SP3" + # :os=>"Ubuntu Linux" + # :os=>"Cisco IOS 12.0(3)T3" + case data[:os] + when /Windows/ + ret.merge(parse_windows_os_str(data[:os])) + else + parts = data[:os].split(/\s+/, 3) + ret[:os_name] = parts[0] + " " + parts[1] + ret[:os_sp] = parts[2] if parts[2] + end + # XXX: We should really be using smb_version's stored fingerprints + # instead of parsing the service info manually. Disable for now so we + # don't count smb twice. + #when 'smb.fingerprint' + # # smb_version is kind enough to store everything we need directly + # ret.merge(fp.data) + # # If it's windows, this should be a pretty high-confidence + # # fingerprint. Otherwise, it's samba which doesn't give us much of + # # anything in most cases. + # ret[:certainty] = 1.0 if fp.data[:os_name] =~ /Windows/ + end + ret[:certainty] ||= 0.8 + + ret + end + + def parse_windows_os_str(str) + ret = {} + + ret[:os_name] = "Microsoft Windows" + ret[:arch] = get_arch_from_string(str) + if str =~ /(Service Pack|SP) ?(\d+)/ + ret[:os_sp] = "SP#{$2}" + end + + # Flavor + case str + when /(XP|2000|2003|Vista|7 .* Edition|7)/ + ret[:os_flavor] = $1 + else + # If we couldn't pull out anything specific for the flavor, just cut + # off the stuff we know for sure isn't it and hope for the best + ret[:os_flavor] ||= str.gsub(/(Microsoft )?Windows|(Service Pack|SP) ?(\d+)/, '').strip + end + + if str =~ /NT|2003|2008|SBS|Server/ + ret[:type] = 'server' + else + ret[:type] = 'client' + end + + ret + end + + # A case switch to return a normalized arch based on a given string. + def get_arch_from_string(str) + case str + when /x64|amd64|x86_64/i + "x64" + when /x86|i[3456]86/i + "x86" + when /PowerPC|PPC|POWER|ppc/ + "ppc" + when /SPARC/i + "sparc" + when /MIPS/i + "mips" + when /ARM/i + "arm" + else + nil + end + end + end end diff --git a/lib/msf/core/model/note.rb b/lib/msf/core/model/note.rb index 74328878d4..c43bb67a48 100644 --- a/lib/msf/core/model/note.rb +++ b/lib/msf/core/model/note.rb @@ -8,6 +8,13 @@ class Note < ActiveRecord::Base belongs_to :host belongs_to :service serialize :data + + def after_save + if data_changed? and ntype =~ /fingerprint/ + host.normalize_os + end + end + end end diff --git a/lib/msf/core/model/service.rb b/lib/msf/core/model/service.rb index a8c5dc10a6..4f8de749be 100644 --- a/lib/msf/core/model/service.rb +++ b/lib/msf/core/model/service.rb @@ -15,6 +15,13 @@ class Service < ActiveRecord::Base has_many :web_vulns, :through => :web_sites serialize :info + + def after_save + if info_changed? + host.normalize_os + end + end + end end diff --git a/lib/msf/core/model/session.rb b/lib/msf/core/model/session.rb new file mode 100644 index 0000000000..6a3610b86e --- /dev/null +++ b/lib/msf/core/model/session.rb @@ -0,0 +1,11 @@ +module Msf +class DBManager + +class Session < ActiveRecord::Base + has_one :host + serialize :datastore + serialize :routes +end + +end +end diff --git a/lib/msf/core/model/session_event.rb b/lib/msf/core/model/session_event.rb new file mode 100644 index 0000000000..a6536050fa --- /dev/null +++ b/lib/msf/core/model/session_event.rb @@ -0,0 +1,11 @@ +module Msf +class DBManager + + # TODO: needs a belongs_to :session when that model gets committed. + +class SessionEvent < ActiveRecord::Base + include DBSave +end + +end +end diff --git a/lib/msf/core/rpc/session.rb b/lib/msf/core/rpc/session.rb index d2be6d61ca..79ba1bd7fc 100644 --- a/lib/msf/core/rpc/session.rb +++ b/lib/msf/core/rpc/session.rb @@ -36,7 +36,7 @@ class Session < Base authenticate(token) s = @framework.sessions[sid.to_i] if(not s) - raise ::XMLRPC::FaultException.new(404, "unknown session") + raise ::XMLRPC::FaultException.new(404, "unknown session while stopping") end s.kill { "result" => "success" } @@ -185,7 +185,7 @@ protected authenticate(token) s = @framework.sessions[sid.to_i] if(not s) - raise ::XMLRPC::FaultException.new(404, "unknown session") + raise ::XMLRPC::FaultException.new(404, "unknown session while validating") end if(s.type != type) raise ::XMLRPC::FaultException.new(403, "session is not "+type) diff --git a/lib/msf/core/session.rb b/lib/msf/core/session.rb index b8f982e3f4..a758d1d7bb 100644 --- a/lib/msf/core/session.rb +++ b/lib/msf/core/session.rb @@ -79,7 +79,8 @@ module Session def initialize self.alive = true self.uuid = Rex::Text.rand_text_alphanumeric(8).downcase - self.routes = [] + @routes = RouteArray.new(self) + #self.routes = [] end # Direct descendents @@ -241,7 +242,16 @@ module Session # # Perform session-specific cleanup. # + # NOTE: session classes overriding this method must call super! + # Also must tolerate being called multiple times. + # def cleanup + if db_record and framework.db.active + db_record.closed_at = Time.now + # ignore exceptions + db_record.save + db_record = nil + end end # @@ -262,6 +272,7 @@ module Session def dead? (not self.alive) end + def alive? (self.alive) end @@ -316,6 +327,10 @@ module Session # An array of routes associated with this session # attr_accessor :routes + # + # This session's associated database record + # + attr_accessor :db_record protected attr_accessor :via # :nodoc: @@ -324,3 +339,21 @@ end end +class RouteArray < Array # :nodoc: all + def initialize(sess) + self.session = sess + super() + end + + def <<(val) + session.framework.events.on_session_route(session, val) + super + end + + def delete(val) + session.framework.events.on_session_route_remove(session, val) + super + end + + attr_accessor :session +end diff --git a/lib/msf/core/session/basic.rb b/lib/msf/core/session/basic.rb index 4c9288abd9..e6b877e0b4 100644 --- a/lib/msf/core/session/basic.rb +++ b/lib/msf/core/session/basic.rb @@ -34,7 +34,11 @@ protected # def _interact framework.events.on_session_interact(self) - interact_stream(rstream) + if self.respond_to?(:ring) + interact_ring(ring) + else + interact_stream(rstream) + end end end diff --git a/lib/msf/core/session/interactive.rb b/lib/msf/core/session/interactive.rb index d6df385a4f..b808f2fbdd 100644 --- a/lib/msf/core/session/interactive.rb +++ b/lib/msf/core/session/interactive.rb @@ -1,4 +1,5 @@ require 'rex/ui' +require 'rex/io/ring_buffer' module Msf module Session @@ -21,7 +22,11 @@ module Interactive # Initializes the session. # def initialize(rstream, opts={}) - self.rstream = rstream + # A nil is passed in the case of non-stream interactive sessions (Meterpreter) + if rstream + self.rstream = rstream + self.ring = Rex::IO::RingBuffer.new(rstream, {:size => opts[:ring_size] || 100 }) + end super() end @@ -37,10 +42,11 @@ module Interactive # Returns the local information. # def tunnel_local + return @local_info if @local_info begin - rstream.localinfo + @local_info = rstream.localinfo rescue ::Exception - '127.0.0.1' + @local_info = '127.0.0.1' end end @@ -48,10 +54,11 @@ module Interactive # Returns the remote peer information. # def tunnel_peer + return @peer_info if @peer_info begin @peer_info = rstream.peerinfo rescue ::Exception - @peer_info ||= '127.0.0.1' + @peer_info = '127.0.0.1' end end @@ -65,7 +72,6 @@ module Interactive # Terminate the session # def kill - self.interacting = false if self.interactive? self.reset_ui self.cleanup super() @@ -76,17 +82,24 @@ module Interactive # def cleanup begin + self.interacting = false if self.interactive? rstream.close if (rstream) rescue ::Exception end rstream = nil + super end # # The remote stream handle. Must inherit from Rex::IO::Stream. # attr_accessor :rstream + + # + # The RingBuffer object used to allow concurrent access to this session + # + attr_accessor :ring protected @@ -104,10 +117,7 @@ protected begin user_want_abort? rescue Interrupt - # The user hit ctrl-c while we were handling a ctrl-c, send a - # literal ctrl-c to the shell. XXX Doesn't actually work. - #$stdout.puts("\n[*] interrupted interrupt, sending literal ctrl-c\n") - #$stdout.puts(run_cmd("\x03")) + # The user hit ctrl-c while we were handling a ctrl-c. Ignore end end diff --git a/lib/msf/core/session_manager.rb b/lib/msf/core/session_manager.rb index 0b4799d83c..95845fb54e 100644 --- a/lib/msf/core/session_manager.rb +++ b/lib/msf/core/session_manager.rb @@ -21,8 +21,47 @@ class SessionManager < Hash self.framework = framework self.sid_pool = 0 self.reaper_thread = framework.threads.spawn("SessionManager", true, self) do |manager| + begin while true - ::IO.select(nil, nil, nil, 0.5) + + rings = values.select{|s| s.respond_to?(:ring) and s.ring and s.rstream } + ready = ::IO.select(rings.map{|s| s.rstream}, nil, nil, 0.5) || [[],[],[]] + + ready[0].each do |fd| + s = rings.select{|s| s.rstream == fd}.first + next if not s + + begin + buff = fd.get_once(-1) + if buff + # Store the data in the associated ring + s.ring.store_data(buff) + + # Store the session event into the database. + # Rescue anything the event handlers raise so they + # don't break our session. + framework.events.on_session_output(s, buff) rescue nil + end + rescue ::Exception => e + wlog("Exception reading from Session #{s.sid}: #{e.class} #{e}") + unless e.kind_of? EOFError + # Don't bother with a call stack if it's just a + # normal EOF + dlog("Call Stack\n#{e.backtrace.join("\n")}", 'core', LEV_3) + end + + # Flush any ring data in the queue + s.ring.clear_data rescue nil + + # Shut down the socket itself + s.rstream.close rescue nil + + # Deregister the session + manager.deregister(s, "Died from #{e.class}") + end + end + + # Check for closed / dead / terminated sessions manager.each_value do |s| if not s.alive? manager.deregister(s, "Died") @@ -31,6 +70,11 @@ class SessionManager < Hash end end end + + rescue ::Exception => e + wlog("Exception in reaper thread #{e.class} #{e}") + wlog("Call Stack\n#{e.backtrace.join("\n")}", 'core', LEV_3) + end end end @@ -61,7 +105,13 @@ class SessionManager < Hash session.framework = framework # Notify the framework that we have a new session opening up... - framework.events.on_session_open(session) + # Don't let errant event handlers kill our session + begin + framework.events.on_session_open(session) + rescue ::Exception => e + wlog("Exception in on_session_open event handler: #{e.class}: #{e}") + wlog("Call Stack\n#{e.backtrace.join("\n")}", 'core', LEV_3) + end if session.respond_to?("console") session.console.on_command_proc = Proc.new { |command, error| framework.events.on_session_command(session, command) } @@ -81,7 +131,7 @@ class SessionManager < Hash end # Tell the framework that we have a parting session - framework.events.on_session_close(session, reason) + framework.events.on_session_close(session, reason) rescue nil # If this session implements the comm interface, remove any routes # that have been created for it. @@ -89,10 +139,6 @@ class SessionManager < Hash Rex::Socket::SwitchBoard.remove_by_comm(session) end - if session.kind_of?(Msf::Session::Interactive) - session.interacting = false - end - # Remove it from the hash self.delete(session.sid.to_i) diff --git a/lib/msf/core/task_manager.rb b/lib/msf/core/task_manager.rb index 19a8af9210..aa5fb13d8e 100644 --- a/lib/msf/core/task_manager.rb +++ b/lib/msf/core/task_manager.rb @@ -211,7 +211,7 @@ class TaskManager elog("taskmanager: task triggered an exception: #{e.class} #{e}") elog("taskmanager: task proc: #{task.proc.inspect} ") - elog("taskmanager: task Call stack: #{task.source.join("\n")} ") + elog("taskmanager: task Call stack: \n#{task.source.join("\n")} ") dlog("Call stack:\n#{$@.join("\n")}") task.status = :dropped diff --git a/lib/msf/core/thread_manager.rb b/lib/msf/core/thread_manager.rb index 21cb6b588a..8b3f8a6499 100644 --- a/lib/msf/core/thread_manager.rb +++ b/lib/msf/core/thread_manager.rb @@ -63,7 +63,8 @@ class ThreadManager < Array begin argv.shift.call(*argv) rescue ::Exception => e - elog("thread exception: #{::Thread.current[:tm_name]} critical=#{::Thread.current[:tm_crit]} error:#{e.class} #{e} #{e.backtrace} source:#{::Thread.current[:tm_call].inspect}") + elog("thread exception: #{::Thread.current[:tm_name]} critical=#{::Thread.current[:tm_crit]} error:#{e.class} #{e} source:#{::Thread.current[:tm_call].inspect}") + elog("Call Stack\n#{e.backtrace.join("\n")}") raise e end end diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index b41f915581..92299c12b5 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -870,11 +870,12 @@ class Core return false end - case args.shift + arg = args.shift + case arg - when "add" + when "add", "remove" if (args.length < 3) - print_error("Missing arguments to route add.") + print_error("Missing arguments to route #{arg}.") return false end @@ -912,56 +913,22 @@ class Core return false end - Rex::Socket::SwitchBoard.add_route( - args[0], - args[1], - gw) - - when "remove" - if (args.length < 3) - print_error("Missing arguments to route remove.") - return false - end - - # Satisfy check to see that formatting is correct - unless Rex::Socket::RangeWalker.new(args[0]).length == 1 - print_error "Invalid IP Address" - return false - end - - unless Rex::Socket::RangeWalker.new(args[1]).length == 1 - print_error "Invalid Subnet mask" - return false - end - - gw = nil - - # Satisfy case problems - args[2] = "Local" if (args[2] =~ /local/i) - - begin - # If the supplied gateway is a global Comm, use it. - if (Rex::Socket::Comm.const_defined?(args[2])) - gw = Rex::Socket::Comm.const_get(args[2]) + if arg == "remove" + worked = Rex::Socket::SwitchBoard.remove_route(args[0], args[1], gw) + if worked + print_status("Route removed") + else + print_error("Route not found") + end + else + worked = Rex::Socket::SwitchBoard.add_route(args[0], args[1], gw) + if worked + print_status("Route added") + else + print_error("Route already exists") end - rescue NameError end - # If we still don't have a gateway, check if it's a session. - if ((gw == nil) and - (session = framework.sessions.get(args[2])) and - (session.kind_of?(Msf::Session::Comm))) - gw = session - elsif (gw == nil) - print_error("Invalid gateway specified.") - return false - end - - Rex::Socket::SwitchBoard.remove_route( - args[0], - args[1], - gw) - when "get" if (args.length == 0) print_error("You must supply an IP address.") diff --git a/lib/rex/io/ring_buffer.rb b/lib/rex/io/ring_buffer.rb new file mode 100644 index 0000000000..7f44f2c1b0 --- /dev/null +++ b/lib/rex/io/ring_buffer.rb @@ -0,0 +1,364 @@ +# +# This class implements a ring buffer with "cursors" in the form of sequence numbers. +# To use this class, pass in a file descriptor and a ring size, the class will read +# data from the file descriptor and store it in the ring. If the ring becomes full, +# the oldest item will be overwritten. To emulate a stream interface, call read_data +# to grab the last sequence number and any buffered data, call read_data again, +# passing in the sequence number and all data newer than that sequence will be +# returned, along with a new sequence to read from. +# + +require 'rex/socket' + +module Rex +module IO + +class RingBuffer + + attr_accessor :queue # The data queue, essentially an array of two-element arrays, containing a sequence and data buffer + attr_accessor :seq # The next available sequence number + attr_accessor :fd # The associated socket or IO object for this ring buffer + attr_accessor :size # The number of available slots in the queue + attr_accessor :mutex # The mutex locking access to the queue + attr_accessor :beg # The index of the earliest data fragment in the ring + attr_accessor :cur # The sequence number of the earliest data fragment in the ring + attr_accessor :monitor # The thread handle of the built-in monitor when used + attr_accessor :monitor_thread_error # :nodoc: # + + # + # Create a new ring buffer + # + def initialize(socket, opts={}) + self.size = opts[:size] || (1024 * 4) + self.fd = socket + self.seq = 0 + self.beg = 0 + self.cur = 0 + self.queue = Array.new( self.size ) + self.mutex = Mutex.new + end + + # + # Start the built-in monitor, not called when used in a larger framework + # + def start_monitor + self.monitor = monitor_thread if not self.monitor + end + + # + # Stop the built-in monitor + # + def stop_monitor + self.monitor.kill if self.monitor + self.monitor = nil + end + + # + # The built-in monitor thread + # + def monitor_thread + Thread.new do + begin + while self.fd + buff = self.fd.get_once(-1, 1.0) + next if not buff + store_data(buff) + end + rescue ::Exception => e + self.monitor_thread_error = e + end + end + end + + # + # Push data back into the associated stream socket. Logging must occur + # elsewhere, this function is simply a passthrough. + # + def put(data) + self.fd.put(data) + end + + # + # The clear_data method wipes the ring buffer + # + def clear_data + self.mutex.synchronize do + self.seq = 0 + self.beg = 0 + self.cur = 0 + self.queue = Array.new( self.size ) + end + end + + # + # The store_data method is used to insert data into the ring buffer. + # + def store_data(data) + self.mutex.synchronize do + # self.cur points to the array index of queue containing the last item + # adding data will result in cur + 1 being used to store said data + # if cur is larger than size - 1, it will wrap back around. If cur + # is *smaller* beg, beg is increemnted to cur + 1 (and wrapped if + # necessary + + loc = 0 + if self.seq > 0 + loc = ( self.cur + 1 ) % self.size + + if loc <= self.beg + self.beg = (self.beg + 1) % self.size + end + end + + self.queue[loc] = [self.seq += 1, data] + self.cur = loc + end + end + + # + # The read_data method returns a two element array with the new reader cursor (a sequence number) + # and the returned data buffer (if any). A result of nil/nil indicates that no data is available + # + def read_data(ptr=nil) + self.mutex.synchronize do + + # Verify that there is data in the queue + return [nil,nil] if not self.queue[self.beg] + + # Configure the beginning read pointer (sequence number, not index) + ptr ||= self.queue[self.beg][0] + return [nil,nil] if not ptr + + # If the pointer is below our baseline, we lost some data, so jump forward + if ptr < self.queue[self.beg][0] + ptr = self.queue[self.beg][0] + end + + # Calculate how many blocks exist between the current sequence number + # and the requested pointer, this becomes the number of blocks we will + # need to read to satisfy the result. Due to the mutex block, we do + # not need to scan to find the sequence of the starting block or + # check the sequence of the ending block. + dis = self.seq - ptr + + # If the requested sequnce number is less than our base pointer, it means + # that no new data is available and we should return empty. + return [nil,nil] if dis < 0 + + # Calculate the beginning block index and number of blocks to read + off = ptr - self.queue[self.beg][0] + set = (self.beg + off) % self.size + + + # Build the buffer by reading forward by the number of blocks needed + # and return the last read sequence number, plus one, as the new read + # pointer. + buff = "" + cnt = 0 + lst = ptr + ptr.upto(self.seq) do |i| + block = self.queue[ (set + cnt) % self.size ] + lst,data = block[0],block[1] + buff += data + cnt += 1 + end + + return [lst + 1, buff] + + end + end + + # + # The base_sequence method returns the earliest sequence number in the queue. This is zero until + # all slots are filled and the ring rotates. + # + def base_sequence + self.mutex.synchronize do + return 0 if not self.queue[self.beg] + return self.queue[self.beg][0] + end + end + + # + # The last_sequence method returns the "next" sequence number where new data will be + # available. + # + def last_sequence + self.seq + end + + # + # The create_steam method assigns a IO::Socket compatible object to the ringer buffer + # + def create_stream + Stream.new(self) + end + + # + # The select method returns when there is a chance of new data + # XXX: This is mostly useless and requires a rewrite to use a + # real select or notify mechanism + # + def select + ::IO.select([ self.fd ], nil, [ self.fd ], 0.10) + end + + # + # The wait method blocks until new data is available + # + def wait(seq) + nseq = nil + while not nseq + nseq,data = read_data(seq) + select + end + end + + # + # The wait_for method blocks until new data is available or the timeout is reached + # + def wait_for(seq,timeout=1) + begin + ::Timeout.timeout(timeout) do + wait(seq) + end + rescue ::Timeout::Error + end + end + + # + # This class provides a backwards compatible "stream" socket that uses + # the parents ring buffer. + # + class Stream + attr_accessor :ring + attr_accessor :seq + attr_accessor :buff + + def initialize(ring) + self.ring = ring + self.seq = ring.base_sequence + self.buff = '' + end + + def read(len=nil) + if len and self.buff.length >= len + data = self.buff.slice!(0,len) + return data + end + + while true + lseq, data = self.ring.read_data( self.seq ) + return if not lseq + + self.seq = lseq + self.buff << data + if len + if self.buff.length >= len + return self.buff.slice!(0,len) + else + IO.select(nil, nil, nil, 0.25) + next + end + end + + data = self.buff + self.buff = '' + + return data + + # Not reached + break + end + + end + + def write(data) + self.ring.write(data) + end + end + +end + +end +end + +=begin + +server = Rex::Socket.create_tcp_server('LocalPort' => 0) +lport = server.getsockname[2] +client = Rex::Socket.create_tcp('PeerHost' => '127.0.0.1', 'PeerPort' => lport) +conn = server.accept + +r = Rex::IO::RingBuffer.new(conn, {:size => 1024*1024}) +client.put("1") +client.put("2") +client.put("3") + +s,d = r.read_data + +client.put("4") +client.put("5") +client.put("6") +s,d = r.read_data(s) + +client.put("7") +client.put("8") +client.put("9") +s,d = r.read_data(s) + +client.put("0") +s,d = r.read_data(s) + +test_counter = 11 +1.upto(100) do + client.put( "X" ) + test_counter += 1 +end + +sleep(1) + +s,d = r.read_data +p s +p d + +fdata = '' +File.open("/bin/ls", "rb") do |fd| + fdata = fd.read(fd.stat.size) + fdata = fdata * 10 + client.put(fdata) +end + +sleep(1) + +s,vdata = r.read_data(s) + +if vdata != fdata + puts "DATA FAILED" +else + puts "DATA VERIFIED" +end + +r.clear_data + +a = r.create_stream +b = r.create_stream + +client.put("ABC123") +sleep(1) + +p a.read +p b.read + +client.put("$$$$$$") +sleep(1) + +p a.read +p b.read + +c = r.create_stream +p c.read + +=end + + diff --git a/lib/rex/text.rb b/lib/rex/text.rb index d8bee04c3f..84d05f0b3d 100644 --- a/lib/rex/text.rb +++ b/lib/rex/text.rb @@ -597,6 +597,20 @@ module Text [ str.downcase.gsub(/'/,'').gsub(/\\?x([a-f0-9][a-f0-9])/, '\1') ].pack("H*") end + # + # Turn non-printable chars into hex representations, leaving others alone + # + # If +whitespace+ is true, converts whitespace (0x20, 0x09, etc) to hex as + # well. + # + def self.ascii_safe_hex(str, whitespace=false) + if whitespace + str.gsub(/([\x00-\x20\x80-\xFF])/){ |x| "\\x%.2x" % x.unpack("C*")[0] } + else + str.gsub(/([\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xFF])/n){ |x| "\\x%.2x" % x.unpack("C*")[0]} + end + end + # # Wraps text at a given column using a supplied indention # diff --git a/lib/rex/ui/interactive.rb b/lib/rex/ui/interactive.rb index 3281248ccb..ea3ebea1b9 100644 --- a/lib/rex/ui/interactive.rb +++ b/lib/rex/ui/interactive.rb @@ -199,6 +199,50 @@ protected end end + + # + # Interacts between a local stream and a remote ring buffer. This has to use + # a secondary thread to prevent the select on the local stream from blocking + # + def interact_ring(ring) + begin + + rdr = Rex::ThreadFactory.spawn("RingMonitor", false) do + seq = nil + while self.interacting + + # Look for any pending data from the remote ring + nseq,data = ring.read_data(seq) + + # Update the sequence number if necessary + seq = nseq || seq + + # Write output to the local stream if successful + user_output.print(data) if data + + # Wait for new data to arrive on this session + ring.wait(seq) + end + end + + while self.interacting + + # Look for any pending input from the local stream + sd = Rex::ThreadSafe.select([ _local_fd ], nil, [_local_fd], 5.0) + + # Write input to the ring's input mechanism + if sd + data = user_input.gets + ring.put(data) + end + end + + ensure + rdr.kill + end + end + + # # Installs a signal handler to monitor suspend signal notifications. # diff --git a/modules/auxiliary/server/browser_autopwn.rb b/modules/auxiliary/server/browser_autopwn.rb index 013979d0fb..9a0b23f725 100644 --- a/modules/auxiliary/server/browser_autopwn.rb +++ b/modules/auxiliary/server/browser_autopwn.rb @@ -801,14 +801,20 @@ class Metasploit3 < Msf::Auxiliary (os_name, os_flavor, os_sp, os_lang, arch, ua_name, ua_ver) = detected_version.split(':') if framework.db.active - host_info = { :host => cli.peerhost } - host_info[:os_name] = os_name if os_name != "undefined" - host_info[:os_flavor] = os_flavor if os_flavor != "undefined" - host_info[:os_sp] = os_sp if os_sp != "undefined" - host_info[:os_lang] = os_lang if os_lang != "undefined" - host_info[:arch] = arch if arch != "undefined" + note_data = { } + note_data[:os_name] = os_name if os_name != "undefined" + note_data[:os_flavor] = os_flavor if os_flavor != "undefined" + note_data[:os_sp] = os_sp if os_sp != "undefined" + note_data[:os_lang] = os_lang if os_lang != "undefined" + note_data[:arch] = arch if arch != "undefined" + print_status("Reporting: #{note_data.inspect}") - report_host(host_info) + report_note({ + :host => cli.peerhost, + :type => 'javascript_fingerprint', + :data => note_data, + :update => :unique_data, + }) client_info = ({ :host => cli.peerhost, :ua_string => request['User-Agent'],