# -*- coding: binary -*- # Check Rex::Parser.nokogiri_loaded for status of the Nokogiri parsers require 'rex/parser/nmap_nokogiri' require 'rex/parser/nexpose_simple_nokogiri' require 'rex/parser/nexpose_raw_nokogiri' require 'rex/parser/foundstone_nokogiri' require 'rex/parser/mbsa_nokogiri' require 'rex/parser/acunetix_nokogiri' require 'rex/parser/appscan_nokogiri' require 'rex/parser/burp_session_nokogiri' require 'rex/parser/ci_nokogiri' require 'rex/parser/wapiti_nokogiri' require 'rex/parser/openvas_nokogiri' require 'rex/parser/fusionvm_nokogiri' # Legacy XML parsers -- these will be converted some day require 'rex/parser/nmap_xml' require 'rex/parser/nexpose_xml' require 'rex/parser/retina_xml' require 'rex/parser/netsparker_xml' require 'rex/parser/nessus_xml' require 'rex/parser/ip360_xml' require 'rex/parser/ip360_aspl_xml' require 'rex/socket' require 'zip' require 'packetfu' require 'uri' require 'tmpdir' require 'csv' module Msf ### # # The states that a host can be in. # ### module HostState # # The host is alive. # Alive = "alive" # # The host is dead. # Dead = "down" # # The host state is unknown. # Unknown = "unknown" end ### # # The states that a service can be in. # ### module ServiceState Open = "open" Closed = "closed" Filtered = "filtered" Unknown = "unknown" end ### # # Events that can occur in the host/service database. # ### module DatabaseEvent # # Called when an existing host's state changes # def on_db_host_state(host, ostate) end # # Called when an existing service's state changes # def on_db_service_state(host, port, ostate) end # # Called when a new host is added to the database. The host parameter is # of type Host. # def on_db_host(host) end # # Called when a new client is added to the database. The client # parameter is of type Client. # def on_db_client(client) end # # Called when a new service is added to the database. The service # parameter is of type Service. # def on_db_service(service) end # # Called when an applicable vulnerability is found for a service. The vuln # parameter is of type Vuln. # def on_db_vuln(vuln) end # # Called when a new reference is created. # def on_db_ref(ref) end end class DBImportError < RuntimeError end ### # # The DB module ActiveRecord definitions for the DBManager # ### class DBManager def rfc3330_reserved(ip) case ip.class.to_s when "PacketFu::Octets" ip_x = ip.to_x ip_i = ip.to_i when "String" if ipv46_validator(ip) ip_x = ip ip_i = Rex::Socket.addr_atoi(ip) else raise ArgumentError, "Invalid IP address: #{ip.inspect}" end when "Fixnum" if (0..2**32-1).include? ip ip_x = Rex::Socket.addr_itoa(ip) ip_i = ip else raise ArgumentError, "Invalid IP address: #{ip.inspect}" end else raise ArgumentError, "Invalid IP address: #{ip.inspect}" end return true if Rex::Socket::RangeWalker.new("0.0.0.0-0.255.255.255").include? ip_x return true if Rex::Socket::RangeWalker.new("127.0.0.0-127.255.255.255").include? ip_x return true if Rex::Socket::RangeWalker.new("169.254.0.0-169.254.255.255").include? ip_x return true if Rex::Socket::RangeWalker.new("224.0.0.0-239.255.255.255").include? ip_x return true if Rex::Socket::RangeWalker.new("255.255.255.255-255.255.255.255").include? ip_x return false end def ipv46_validator(addr) ipv4_validator(addr) or ipv6_validator(addr) end def ipv4_validator(addr) return false unless addr.kind_of? String Rex::Socket.is_ipv4?(addr) end def ipv6_validator(addr) Rex::Socket.is_ipv6?(addr) end # Takes a space-delimited set of ips and ranges, and subjects # them to RangeWalker for validation. Returns true or false. def validate_ips(ips) ret = true begin ips.split(/\s+/).each {|ip| unless Rex::Socket::RangeWalker.new(ip).ranges ret = false break end } rescue ret = false end return ret end # # Determines if the database is functional # def check ::ActiveRecord::Base.connection_pool.with_connection { res = ::Mdm::Host.find(:first) } end def default_workspace ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::Workspace.default } end def find_workspace(name) ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::Workspace.find_by_name(name) } end # # Creates a new workspace in the database # def add_workspace(name) ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::Workspace.find_or_create_by_name(name) } end def workspaces ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::Workspace.find(:all) } end # # Wait for all pending write to finish # def sync # There is no more queue. end # # Find a host. Performs no database writes. # def get_host(opts) if opts.kind_of? ::Mdm::Host return opts elsif opts.kind_of? String raise RuntimeError, "This invokation of get_host is no longer supported: #{caller}" else address = opts[:addr] || opts[:address] || opts[:host] || return return address if address.kind_of? ::Mdm::Host end ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace if wspace.kind_of? String wspace = find_workspace(wspace) end address = normalize_host(address) return wspace.hosts.find_by_address(address) } end # # Exactly like report_host but waits for the database to create a host and returns it. # def find_or_create_host(opts) report_host(opts) end # # Report a host's attributes such as operating system and service pack # # The opts parameter MUST contain # +:host+:: -- the host's ip address # # The opts parameter can contain: # +:state+:: -- one of the Msf::HostState constants # +:os_name+:: -- one of the Msf::OperatingSystems constants # +:os_flavor+:: -- something like "XP" or "Gentoo" # +:os_sp+:: -- something like "SP2" # +:os_lang+:: -- something like "English", "French", or "en-US" # +:arch+:: -- one of the ARCH_* constants # +:mac+:: -- the host's MAC address # +:scope+:: -- interface identifier for link-local IPv6 # +:virtual_host+:: -- the name of the VM host software, eg "VMWare", "QEMU", "Xen", etc. # def report_host(opts) return if not active addr = opts.delete(:host) || return # Sometimes a host setup through a pivot will see the address as "Remote Pipe" if addr.eql? "Remote Pipe" return end ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace if wspace.kind_of? String wspace = find_workspace(wspace) end ret = { } if not addr.kind_of? ::Mdm::Host addr = normalize_host(addr) addr, scope = addr.split('%', 2) opts[:scope] = scope if scope unless ipv46_validator(addr) raise ::ArgumentError, "Invalid IP address in report_host(): #{addr}" end if opts[:comm] and opts[:comm].length > 0 host = wspace.hosts.find_or_initialize_by_address_and_comm(addr, opts[:comm]) else host = wspace.hosts.find_or_initialize_by_address(addr) end else host = addr end # Truncate the info field at the maximum field length if opts[:info] opts[:info] = opts[:info][0,65535] end # Truncate the name field at the maximum field length if opts[:name] opts[:name] = opts[:name][0,255] end opts.each { |k,v| if (host.attribute_names.include?(k.to_s)) unless host.attribute_locked?(k.to_s) host[k] = v.to_s.gsub(/[\x00-\x1f]/, '') end else dlog("Unknown attribute for ::Mdm::Host: #{k}") end } host.info = host.info[0,::Mdm::Host.columns_hash["info"].limit] if host.info # Set default fields if needed host.state = HostState::Alive if not host.state host.comm = '' if not host.comm host.workspace = wspace if not host.workspace if host.changed? msf_import_timestamps(opts,host) host.save! end host } end # # Update a host's attributes via semi-standardized sysinfo hash (Meterpreter) # # The opts parameter MUST contain the following entries # +:host+:: -- the host's ip address # +:info+:: -- the information hash # * 'Computer' -- the host name # * 'OS' -- the operating system string # * 'Architecture' -- the hardware architecture # * 'System Language' -- the system language # # The opts parameter can contain: # +:workspace+:: -- the workspace for this host # def update_host_via_sysinfo(opts) return if not active addr = opts.delete(:host) || return info = opts.delete(:info) || return # Sometimes a host setup through a pivot will see the address as "Remote Pipe" if addr.eql? "Remote Pipe" return end ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace if wspace.kind_of? String wspace = find_workspace(wspace) end if not addr.kind_of? ::Mdm::Host addr = normalize_host(addr) addr, scope = addr.split('%', 2) opts[:scope] = scope if scope unless ipv46_validator(addr) raise ::ArgumentError, "Invalid IP address in report_host(): #{addr}" end if opts[:comm] and opts[:comm].length > 0 host = wspace.hosts.find_or_initialize_by_address_and_comm(addr, opts[:comm]) else host = wspace.hosts.find_or_initialize_by_address(addr) end else host = addr end res = {} if info['Computer'] res[:name] = info['Computer'] end if info['Architecture'] res[:arch] = info['Architecture'].split(/\s+/).first end if info['OS'] =~ /^Windows\s*([^\(]+)\(([^\)]+)\)/i res[:os_name] = "Microsoft Windows" res[:os_flavor] = $1.strip build = $2.strip if build =~ /Service Pack (\d+)/ res[:os_sp] = "SP" + $1 else res[:os_sp] = "SP0" end end if info["System Language"] case info["System Language"] when /^en_/ res[:os_lang] = "English" end end # Truncate the info field at the maximum field length if res[:info] res[:info] = res[:info][0,65535] end # Truncate the name field at the maximum field length if res[:name] res[:name] = res[:name][0,255] end res.each { |k,v| if (host.attribute_names.include?(k.to_s)) unless host.attribute_locked?(k.to_s) host[k] = v.to_s.gsub(/[\x00-\x1f]/, '') end else dlog("Unknown attribute for Host: #{k}") end } # Set default fields if needed host.state = HostState::Alive if not host.state host.comm = '' if not host.comm host.workspace = wspace if not host.workspace if host.changed? host.save! end host } end # # Iterates over the hosts table calling the supplied block with the host # instance of each entry. # def each_host(wspace=workspace, &block) ::ActiveRecord::Base.connection_pool.with_connection { wspace.hosts.each do |host| block.call(host) end } end # # Returns a list of all hosts in the database # def hosts(wspace = workspace, only_up = false, addresses = nil) ::ActiveRecord::Base.connection_pool.with_connection { conditions = {} conditions[:state] = [Msf::HostState::Alive, Msf::HostState::Unknown] if only_up conditions[:address] = addresses if addresses wspace.hosts.where(conditions).order(:address) } end def find_or_create_service(opts) report_service(opts) end # # Record a service in the database. # # opts MUST contain # +:host+:: the host where this service is running # +:port+:: the port where this service listens # +:proto+:: the transport layer protocol (e.g. tcp, udp) # # opts may contain # +:name+:: the application layer protocol (e.g. ssh, mssql, smb) # +:sname+:: an alias for the above # def report_service(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { |conn| addr = opts.delete(:host) || return hname = opts.delete(:host_name) hmac = opts.delete(:mac) host = nil wspace = opts.delete(:workspace) || workspace hopts = {:workspace => wspace, :host => addr} hopts[:name] = hname if hname hopts[:mac] = hmac if hmac # Other report_* methods take :sname to mean the service name, so we # map it here to ensure it ends up in the right place despite not being # a real column. if opts[:sname] opts[:name] = opts.delete(:sname) end if addr.kind_of? ::Mdm::Host host = addr addr = host.address else host = report_host(hopts) end if opts[:port].to_i.zero? dlog("Skipping port zero for service '%s' on host '%s'" % [opts[:name],host.address]) return nil end ret = {} =begin host = get_host(:workspace => wspace, :address => addr) if host host.updated_at = host.created_at host.state = HostState::Alive host.save! end =end proto = opts[:proto] || 'tcp' service = host.services.find_or_initialize_by_port_and_proto(opts[:port].to_i, proto) opts.each { |k,v| if (service.attribute_names.include?(k.to_s)) service[k] = ((v and k == :name) ? v.to_s.downcase : v) else dlog("Unknown attribute for Service: #{k}") end } service.state ||= ServiceState::Open service.info ||= "" if (service and service.changed?) msf_import_timestamps(opts,service) service.save! end ret[:service] = service } end def get_service(wspace, host, proto, port) ::ActiveRecord::Base.connection_pool.with_connection { host = get_host(:workspace => wspace, :address => host) return if not host return host.services.find_by_proto_and_port(proto, port) } end # # Iterates over the services table calling the supplied block with the # service instance of each entry. # def each_service(wspace=workspace, &block) ::ActiveRecord::Base.connection_pool.with_connection { services(wspace).each do |service| block.call(service) end } end # # Returns a list of all services in the database # def services(wspace = workspace, only_up = false, proto = nil, addresses = nil, ports = nil, names = nil) ::ActiveRecord::Base.connection_pool.with_connection { conditions = {} conditions[:state] = [ServiceState::Open] if only_up conditions[:proto] = proto if proto conditions["hosts.address"] = addresses if addresses conditions[:port] = ports if ports conditions[:name] = names if names wspace.services.includes(:host).where(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 ::ActiveRecord::Base.connection_pool.with_connection { 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 ::Mdm::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 ::ActiveRecord::Base.connection_pool.with_connection { 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.respond_to?(:arch) and 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.utc, :last_seen => Time.now.utc, :local_id => session.sid } elsif opts[:host] raise ArgumentError.new("Invalid :host, expected Host object") unless opts[:host].kind_of? ::Mdm::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], :last_seen => opts[:last_seen] || opts[:closed_at], :close_reason => opts[:close_reason], } else raise ArgumentError.new("Missing option :session or :host") end ret = {} # Truncate the session data if necessary if sess_data[:desc] sess_data[:desc] = sess_data[:desc][0,255] end # In the case of multi handler we cannot yet determine the true # exploit responsible. But we can at least show the parent versus # just the generic handler: if session and session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule'] sess_data[:via_exploit] = sess_data[:datastore]['ParentModule'] end s = ::Mdm::Session.new(sess_data) s.save! if opts[:session] session.db_record = s end # If this is a live session, we know the host is vulnerable to something. if opts[:session] and session.via_exploit return unless host mod = framework.modules.create(session.via_exploit) if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule'] mod_fullname = sess_data[:datastore]['ParentModule'] mod_name = ::Mdm::ModuleDetail.find_by_fullname(mod_fullname).name else mod_name = mod.name mod_fullname = mod.fullname end vuln_info = { :host => host.address, :name => mod_name, :refs => mod.references, :workspace => wspace, :exploited_at => Time.now.utc, :info => "Exploited by #{mod_fullname} to create Session #{s.id}" } port = session.exploit_datastore["RPORT"] service = (port ? host.services.find_by_port(port.to_i) : nil) vuln_info[:service] = service if service vuln = framework.db.report_vuln(vuln_info) if session.via_exploit == "exploit/multi/handler" and sess_data[:datastore]['ParentModule'] via_exploit = sess_data[:datastore]['ParentModule'] else via_exploit = session.via_exploit end attempt_info = { :timestamp => Time.now.utc, :workspace => wspace, :module => via_exploit, :username => session.username, :refs => mod.references, :session_id => s.id, :host => host, :service => service, :vuln => vuln } framework.db.report_exploit_success(attempt_info) end s } end # # Record a session event in the database # # opts MUST contain one of: # +:session+:: the Msf::Session OR the ::Mdm::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] session = nil ::ActiveRecord::Base.connection_pool.with_connection { if opts[:session].respond_to? :db_record session = opts[:session].db_record if session.nil? # The session doesn't have a db_record which means # a) the database wasn't connected at session registration time # or # b) something awful happened and the report_session call failed # # Either way, we can't do anything with this session as is, so # log a warning and punt. wlog("Warning: trying to report a session_event for a session with no db_record (#{opts[:session].sid})") return end event_data = { :created_at => Time.now } else session = opts[:session] event_data = { :created_at => opts[:created_at] } 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 s = ::Mdm::SessionEvent.create(event_data) } end def report_session_route(session, route) return if not active if session.respond_to? :db_record s = session.db_record else s = session end unless s.respond_to?(:routes) raise ArgumentError.new("Invalid :session, expected Session object got #{session.class}") end ::ActiveRecord::Base.connection_pool.with_connection { subnet, netmask = route.split("/") s.routes.create(:subnet => subnet, :netmask => netmask) } end def report_session_route_remove(session, route) return if not active if session.respond_to? :db_record s = session.db_record else s = session end unless s.respond_to?(:routes) raise ArgumentError.new("Invalid :session, expected Session object got #{session.class}") end ::ActiveRecord::Base.connection_pool.with_connection { subnet, netmask = route.split("/") r = s.routes.find_by_subnet_and_netmask(subnet, netmask) r.destroy if r } end def report_exploit_success(opts) ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace mrefs = opts.delete(:refs) || return host = opts.delete(:host) port = opts.delete(:port) prot = opts.delete(:proto) svc = opts.delete(:service) vuln = opts.delete(:vuln) timestamp = opts.delete(:timestamp) username = opts.delete(:username) mname = opts.delete(:module) # Look up or generate the host as appropriate if not (host and host.kind_of? ::Mdm::Host) if svc.kind_of? ::Mdm::Service host = svc.host else host = report_host(:workspace => wspace, :address => host ) end end # Bail if we dont have a host object return if not host # Look up or generate the service as appropriate if port and svc.nil? svc = report_service(:workspace => wspace, :host => host, :port => port, :proto => prot ) if port end if not vuln # Create a references map from the module list ref_objs = ::Mdm::Ref.where(:name => mrefs.map { |ref| if ref.respond_to?(:ctx_id) and ref.respond_to?(:ctx_val) "#{ref.ctx_id}-#{ref.ctx_val}" else ref.to_s end }) # Try find a matching vulnerability vuln = find_vuln_by_refs(ref_objs, host, svc) end # We have match, lets create a vuln_attempt record if vuln attempt_info = { :vuln_id => vuln.id, :attempted_at => timestamp || Time.now.utc, :exploited => true, :username => username || "unknown", :module => mname } attempt_info[:session_id] = opts[:session_id] if opts[:session_id] attempt_info[:loot_id] = opts[:loot_id] if opts[:loot_id] vuln.vuln_attempts.create(attempt_info) # Correct the vuln's associated service if necessary if svc and vuln.service_id.nil? vuln.service = svc vuln.save end end # Report an exploit attempt all the same attempt_info = { :attempted_at => timestamp || Time.now.utc, :exploited => true, :username => username || "unknown", :module => mname } attempt_info[:vuln_id] = vuln.id if vuln attempt_info[:session_id] = opts[:session_id] if opts[:session_id] attempt_info[:loot_id] = opts[:loot_id] if opts[:loot_id] if svc attempt_info[:port] = svc.port attempt_info[:proto] = svc.proto end if port and svc.nil? attempt_info[:port] = port attempt_info[:proto] = prot || "tcp" end host.exploit_attempts.create(attempt_info) } end def report_exploit_failure(opts) ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace mrefs = opts.delete(:refs) || return host = opts.delete(:host) port = opts.delete(:port) prot = opts.delete(:proto) svc = opts.delete(:service) vuln = opts.delete(:vuln) timestamp = opts.delete(:timestamp) freason = opts.delete(:fail_reason) fdetail = opts.delete(:fail_detail) username = opts.delete(:username) mname = opts.delete(:module) # Look up the host as appropriate if not (host and host.kind_of? ::Mdm::Host) if svc.kind_of? ::Mdm::Service host = svc.host else host = get_host( :workspace => wspace, :address => host ) end end # Bail if we dont have a host object return if not host # Look up the service as appropriate if port and svc.nil? prot ||= "tcp" svc = get_service(wspace, host, prot, port) if port end if not vuln # Create a references map from the module list ref_objs = ::Mdm::Ref.where(:name => mrefs.map { |ref| if ref.respond_to?(:ctx_id) and ref.respond_to?(:ctx_val) "#{ref.ctx_id}-#{ref.ctx_val}" else ref.to_s end }) # Try find a matching vulnerability vuln = find_vuln_by_refs(ref_objs, host, svc) end # Report a vuln_attempt if we found a match if vuln attempt_info = { :attempted_at => timestamp || Time.now.utc, :exploited => false, :fail_reason => freason, :fail_detail => fdetail, :username => username || "unknown", :module => mname } vuln.vuln_attempts.create(attempt_info) end # Report an exploit attempt all the same attempt_info = { :attempted_at => timestamp || Time.now.utc, :exploited => false, :username => username || "unknown", :module => mname, :fail_reason => freason, :fail_detail => fdetail } attempt_info[:vuln_id] = vuln.id if vuln if svc attempt_info[:port] = svc.port attempt_info[:proto] = svc.proto end if port and svc.nil? attempt_info[:port] = port attempt_info[:proto] = prot || "tcp" end host.exploit_attempts.create(attempt_info) } end def report_vuln_attempt(vuln, opts) ::ActiveRecord::Base.connection_pool.with_connection { return if not vuln info = {} # Opts can be keyed by strings or symbols ::Mdm::VulnAttempt.column_names.each do |kn| k = kn.to_sym next if ['id', 'vuln_id'].include?(kn) info[k] = opts[kn] if opts[kn] info[k] = opts[k] if opts[k] end return unless info[:attempted_at] vuln.vuln_attempts.create(info) } end def report_exploit_attempt(host, opts) ::ActiveRecord::Base.connection_pool.with_connection { return if not host info = {} # Opts can be keyed by strings or symbols ::Mdm::VulnAttempt.column_names.each do |kn| k = kn.to_sym next if ['id', 'host_id'].include?(kn) info[k] = opts[kn] if opts[kn] info[k] = opts[k] if opts[k] end host.exploit_attempts.create(info) } end def get_client(opts) ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace host = get_host(:workspace => wspace, :host => opts[:host]) || return client = host.clients.where({:ua_string => opts[:ua_string]}).first() return client } end def find_or_create_client(opts) report_client(opts) end # # Report a client running on a host. # # opts MUST contain # +:ua_string+:: the value of the User-Agent header # +:host+:: the host where this client connected from, can be an ip address or a Host object # # opts can contain # +:ua_name+:: one of the Msf::HttpClients constants # +:ua_ver+:: detected version of the given client # +:campaign+:: an id or Campaign object # # Returns a Client. # def report_client(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { addr = opts.delete(:host) || return wspace = opts.delete(:workspace) || workspace report_host(:workspace => wspace, :host => addr) ret = {} host = get_host(:workspace => wspace, :host => addr) client = host.clients.find_or_initialize_by_ua_string(opts[:ua_string]) opts[:ua_string] = opts[:ua_string].to_s campaign = opts.delete(:campaign) if campaign case campaign when Campaign opts[:campaign_id] = campaign.id else opts[:campaign_id] = campaign end end opts.each { |k,v| if (client.attribute_names.include?(k.to_s)) client[k] = v else dlog("Unknown attribute for Client: #{k}") end } if (client and client.changed?) client.save! end ret[:client] = client } end # # This method iterates the vulns table calling the supplied block with the # vuln instance of each entry. # def each_vuln(wspace=workspace,&block) ::ActiveRecord::Base.connection_pool.with_connection { wspace.vulns.each do |vulns| block.call(vulns) end } end # # This methods returns a list of all vulnerabilities in the database # def vulns(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.vulns } end # # This methods returns a list of all credentials in the database # def creds(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { Mdm::Cred.includes({:service => :host}).where("hosts.workspace_id = ?", wspace.id) } end # # This method returns a list of all exploited hosts in the database. # def exploited_hosts(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.exploited_hosts } end # # This method iterates the notes table calling the supplied block with the # note instance of each entry. # def each_note(wspace=workspace, &block) ::ActiveRecord::Base.connection_pool.with_connection { wspace.notes.each do |note| block.call(note) end } end # # Find or create a note matching this type/data # def find_or_create_note(opts) report_note(opts) end # # Report a Note to the database. Notes can be tied to a ::Mdm::Workspace, Host, or Service. # # opts MUST contain # +:data+:: whatever it is you're making a note of # +:type+:: The type of note, e.g. smb_peer_os # # opts can contain # +:workspace+:: the workspace to associate with this Note # +:host+:: an IP address or a Host object to associate with this Note # +:service+:: a Service object to associate with this Note # +:port+:: along with +:host+ and +:proto+, a service to associate with this Note # +:proto+:: along with +:host+ and +:port+, a service to associate with this Note # +:update+:: what to do in case a similar Note exists, see below # # The +:update+ option can have the following values: # +:unique+:: allow only a single Note per +:host+/+:type+ pair # +:unique_data+:: like +:uniqe+, but also compare +:data+ # +:insert+:: always insert a new Note even if one with identical values exists # # If the provided +:host+ is an IP address and does not exist in the # database, it will be created. If +:workspace+, +:host+ and +:service+ # are all omitted, the new Note will be associated with the current # workspace. # def report_note(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { 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 addr = nil # Report the host so it's there for the Proc to use below if opts[:host] if opts[:host].kind_of? ::Mdm::Host host = opts[:host] else addr = normalize_host(opts[:host]) host = report_host({:workspace => wspace, :host => addr}) end # Do the same for a service if that's also included. if (opts[:port]) proto = nil sname = nil case opts[:proto].to_s.downcase # Catch incorrect usages when 'tcp','udp' proto = opts[:proto] sname = opts[:sname] if opts[:sname] when 'dns','snmp','dhcp' proto = 'udp' sname = opts[:proto] else proto = 'tcp' sname = opts[:proto] end sopts = { :workspace => wspace, :host => host, :port => opts[:port], :proto => proto } sopts[:name] = sname if sname report_service(sopts) end end # Update Modes can be :unique, :unique_data, :insert mode = opts[:update] || :unique ret = {} if addr and not host host = get_host(:workspace => wspace, :host => addr) end if host and (opts[:port] and opts[:proto]) service = get_service(wspace, host, opts[:proto], opts[:port]) elsif opts[:service] and opts[:service].kind_of? ::Mdm::Service service = opts[:service] end =begin if host host.updated_at = host.created_at host.state = HostState::Alive host.save! end =end ntype = opts.delete(:type) || opts.delete(:ntype) || (raise RuntimeError, "A note :type or :ntype is required") data = opts[:data] || (raise RuntimeError, "Note :data is required") method = nil args = [] note = nil conditions = { :ntype => ntype } conditions[:host_id] = host[:id] if host conditions[:service_id] = service[:id] if service case mode when :unique notes = wspace.notes.where(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? note = wspace.notes.new(conditions) else note = notes[0] end note.data = data when :unique_data notes = wspace.notes.where(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| # Compare the deserialized data from the table to the raw # data we're looking for. Because of the serialization we # can't do this easily or reliably in SQL. if n.data == data note = n break end end if not note # We didn't find one with the data we're looking for, make # a new one. note = wspace.notes.new(conditions.merge(:data => data)) end else # Otherwise, assume :insert, which means always make a new one note = wspace.notes.new if host note.host_id = host[:id] end if opts[:service] and opts[:service].kind_of? ::Mdm::Service note.service_id = opts[:service][:id] end note.seen = seen note.critical = crit note.ntype = ntype note.data = data end msf_import_timestamps(opts,note) note.save! ret[:note] = note } end # # This methods returns a list of all notes in the database # def notes(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.notes } end # This is only exercised by MSF3 XML importing for now. Needs the wait # conditions and return hash as well. def report_host_tag(opts) name = opts.delete(:name) raise DBImportError.new("Missing required option :name") unless name addr = opts.delete(:addr) raise DBImportError.new("Missing required option :addr") unless addr wspace = opts.delete(:wspace) raise DBImportError.new("Missing required option :wspace") unless wspace ::ActiveRecord::Base.connection_pool.with_connection { if wspace.kind_of? String wspace = find_workspace(wspace) end host = nil report_host(:workspace => wspace, :address => addr) host = get_host(:workspace => wspace, :address => addr) desc = opts.delete(:desc) summary = opts.delete(:summary) detail = opts.delete(:detail) crit = opts.delete(:crit) possible_tags = Mdm::Tag.includes(:hosts).where("hosts.workspace_id = ? and tags.name = ?", wspace.id, name).order("tags.id DESC").limit(1) tag = (possible_tags.blank? ? Mdm::Tag.new : possible_tags.first) tag.name = name tag.desc = desc tag.report_summary = !!summary tag.report_detail = !!detail tag.critical = !!crit tag.hosts = tag.hosts | [host] tag.save! if tag.changed? } end # # Store a set of credentials in the database. # # report_auth_info used to create a note, now it creates # an entry in the creds table. It's much more akin to # report_vuln() now. # # opts MUST contain # +:host+:: an IP address or Host object reference # +:port+:: a port number # # opts can contain # +:user+:: the username # +:pass+:: the password, or path to ssh_key # +:ptype+:: the type of password (password(ish), hash, or ssh_key) # +:proto+:: a transport name for the port # +:sname+:: service name # +:active+:: by default, a cred is active, unless explicitly false # +:proof+:: data used to prove the account is actually active. # # Sources: Credentials can be sourced from another credential, or from # a vulnerability. For example, if an exploit was used to dump the # smb_hashes, and this credential comes from there, the source_id would # be the Vuln id (as reported by report_vuln) and the type would be "Vuln". # # +:source_id+:: The Vuln or Cred id of the source of this cred. # +:source_type+:: Either Vuln or Cred # # TODO: This is written somewhat host-centric, when really the # Service is the thing. Need to revisit someday. def report_auth_info(opts={}) return if not active raise ArgumentError.new("Missing required option :host") if opts[:host].nil? raise ArgumentError.new("Missing required option :port") if (opts[:port].nil? and opts[:service].nil?) if (not opts[:host].kind_of?(::Mdm::Host)) and (not validate_ips(opts[:host])) raise ArgumentError.new("Invalid address or object for :host (#{opts[:host].inspect})") end host = opts.delete(:host) ptype = opts.delete(:type) || "password" token = [opts.delete(:user), opts.delete(:pass)] sname = opts.delete(:sname) port = opts.delete(:port) proto = opts.delete(:proto) || "tcp" proof = opts.delete(:proof) source_id = opts.delete(:source_id) source_type = opts.delete(:source_type) duplicate_ok = opts.delete(:duplicate_ok) # Nil is true for active. active = (opts[:active] || opts[:active].nil?) ? true : false wspace = opts.delete(:workspace) || workspace # Service management; assume the user knows what # he's talking about. service = opts.delete(:service) || report_service(:host => host, :port => port, :proto => proto, :name => sname, :workspace => wspace) # Non-US-ASCII usernames are tripping up the database at the moment, this is a temporary fix until we update the tables ( token[0] = token[0].gsub(/[\x00-\x1f\x7f-\xff]/){|m| "\\x%.2x" % m.unpack("C")[0] } ) if token[0] ( token[1] = token[1].gsub(/[\x00-\x1f\x7f-\xff]/){|m| "\\x%.2x" % m.unpack("C")[0] } ) if token[1] ret = {} #Check to see if the creds already exist. We look also for a downcased username with the #same password because we can fairly safely assume they are not in fact two seperate creds. #this allows us to hedge against duplication of creds in the DB. if duplicate_ok # If duplicate usernames are okay, find by both user and password (allows # for actual duplicates to get modified updated_at, sources, etc) if token[0].nil? or token[0].empty? cred = service.creds.find_or_initialize_by_user_and_ptype_and_pass(token[0] || "", ptype, token[1] || "") else cred = service.creds.find_by_user_and_ptype_and_pass(token[0] || "", ptype, token[1] || "") unless cred dcu = token[0].downcase cred = service.creds.find_by_user_and_ptype_and_pass( dcu || "", ptype, token[1] || "") unless cred cred = service.creds.find_or_initialize_by_user_and_ptype_and_pass(token[0] || "", ptype, token[1] || "") end end end else # Create the cred by username only (so we can change passwords) if token[0].nil? or token[0].empty? cred = service.creds.find_or_initialize_by_user_and_ptype(token[0] || "", ptype) else cred = service.creds.find_by_user_and_ptype(token[0] || "", ptype) unless cred dcu = token[0].downcase cred = service.creds.find_by_user_and_ptype_and_pass( dcu || "", ptype, token[1] || "") unless cred cred = service.creds.find_or_initialize_by_user_and_ptype(token[0] || "", ptype) end end end end # Update with the password cred.pass = (token[1] || "") # Annotate the credential cred.ptype = ptype cred.active = active # Update the source ID only if there wasn't already one. if source_id and !cred.source_id cred.source_id = source_id cred.source_type = source_type if source_type end # Safe proof (lazy way) -- doesn't chop expanded # characters correctly, but shouldn't ever be a problem. unless proof.nil? proof = Rex::Text.to_hex_ascii(proof) proof = proof[0,4096] end cred.proof = proof # Update the timestamp if cred.changed? msf_import_timestamps(opts,cred) cred.save! end # Ensure the updated_at is touched any time report_auth_info is called # except when it's set explicitly (as it is for imports) unless opts[:updated_at] || opts["updated_at"] cred.updated_at = Time.now.utc cred.save! end ret[:cred] = cred end alias :report_cred :report_auth_info alias :report_auth :report_auth_info # # Find or create a credential matching this type/data # def find_or_create_cred(opts) report_auth_info(opts) end # # This method iterates the creds table calling the supplied block with the # cred instance of each entry. # def each_cred(wspace=workspace,&block) ::ActiveRecord::Base.connection_pool.with_connection { wspace.creds.each do |cred| block.call(cred) end } end def each_exploited_host(wspace=workspace,&block) ::ActiveRecord::Base.connection_pool.with_connection { wspace.exploited_hosts.each do |eh| block.call(eh) end } end # # Find or create a vuln matching this service/name # def find_or_create_vuln(opts) report_vuln(opts) end # # opts MUST contain # +:host+:: the host where this vulnerability resides # +:name+:: the friendly name for this vulnerability (title) # # opts can contain # +:info+:: a human readable description of the vuln, free-form text # +:refs+:: an array of Ref objects or string names of references # +:details:: a hash with :key pointed to a find criteria hash and the rest containing VulnDetail fields # def report_vuln(opts) return if not active raise ArgumentError.new("Missing required option :host") if opts[:host].nil? raise ArgumentError.new("Deprecated data column for vuln, use .info instead") if opts[:data] name = opts[:name] || return info = opts[:info] ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace exploited_at = opts[:exploited_at] || opts["exploited_at"] details = opts.delete(:details) rids = opts.delete(:ref_ids) if opts[:refs] rids ||= [] opts[:refs].each do |r| if (r.respond_to?(:ctx_id)) and (r.respond_to?(:ctx_val)) r = "#{r.ctx_id}-#{r.ctx_val}" end rids << find_or_create_ref(:name => r) end end host = nil addr = nil if opts[:host].kind_of? ::Mdm::Host host = opts[:host] else host = report_host({:workspace => wspace, :host => opts[:host]}) addr = normalize_host(opts[:host]) end ret = {} # Truncate the info field at the maximum field length if info info = info[0,65535] end # Truncate the name field at the maximum field length name = name[0,255] # Placeholder for the vuln object vuln = nil # Identify the associated service service = opts.delete(:service) # Treat port zero as no service if service or opts[:port].to_i > 0 if not service proto = nil case opts[:proto].to_s.downcase # Catch incorrect usages, as in report_note when 'tcp','udp' proto = opts[:proto] when 'dns','snmp','dhcp' proto = 'udp' sname = opts[:proto] else proto = 'tcp' sname = opts[:proto] end service = host.services.find_or_create_by_port_and_proto(opts[:port].to_i, proto) end # Try to find an existing vulnerability with the same service & references # If there are multiple matches, choose the one with the most matches # If a match is found on a vulnerability with no associated service, # update that vulnerability with our service information. This helps # prevent dupes of the same vuln found by both local patch and # service detection. if rids and rids.length > 0 vuln = find_vuln_by_refs(rids, host, service) vuln.service = service if vuln end else # Try to find an existing vulnerability with the same host & references # If there are multiple matches, choose the one with the most matches if rids and rids.length > 0 vuln = find_vuln_by_refs(rids, host) end end # Try to match based on vuln_details records if not vuln and opts[:details_match] vuln = find_vuln_by_details(opts[:details_match], host, service) if vuln and service and not vuln.service vuln.service = service end end # No matches, so create a new vuln record unless vuln if service vuln = service.vulns.find_by_name(name) else vuln = host.vulns.find_by_name(name) end unless vuln vinf = { :host_id => host.id, :name => name, :info => info } vinf[:service_id] = service.id if service vuln = Mdm::Vuln.create(vinf) end end # Set the exploited_at value if provided vuln.exploited_at = exploited_at if exploited_at # Merge the references if rids vuln.refs << (rids - vuln.refs) end # Finalize if vuln.changed? msf_import_timestamps(opts,vuln) vuln.save! end # Handle vuln_details parameters report_vuln_details(vuln, details) if details vuln } end def find_vuln_by_refs(refs, host, service=nil) vuln = nil # Try to find an existing vulnerability with the same service & references # If there are multiple matches, choose the one with the most matches if service refs_ids = refs.map{|x| x.id } vuln = service.vulns.find(:all, :include => [:refs], :conditions => { 'refs.id' => refs_ids }).sort { |a,b| ( refs_ids - a.refs.map{|x| x.id } ).length <=> ( refs_ids - b.refs.map{|x| x.id } ).length }.first end # Return if we matched based on service return vuln if vuln # Try to find an existing vulnerability with the same host & references # If there are multiple matches, choose the one with the most matches refs_ids = refs.map{|x| x.id } vuln = host.vulns.find(:all, :include => [:refs], :conditions => { 'service_id' => nil, 'refs.id' => refs_ids }).sort { |a,b| ( refs_ids - a.refs.map{|x| x.id } ).length <=> ( refs_ids - b.refs.map{|x| x.id } ).length }.first return vuln end def find_vuln_by_details(details_map, host, service=nil) # Create a modified version of the criteria in order to match against # the joined version of the fields crit = {} details_map.each_pair do |k,v| crit[ "vuln_details.#{k}" ] = v end vuln = nil if service vuln = service.vulns.find(:first, :include => [:vuln_details], :conditions => crit) end # Return if we matched based on service return vuln if vuln # Prevent matches against other services crit["vulns.service_id"] = nil if service vuln = host.vulns.find(:first, :include => [:vuln_details], :conditions => crit) return vuln end def get_vuln(wspace, host, service, name, data='') raise RuntimeError, "Not workspace safe: #{caller.inspect}" ::ActiveRecord::Base.connection_pool.with_connection { vuln = nil if (service) vuln = ::Mdm::Vuln.find.where("name = ? and service_id = ? and host_id = ?", name, service.id, host.id).order("vulns.id DESC").first() else vuln = ::Mdm::Vuln.find.where("name = ? and host_id = ?", name, host.id).first() end return vuln } end # # Find or create a reference matching this name # def find_or_create_ref(opts) ret = {} ret[:ref] = get_ref(opts[:name]) return ret[:ref] if ret[:ref] ::ActiveRecord::Base.connection_pool.with_connection { ref = ::Mdm::Ref.find_or_initialize_by_name(opts[:name]) if ref and ref.changed? ref.save! end ret[:ref] = ref } end def get_ref(name) ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::Ref.find_by_name(name) } end # # Populate the vuln_details table with additional # information, matched by a specific criteria # def report_vuln_details(vuln, details) ::ActiveRecord::Base.connection_pool.with_connection { detail = ::Mdm::VulnDetail.where(( details.delete(:key) || {} ).merge(:vuln_id => vuln.id)).first if detail details.each_pair do |k,v| detail[k] = v end detail.save! if detail.changed? detail else detail = ::Mdm::VulnDetail.create(details.merge(:vuln_id => vuln.id)) end } end # # Update vuln_details records en-masse based on specific criteria # Note that this *can* update data across workspaces # def update_vuln_details(details) criteria = details.delete(:key) || {} ::Mdm::VulnDetail.update(key, details) end # # Populate the host_details table with additional # information, matched by a specific criteria # def report_host_details(host, details) ::ActiveRecord::Base.connection_pool.with_connection { detail = ::Mdm::HostDetail.where(( details.delete(:key) || {} ).merge(:host_id => host.id)).first if detail details.each_pair do |k,v| detail[k] = v end detail.save! if detail.changed? detail else detail = ::Mdm::HostDetail.create(details.merge(:host_id => host.id)) end } end # report_exploit() used to be used to track sessions and which modules # opened them. That information is now available with the session table # directly. TODO: kill this completely some day -- for now just warn if # some other UI is actually using it. def report_exploit(opts={}) wlog("Deprecated method call: report_exploit()\n" + "report_exploit() options: #{opts.inspect}\n" + "report_exploit() call stack:\n\t#{caller.join("\n\t")}" ) end # # Deletes a host and associated data matching this address/comm # def del_host(wspace, address, comm='') ::ActiveRecord::Base.connection_pool.with_connection { address, scope = address.split('%', 2) host = wspace.hosts.find_by_address_and_comm(address, comm) host.destroy if host } end # # Deletes a port and associated vulns matching this port # def del_service(wspace, address, proto, port, comm='') host = get_host(:workspace => wspace, :address => address) return unless host ::ActiveRecord::Base.connection_pool.with_connection { host.services.where({:proto => proto, :port => port}).each { |s| s.destroy } } end # # Find a reference matching this name # def has_ref?(name) ::ActiveRecord::Base.connection_pool.with_connection { Mdm::Ref.find_by_name(name) } end # # Find a vulnerability matching this name # def has_vuln?(name) ::ActiveRecord::Base.connection_pool.with_connection { Mdm::Vuln.find_by_name(name) } end # # Look for an address across all comms # def has_host?(wspace,addr) ::ActiveRecord::Base.connection_pool.with_connection { address, scope = addr.split('%', 2) wspace.hosts.find_by_address(addr) } end def events(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.events.find :all, :order => 'created_at ASC' } end def report_event(opts = {}) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace return if not wspace # Temp fix? uname = opts.delete(:username) if ! opts[:host].kind_of? ::Mdm::Host and opts[:host] opts[:host] = report_host(:workspace => wspace, :host => opts[:host]) end ::Mdm::Event.create(opts.merge(:workspace_id => wspace[:id], :username => uname)) } end # # Loot collection # # # This method iterates the loot table calling the supplied block with the # instance of each entry. # def each_loot(wspace=workspace, &block) ::ActiveRecord::Base.connection_pool.with_connection { wspace.loots.each do |note| block.call(note) end } end # # Find or create a loot matching this type/data # def find_or_create_loot(opts) report_loot(opts) end def report_loot(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace path = opts.delete(:path) || (raise RuntimeError, "A loot :path is required") host = nil addr = nil # Report the host so it's there for the Proc to use below if opts[:host] if opts[:host].kind_of? ::Mdm::Host host = opts[:host] else host = report_host({:workspace => wspace, :host => opts[:host]}) addr = normalize_host(opts[:host]) end end ret = {} ltype = opts.delete(:type) || opts.delete(:ltype) || (raise RuntimeError, "A loot :type or :ltype is required") ctype = opts.delete(:ctype) || opts.delete(:content_type) || 'text/plain' name = opts.delete(:name) info = opts.delete(:info) data = opts[:data] loot = wspace.loots.new if host loot.host_id = host[:id] end if opts[:service] and opts[:service].kind_of? ::Mdm::Service loot.service_id = opts[:service][:id] end loot.path = path loot.ltype = ltype loot.content_type = ctype loot.data = data loot.name = name if name loot.info = info if info msf_import_timestamps(opts,loot) loot.save! if !opts[:created_at] =begin if host host.updated_at = host.created_at host.state = HostState::Alive host.save! end =end end ret[:loot] = loot } end # # This methods returns a list of all loot in the database # def loots(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.loots } end # # Find or create a task matching this type/data # def find_or_create_task(opts) report_task(opts) end def report_task(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace path = opts.delete(:path) || (raise RuntimeError, "A task :path is required") ret = {} user = opts.delete(:user) desc = opts.delete(:desc) error = opts.delete(:error) info = opts.delete(:info) mod = opts.delete(:mod) options = opts.delete(:options) prog = opts.delete(:prog) result = opts.delete(:result) completed_at = opts.delete(:completed_at) task = wspace.tasks.new task.created_by = user task.description = desc task.error = error if error task.info = info task.module = mod task.options = options task.path = path task.progress = prog task.result = result if result msf_import_timestamps(opts,task) # Having blank completed_ats, while accurate, will cause unstoppable tasks. if completed_at.nil? || completed_at.empty? task.completed_at = opts[:updated_at] else task.completed_at = completed_at end task.save! ret[:task] = task } end # # This methods returns a list of all tasks in the database # def tasks(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.tasks } end # # Find or create a task matching this type/data # def find_or_create_report(opts) report_report(opts) end def report_report(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace path = opts.delete(:path) || (raise RuntimeError, "A report :path is required") ret = {} user = opts.delete(:user) options = opts.delete(:options) rtype = opts.delete(:rtype) report = wspace.reports.new report.created_by = user report.options = options report.rtype = rtype report.path = path msf_import_timestamps(opts,report) report.save! ret[:task] = report } end # # This methods returns a list of all reports in the database # def reports(wspace=workspace) ::ActiveRecord::Base.connection_pool.with_connection { wspace.reports } end # # WMAP # Support methods # # # Report a Web Site to the database. WebSites must be tied to an existing Service # # opts MUST contain # +:service+:: the service object this site should be associated with # +:vhost+:: the virtual host name for this particular web site` # # If +:service+ is NOT specified, the following values are mandatory # +:host+:: the ip address of the server hosting the web site # +:port+:: the port number of the associated web site # +:ssl+:: whether or not SSL is in use on this port # # These values will be used to create new host and service records # # opts can contain # +:options+:: a hash of options for accessing this particular web site # +:info+:: if present, report the service with this info # # Duplicate records for a given host, port, vhost combination will be overwritten # def report_web_site(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { |conn| wspace = opts.delete(:workspace) || workspace vhost = opts.delete(:vhost) addr = nil port = nil name = nil serv = nil info = nil if opts[:service] and opts[:service].kind_of?(::Mdm::Service) serv = opts[:service] else addr = opts[:host] port = opts[:port] name = opts[:ssl] ? 'https' : 'http' info = opts[:info] if not (addr and port) raise ArgumentError, "report_web_site requires service OR host/port/ssl" end # Force addr to be the address and not hostname addr = Rex::Socket.getaddress(addr, true) end ret = {} host = serv ? serv.host : find_or_create_host( :workspace => wspace, :host => addr, :state => Msf::HostState::Alive ) if host.name.to_s.empty? host.name = vhost host.save! end serv = serv ? serv : find_or_create_service( :workspace => wspace, :host => host, :port => port, :proto => 'tcp', :state => 'open' ) # Change the service name if it is blank or it has # been explicitly specified. if opts.keys.include?(:ssl) or serv.name.to_s.empty? name = opts[:ssl] ? 'https' : 'http' serv.name = name end # Add the info if it's there. unless info.to_s.empty? serv.info = info end serv.save! if serv.changed? =begin host.updated_at = host.created_at host.state = HostState::Alive host.save! =end vhost ||= host.address site = ::Mdm::WebSite.find_or_initialize_by_vhost_and_service_id(vhost, serv[:id]) site.options = opts[:options] if opts[:options] # XXX: msf_import_timestamps(opts, site) site.save! ret[:web_site] = site } end # # Report a Web Page to the database. WebPage must be tied to an existing Web Site # # opts MUST contain # +:web_site+:: the web site object that this page should be associated with # +:path+:: the virtual host name for this particular web site # +:code+:: the http status code from requesting this page # +:headers+:: this is a HASH of headers (lowercase name as key) of ARRAYs of values # +:body+:: the document body of the server response # +:query+:: the query string after the path # # If web_site is NOT specified, the following values are mandatory # +:host+:: the ip address of the server hosting the web site # +:port+:: the port number of the associated web site # +:vhost+:: the virtual host for this particular web site # +:ssl+:: whether or not SSL is in use on this port # # These values will be used to create new host, service, and web_site records # # opts can contain # +:cookie+:: the Set-Cookie headers, merged into a string # +:auth+:: the Authorization headers, merged into a string # +:ctype+:: the Content-Type headers, merged into a string # +:mtime+:: the timestamp returned from the server of the last modification time # +:location+:: the URL that a redirect points to # # Duplicate records for a given web_site, path, and query combination will be overwritten # def report_web_page(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace path = opts[:path] code = opts[:code].to_i body = opts[:body].to_s query = opts[:query].to_s headers = opts[:headers] site = nil if not (path and code and body and headers) raise ArgumentError, "report_web_page requires the path, query, code, body, and headers parameters" end if opts[:web_site] and opts[:web_site].kind_of?(::Mdm::WebSite) site = opts.delete(:web_site) else site = report_web_site( :workspace => wspace, :host => opts[:host], :port => opts[:port], :vhost => opts[:host], :ssl => opts[:ssl] ) if not site raise ArgumentError, "report_web_page was unable to create the associated web site" end end ret = {} page = ::Mdm::WebPage.find_or_initialize_by_web_site_id_and_path_and_query(site[:id], path, query) page.code = code page.body = body page.headers = headers page.cookie = opts[:cookie] if opts[:cookie] page.auth = opts[:auth] if opts[:auth] page.mtime = opts[:mtime] if opts[:mtime] page.ctype = opts[:ctype] if opts[:ctype] page.location = opts[:location] if opts[:location] msf_import_timestamps(opts, page) page.save! ret[:web_page] = page } end # # Report a Web Form to the database. WebForm must be tied to an existing Web Site # # opts MUST contain # +:web_site+:: the web site object that this page should be associated with # +:path+:: the virtual host name for this particular web site # +:query+:: the query string that is appended to the path (not valid for GET) # +:method+:: the form method, one of GET, POST, or PATH # +:params+:: an ARRAY of all parameters and values specified in the form # # If web_site is NOT specified, the following values are mandatory # +:host+:: the ip address of the server hosting the web site # +:port+:: the port number of the associated web site # +:vhost+:: the virtual host for this particular web site # +:ssl+:: whether or not SSL is in use on this port # # Duplicate records for a given web_site, path, method, and params combination will be overwritten # def report_web_form(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace path = opts[:path] meth = opts[:method].to_s.upcase para = opts[:params] quer = opts[:query].to_s site = nil if not (path and meth) raise ArgumentError, "report_web_form requires the path and method parameters" end if not %W{GET POST PATH}.include?(meth) raise ArgumentError, "report_web_form requires the method to be one of GET, POST, PATH" end if opts[:web_site] and opts[:web_site].kind_of?(::Mdm::WebSite) site = opts.delete(:web_site) else site = report_web_site( :workspace => wspace, :host => opts[:host], :port => opts[:port], :vhost => opts[:host], :ssl => opts[:ssl] ) if not site raise ArgumentError, "report_web_form was unable to create the associated web site" end end ret = {} # Since one of our serialized fields is used as a unique parameter, we must do the final # comparisons through ruby and not SQL. form = nil ::Mdm::WebForm.find_all_by_web_site_id_and_path_and_method_and_query(site[:id], path, meth, quer).each do |xform| if xform.params == para form = xform break end end if not form form = ::Mdm::WebForm.new form.web_site_id = site[:id] form.path = path form.method = meth form.params = para form.query = quer end msf_import_timestamps(opts, form) form.save! ret[:web_form] = form } end # # Report a Web Vuln to the database. WebVuln must be tied to an existing Web Site # # opts MUST contain # +:web_site+:: the web site object that this page should be associated with # +:path+:: the virtual host name for this particular web site # +:query+:: the query string appended to the path (not valid for GET method flaws) # +:method+:: the form method, one of GET, POST, or PATH # +:params+:: an ARRAY of all parameters and values specified in the form # +:pname+:: the specific field where the vulnerability occurs # +:proof+:: the string showing proof of the vulnerability # +:risk+:: an INTEGER value from 0 to 5 indicating the risk (5 is highest) # +:name+:: the string indicating the type of vulnerability # # If web_site is NOT specified, the following values are mandatory # +:host+:: the ip address of the server hosting the web site # +:port+:: the port number of the associated web site # +:vhost+:: the virtual host for this particular web site # +:ssl+:: whether or not SSL is in use on this port # # # Duplicate records for a given web_site, path, method, pname, and name # combination will be overwritten # def report_web_vuln(opts) return if not active ::ActiveRecord::Base.connection_pool.with_connection { wspace = opts.delete(:workspace) || workspace path = opts[:path] meth = opts[:method] para = opts[:params] || [] quer = opts[:query].to_s pname = opts[:pname] proof = opts[:proof] risk = opts[:risk].to_i name = opts[:name].to_s.strip blame = opts[:blame].to_s.strip desc = opts[:description].to_s.strip conf = opts[:confidence].to_i cat = opts[:category].to_s.strip payload = opts[:payload].to_s owner = opts[:owner] ? opts[:owner].shortname : nil site = nil if not (path and meth and proof and pname) raise ArgumentError, "report_web_vuln requires the path, method, proof, risk, name, params, and pname parameters. Received #{opts.inspect}" end if not %W{GET POST PATH}.include?(meth) raise ArgumentError, "report_web_vuln requires the method to be one of GET, POST, PATH. Received '#{meth}'" end if risk < 0 or risk > 5 raise ArgumentError, "report_web_vuln requires the risk to be between 0 and 5 (inclusive). Received '#{risk}'" end if conf < 0 or conf > 100 raise ArgumentError, "report_web_vuln requires the confidence to be between 1 and 100 (inclusive). Received '#{conf}'" end if cat.empty? raise ArgumentError, "report_web_vuln requires the category to be a valid string" end if name.empty? raise ArgumentError, "report_web_vuln requires the name to be a valid string" end if opts[:web_site] and opts[:web_site].kind_of?(::Mdm::WebSite) site = opts.delete(:web_site) else site = report_web_site( :workspace => wspace, :host => opts[:host], :port => opts[:port], :vhost => opts[:host], :ssl => opts[:ssl] ) if not site raise ArgumentError, "report_web_form was unable to create the associated web site" end end ret = {} meth = meth.to_s.upcase vuln = ::Mdm::WebVuln.find_or_initialize_by_web_site_id_and_path_and_method_and_pname_and_name_and_category_and_query(site[:id], path, meth, pname, name, cat, quer) vuln.name = name vuln.risk = risk vuln.params = para vuln.proof = proof.to_s vuln.category = cat vuln.blame = blame vuln.description = desc vuln.confidence = conf vuln.payload = payload vuln.owner = owner msf_import_timestamps(opts, vuln) vuln.save! ret[:web_vuln] = vuln } end # # WMAP # Selected host # def selected_host ::ActiveRecord::Base.connection_pool.with_connection { selhost = ::Mdm::WmapTarget.where("selected != 0").first() if selhost return selhost.host else return end } end # # WMAP # Selected target # def selected_wmap_target ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapTarget.find.where("selected != 0") } end # # WMAP # Selected port # def selected_port selected_wmap_target.port end # # WMAP # Selected ssl # def selected_ssl selected_wmap_target.ssl end # # WMAP # Selected id # def selected_id selected_wmap_target.object_id end # # WMAP # This method iterates the requests table identifiying possible targets # This method wiil be remove on second phase of db merging. # def each_distinct_target(&block) request_distinct_targets.each do |target| block.call(target) end end # # WMAP # This method returns a list of all possible targets available in requests # This method wiil be remove on second phase of db merging. # def request_distinct_targets ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapRequest.select('DISTINCT host,address,port,ssl') } end # # WMAP # This method iterates the requests table returning a list of all requests of a specific target # def each_request_target_with_path(&block) target_requests('AND wmap_requests.path IS NOT NULL').each do |req| block.call(req) end end # # WMAP # This method iterates the requests table returning a list of all requests of a specific target # def each_request_target_with_query(&block) target_requests('AND wmap_requests.query IS NOT NULL').each do |req| block.call(req) end end # # WMAP # This method iterates the requests table returning a list of all requests of a specific target # def each_request_target_with_body(&block) target_requests('AND wmap_requests.body IS NOT NULL').each do |req| block.call(req) end end # # WMAP # This method iterates the requests table returning a list of all requests of a specific target # def each_request_target_with_headers(&block) target_requests('AND wmap_requests.headers IS NOT NULL').each do |req| block.call(req) end end # # WMAP # This method iterates the requests table returning a list of all requests of a specific target # def each_request_target(&block) target_requests('').each do |req| block.call(req) end end # # WMAP # This method returns a list of all requests from target # def target_requests(extra_condition) ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapRequest.where("wmap_requests.host = ? AND wmap_requests.port = ? #{extra_condition}",selected_host,selected_port) } end # # WMAP # This method iterates the requests table calling the supplied block with the # request instance of each entry. # def each_request(&block) requests.each do |request| block.call(request) end end # # WMAP # This method allows to query directly the requests table. To be used mainly by modules # def request_sql(host,port,extra_condition) ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapRequest.where("wmap_requests.host = ? AND wmap_requests.port = ? #{extra_condition}", host , port) } end # # WMAP # This methods returns a list of all targets in the database # def requests ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapRequest.find(:all) } end # # WMAP # This method iterates the targets table calling the supplied block with the # target instance of each entry. # def each_target(&block) targets.each do |target| block.call(target) end end # # WMAP # This methods returns a list of all targets in the database # def targets ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapTarget.find(:all) } end # # WMAP # This methods deletes all targets from targets table in the database # def delete_all_targets ::ActiveRecord::Base.connection_pool.with_connection { ::Mdm::WmapTarget.delete_all } end # # WMAP # Find a target matching this id # def get_target(id) ::ActiveRecord::Base.connection_pool.with_connection { target = ::Mdm::WmapTarget.where("id = ?", id).first() return target } end # # WMAP # Create a target # def create_target(host,port,ssl,sel) ::ActiveRecord::Base.connection_pool.with_connection { tar = ::Mdm::WmapTarget.create( :host => host, :address => host, :port => port, :ssl => ssl, :selected => sel ) #framework.events.on_db_target(rec) } end # # WMAP # Create a request (by hand) # def create_request(host,port,ssl,meth,path,headers,query,body,respcode,resphead,response) ::ActiveRecord::Base.connection_pool.with_connection { req = ::Mdm::WmapRequest.create( :host => host, :address => host, :port => port, :ssl => ssl, :meth => meth, :path => path, :headers => headers, :query => query, :body => body, :respcode => respcode, :resphead => resphead, :response => response ) #framework.events.on_db_request(rec) } end # # WMAP # Quick way to query the database (used by wmap_sql) # def sql_query(sqlquery) ::ActiveRecord::Base.connection_pool.with_connection { ActiveRecord::Base.connection.select_all(sqlquery) } end # Returns a REXML::Document from the given data. def rexmlify(data) if data.kind_of?(REXML::Document) return data else # Make an attempt to recover from a REXML import fail, since # it's better than dying outright. begin return REXML::Document.new(data) rescue REXML::ParseException => e dlog("REXML error: Badly formatted XML, attempting to recover. Error was: #{e.inspect}") return REXML::Document.new(data.gsub(/([\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xff])/){ |x| "\\x%.2x" % x.unpack("C*")[0] }) end end end # Handles timestamps from Metasploit Express/Pro imports. def msf_import_timestamps(opts,obj) obj.created_at = opts["created_at"] if opts["created_at"] obj.created_at = opts[:created_at] if opts[:created_at] obj.updated_at = opts["updated_at"] ? opts["updated_at"] : obj.created_at obj.updated_at = opts[:updated_at] ? opts[:updated_at] : obj.created_at return obj end ## # # Import methods # ## # # Generic importer that automatically determines the file type being # imported. Since this looks for vendor-specific strings in the given # file, there shouldn't be any false detections, but no guarantees. # def import_file(args={}, &block) filename = args[:filename] || args['filename'] wspace = args[:wspace] || args['wspace'] || workspace @import_filedata = {} @import_filedata[:filename] = filename data = "" ::File.open(filename, 'rb') do |f| data = f.read(4) end case data[0,4] when "PK\x03\x04" data = Zip::ZipFile.open(filename) when "\xd4\xc3\xb2\xa1", "\xa1\xb2\xc3\xd4" data = PacketFu::PcapFile.new(:filename => filename) else ::File.open(filename, 'rb') do |f| sz = f.stat.size data = f.read(sz) end end if block import(args.merge(:data => data)) { |type,data| yield type,data } else import(args.merge(:data => data)) end end # A dispatcher method that figures out the data's file type, # and sends it off to the appropriate importer. Note that # import_file_detect will raise an error if the filetype # is unknown. def import(args={}, &block) data = args[:data] || args['data'] wspace = args[:wspace] || args['wspace'] || workspace ftype = import_filetype_detect(data) yield(:filetype, @import_filedata[:type]) if block self.send "import_#{ftype}".to_sym, args, &block end # Returns one of: :nexpose_simplexml :nexpose_rawxml :nmap_xml :openvas_xml # :nessus_xml :nessus_xml_v2 :qualys_scan_xml, :qualys_asset_xml, :msf_xml :nessus_nbe :amap_mlog # :amap_log :ip_list, :msf_zip, :libpcap, :foundstone_xml, :acunetix_xml, :appscan_xml # :burp_session, :ip360_xml_v3, :ip360_aspl_xml, :nikto_xml # If there is no match, an error is raised instead. def import_filetype_detect(data) if data and data.kind_of? Zip::ZipFile raise DBImportError.new("The zip file provided is empty.") if data.entries.empty? @import_filedata ||= {} @import_filedata[:zip_filename] = File.split(data.to_s).last @import_filedata[:zip_basename] = @import_filedata[:zip_filename].gsub(/\.zip$/,"") @import_filedata[:zip_entry_names] = data.entries.map {|x| x.name} begin @import_filedata[:zip_xml] = @import_filedata[:zip_entry_names].grep(/^(.*)_[0-9]+\.xml$/).first || raise @import_filedata[:zip_wspace] = @import_filedata[:zip_xml].to_s.match(/^(.*)_[0-9]+\.xml$/)[1] @import_filedata[:type] = "Metasploit ZIP Report" return :msf_zip rescue ::Interrupt raise $! rescue ::Exception raise DBImportError.new("The zip file provided is not a Metasploit ZIP report") end end if data and data.kind_of? PacketFu::PcapFile # Don't check for emptiness here because unlike other formats, we # haven't read any actual data in yet, only magic bytes to discover # that this is indeed a pcap file. #raise DBImportError.new("The pcap file provided is empty.") if data.body.empty? @import_filedata ||= {} @import_filedata[:type] = "Libpcap Packet Capture" return :libpcap end # This is a text string, lets make sure its treated as binary data = data.unpack("C*").pack("C*") if data and data.to_s.strip.length == 0 raise DBImportError.new("The data provided to the import function was empty") end # Parse the first line or 4k of data from the file di = data.index("\n") || 4096 firstline = data[0, di] @import_filedata ||= {} if (firstline.index("")) @import_filedata[:type] = "Retina XML" return :retina_xml elsif (firstline.index("")) @import_filedata[:type] = "OpenVAS XML" return :openvas_new_xml elsif (firstline.index("")) @import_filedata[:type] = "Nessus XML (v1)" return :nessus_xml elsif (firstline.index("]/ case $1 when "niktoscan" @import_filedata[:type] = "Nikto XML" return :nikto_xml when "nmaprun" @import_filedata[:type] = "Nmap XML" return :nmap_xml when "openvas-report" @import_filedata[:type] = "OpenVAS Report" return :openvas_xml when "NessusClientData" @import_filedata[:type] = "Nessus XML (v1)" return :nessus_xml when "NessusClientData_v2" @import_filedata[:type] = "Nessus XML (v2)" return :nessus_xml_v2 when "SCAN" @import_filedata[:type] = "Qualys Scan XML" return :qualys_scan_xml when "report" @import_filedata[:type] = "Wapiti XML" return :wapiti_xml when "ASSET_DATA_REPORT" @import_filedata[:type] = "Qualys Asset XML" return :qualys_asset_xml when /MetasploitExpressV[1234]/ @import_filedata[:type] = "Metasploit XML" return :msf_xml when /MetasploitV4/ @import_filedata[:type] = "Metasploit XML" return :msf_xml when /netsparker/ @import_filedata[:type] = "NetSparker XML" return :netsparker_xml when /audits?/ # and are both valid for nCircle. wtfmate. @import_filedata[:type] = "IP360 XML v3" return :ip360_xml_v3 when /ontology/ @import_filedata[:type] = "IP360 ASPL" return :ip360_aspl_xml when /ReportInfo/ @import_filedata[:type] = "Foundstone" return :foundstone_xml when /ScanGroup/ @import_filedata[:type] = "Acunetix" return :acunetix_xml when /AppScanInfo/ # Actually the second line @import_filedata[:type] = "Appscan" return :appscan_xml when "entities" if line =~ /creator.*\x43\x4f\x52\x45\x20\x49\x4d\x50\x41\x43\x54/i @import_filedata[:type] = "CI" return :ci_xml end else # Give up if we haven't hit the root tag in the first few lines break if line_count > 10 end line_count += 1 } elsif (firstline.index("timestamps|||scan_start")) @import_filedata[:type] = "Nessus NBE Report" # then it's a nessus nbe return :nessus_nbe elsif (firstline.index("# amap v")) # then it's an amap mlog @import_filedata[:type] = "Amap Log -m" return :amap_mlog elsif (firstline.index("amap v")) # then it's an amap log @import_filedata[:type] = "Amap Log" return :amap_log elsif ipv46_validator(firstline) # then its an IP list @import_filedata[:type] = "IP Address List" return :ip_list elsif (data[0,1024].index(" wspace, :host => addr, :type => "service.nikto.scan.description", :data => desc_text, :proto => "tcp", :port => port.to_i, :sname => uri.scheme, :update => :unique_data } # Always report it as a note. report_note(desc_data) # Sometimes report it as a vuln, too. # XXX: There's a Vuln.info field but nothing reads from it? See Bug #5837 if item.attributes['osvdbid'].to_i != 0 desc_data[:refs] = ["OSVDB-#{item.attributes['osvdbid']}"] desc_data[:name] = "NIKTO-#{item.attributes['id']}" desc_data.delete(:data) desc_data.delete(:type) desc_data.delete(:update) report_vuln(desc_data) end end end end end end def import_wapiti_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_wapiti_xml(args.merge(:data => data)) end def import_wapiti_xml(args={}, &block) if block doc = Rex::Parser::WapitiDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::WapitiDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_openvas_new_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_wapiti_xml(args.merge(:data => data)) end def import_openvas_new_xml(args={}, &block) if block doc = Rex::Parser::OpenVASDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::OpenVASDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_libpcap_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = PacketFu::PcapFile.new(:filename => filename) import_libpcap(args.merge(:data => data)) end # The libpcap file format is handled by PacketFu for data # extraction. TODO: Make this its own mixin, and possibly # extend PacketFu to do better stream analysis on the fly. def import_libpcap(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] # seen_hosts is only used for determining when to yield an address. Once we get # some packet analysis going, the values will have all sorts of info. The plan # is to ru through all the packets as a first pass and report host and service, # then, once we have everything parsed, we can reconstruct sessions and ngrep # out things like authentication sequences, examine ttl's and window sizes, all # kinds of crazy awesome stuff like that. seen_hosts = {} decoded_packets = 0 last_count = 0 data.read_packet_bytes do |p| if (decoded_packets >= last_count + 1000) and block yield(:pcap_count, decoded_packets) last_count = decoded_packets end decoded_packets += 1 pkt = PacketFu::Packet.parse(p) rescue next # Just silently skip bad packets next unless pkt.is_ip? # Skip anything that's not IP. Technically, not Ethernet::Ip next if pkt.is_tcp? && (pkt.tcp_src == 0 || pkt.tcp_dst == 0) # Skip port 0 next if pkt.is_udp? && (pkt.udp_src == 0 || pkt.udp_dst == 0) # Skip port 0 saddr = pkt.ip_saddr daddr = pkt.ip_daddr # Handle blacklists and obviously useless IP addresses, and report the host. next if (bl | [saddr,daddr]).size == bl.size # Both hosts are blacklisted, skip everything. unless( bl.include?(saddr) || rfc3330_reserved(saddr)) yield(:address,saddr) if block and !seen_hosts.keys.include?(saddr) report_host(:workspace => wspace, :host => saddr, :state => Msf::HostState::Alive) unless seen_hosts[saddr] seen_hosts[saddr] ||= [] end unless( bl.include?(daddr) || rfc3330_reserved(daddr)) yield(:address,daddr) if block and !seen_hosts.keys.include?(daddr) report_host(:workspace => wspace, :host => daddr, :state => Msf::HostState::Alive) unless seen_hosts[daddr] seen_hosts[daddr] ||= [] end if pkt.is_tcp? # First pass on TCP packets if (pkt.tcp_flags.syn == 1 and pkt.tcp_flags.ack == 1) or # Oh, this kills me pkt.tcp_src < 1024 # If it's a low port, assume it's a proper service. if seen_hosts[saddr] unless seen_hosts[saddr].include? [pkt.tcp_src,"tcp"] report_service( :workspace => wspace, :host => saddr, :proto => "tcp", :port => pkt.tcp_src, :state => Msf::ServiceState::Open ) seen_hosts[saddr] << [pkt.tcp_src,"tcp"] yield(:service,"%s:%d/%s" % [saddr,pkt.tcp_src,"tcp"]) end end end elsif pkt.is_udp? # First pass on UDP packets if pkt.udp_src == pkt.udp_dst # Very basic p2p detection. [saddr,daddr].each do |xaddr| if seen_hosts[xaddr] unless seen_hosts[xaddr].include? [pkt.udp_src,"udp"] report_service( :workspace => wspace, :host => xaddr, :proto => "udp", :port => pkt.udp_src, :state => Msf::ServiceState::Open ) seen_hosts[xaddr] << [pkt.udp_src,"udp"] yield(:service,"%s:%d/%s" % [xaddr,pkt.udp_src,"udp"]) end end end elsif pkt.udp_src < 1024 # Probably a service if seen_hosts[saddr] unless seen_hosts[saddr].include? [pkt.udp_src,"udp"] report_service( :workspace => wspace, :host => saddr, :proto => "udp", :port => pkt.udp_src, :state => Msf::ServiceState::Open ) seen_hosts[saddr] << [pkt.udp_src,"udp"] yield(:service,"%s:%d/%s" % [saddr,pkt.udp_src,"udp"]) end end end end # tcp or udp inspect_single_packet(pkt,wspace) end # data.body.map # Right about here, we should have built up some streams for some stream analysis. # Not sure what form that will take, but people like shoving many hundreds of # thousands of packets through this thing, so it'll need to be memory efficient. end # Do all the single packet analysis we can while churning through the pcap # the first time. Multiple packet inspection will come later, where we can # do stream analysis, compare requests and responses, etc. def inspect_single_packet(pkt,wspace) if pkt.is_tcp? or pkt.is_udp? inspect_single_packet_http(pkt,wspace) end end # Checks for packets that are headed towards port 80, are tcp, contain an HTTP/1.0 # line, contains an Authorization line, contains a b64-encoded credential, and # extracts it. Reports this credential and solidifies the service as HTTP. def inspect_single_packet_http(pkt,wspace) # First, check the server side (data from port 80). if pkt.is_tcp? and pkt.tcp_src == 80 and !pkt.payload.nil? and !pkt.payload.empty? if pkt.payload =~ /^HTTP\x2f1\x2e[01]/ http_server_match = pkt.payload.match(/\nServer:\s+([^\r\n]+)[\r\n]/) if http_server_match.kind_of?(MatchData) and http_server_match[1] report_service( :workspace => wspace, :host => pkt.ip_saddr, :port => pkt.tcp_src, :proto => "tcp", :name => "http", :info => http_server_match[1], :state => Msf::ServiceState::Open ) # That's all we want to know from this service. return :something_significant end end end # Next, check the client side (data to port 80) if pkt.is_tcp? and pkt.tcp_dst == 80 and !pkt.payload.nil? and !pkt.payload.empty? if pkt.payload.match(/[\x00-\x20]HTTP\x2f1\x2e[10]/) auth_match = pkt.payload.match(/\nAuthorization:\s+Basic\s+([A-Za-z0-9=\x2b]+)/) if auth_match.kind_of?(MatchData) and auth_match[1] b64_cred = auth_match[1] else return false end # If we're this far, we can surmise that at least the client is a web browser, # he thinks the server is HTTP and he just made an authentication attempt. At # this point, we'll just believe everything the packet says -- validation ought # to come later. user,pass = b64_cred.unpack("m*").first.split(/:/,2) report_service( :workspace => wspace, :host => pkt.ip_daddr, :port => pkt.tcp_dst, :proto => "tcp", :name => "http" ) report_auth_info( :workspace => wspace, :host => pkt.ip_daddr, :port => pkt.tcp_dst, :proto => "tcp", :type => "password", :active => true, # Once we can build a stream, determine if the auth was successful. For now, assume it is. :user => user, :pass => pass ) # That's all we want to know from this service. return :something_significant end end end def import_spiceworks_csv(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] CSV.parse(data) do |row| next unless (["Name", "Manufacturer", "Device Type"] & row).empty? #header name = row[0] manufacturer = row[1] device = row[2] model = row[3] ip = row[4] serialno = row[5] location = row[6] os = row[7] next unless ip next if bl.include? ip conf = { :workspace => wspace, :host => ip, :name => name } conf[:os_name] = os if os info = [] info << "Serial Number: #{serialno}" unless (serialno.blank? or serialno == name) info << "Location: #{location}" unless location.blank? conf[:info] = info.join(", ") unless info.empty? host = report_host(conf) report_import_note(wspace, host) end end # # Metasploit PWDump Export # # This file format is generated by the db_export -f pwdump and # the Metasploit Express and Pro report types of "PWDump." # # This particular block scheme is temporary, since someone is # bound to want to import gigantic lists, so we'll want a # stream parser eventually (just like the other non-nmap formats). # # The file format is: # # 1.2.3.4:23/tcp (telnet) # username password # user2 p\x01a\x02ss2 # pass3 # user3 # smbuser:sid:lmhash:nthash::: # # Note the leading hash for the host:port line. Note also all usernames # and passwords must be in 7-bit ASCII (character sequences of "\x01" # will be interpolated -- this includes spaces, which must be notated # as "\x20". Blank usernames or passwords should be . # def import_msf_pwdump(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] last_host = nil addr = nil port = nil proto = nil sname = nil ptype = nil active = false # Are there cases where imported creds are good? I just hate trusting the import right away. data.each_line do |line| case line when /^[\s]*#/ # Comment lines if line[/^#[\s]*([0-9.]+):([0-9]+)(\x2f(tcp|udp))?[\s]*(\x28([^\x29]*)\x29)?/] addr = $1 port = $2 proto = $4 sname = $6 end when /^[\s]*Warning:/ next # Discard warning messages. when /^[\s]*([^\s:]+):[0-9]+:([A-Fa-f0-9]+:[A-Fa-f0-9]+):[^\s]*$/ # SMB Hash user = ([nil, ""].include?($1)) ? "" : $1 pass = ([nil, ""].include?($2)) ? "" : $2 ptype = "smb_hash" when /^[\s]*([^\s:]+):([0-9]+):NO PASSWORD\*+:NO PASSWORD\*+[^\s]*$/ # SMB Hash user = ([nil, ""].include?($1)) ? "" : $1 pass = "" ptype = "smb_hash" when /^[\s]*([\x21-\x7f]+)[\s]+([\x21-\x7f]+)?/ # Must be a user pass user = ([nil, ""].include?($1)) ? "" : dehex($1) pass = ([nil, ""].include?($2)) ? "" : dehex($2) ptype = "password" else # Some unknown line not broken by a space. next end next unless [addr,port,user,pass].compact.size == 4 next unless ipv46_validator(addr) # Skip Malformed addrs next unless port[/^[0-9]+$/] # Skip malformed ports if bl.include? addr next else yield(:address,addr) if block and addr != last_host last_host = addr end cred_info = { :host => addr, :port => port, :user => user, :pass => pass, :type => ptype, :workspace => wspace } cred_info[:proto] = proto if proto cred_info[:sname] = sname if sname cred_info[:active] = active report_auth_info(cred_info) user = pass = ptype = nil end end # If hex notation is present, turn them into a character. def dehex(str) hexen = str.scan(/\x5cx[0-9a-fA-F]{2}/) hexen.each { |h| str.gsub!(h,h[2,2].to_i(16).chr) } return str end # # Nexpose Simple XML # # XXX At some point we'll want to make this a stream parser for dealing # with large results files # def import_nexpose_simplexml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_nexpose_simplexml(args.merge(:data => data)) end # Import a Metasploit XML file. def import_msf_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_msf_xml(args.merge(:data => data)) end # Import a Metasploit Express ZIP file. Note that this requires # a fair bit of filesystem manipulation, and is very much tied # up with the Metasploit Express ZIP file format export (for # obvious reasons). In the event directories exist, they will # be reused. If target files exist, they will be overwritten. # # XXX: Refactor so it's not quite as sanity-blasting. def import_msf_zip(args={}, &block) data = args[:data] wpsace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] new_tmp = ::File.join(Dir::tmpdir,"msf","imp_#{Rex::Text::rand_text_alphanumeric(4)}",@import_filedata[:zip_basename]) if ::File.exists? new_tmp unless (::File.directory?(new_tmp) && ::File.writable?(new_tmp)) raise DBImportError.new("Could not extract zip file to #{new_tmp}") end else FileUtils.mkdir_p(new_tmp) end @import_filedata[:zip_tmp] = new_tmp # Grab the list of unique basedirs over all entries. @import_filedata[:zip_tmp_subdirs] = @import_filedata[:zip_entry_names].map {|x| ::File.split(x)}.map {|x| x[0]}.uniq.reject {|x| x == "."} # mkdir all of the base directores we just pulled out, if they don't # already exist @import_filedata[:zip_tmp_subdirs].each {|sub| tmp_subdirs = ::File.join(@import_filedata[:zip_tmp],sub) if File.exists? tmp_subdirs unless (::File.directory?(tmp_subdirs) && File.writable?(tmp_subdirs)) # if it exists but we can't write to it, give up raise DBImportError.new("Could not extract zip file to #{tmp_subdirs}") end else ::FileUtils.mkdir(tmp_subdirs) end } data.entries.each do |e| target = ::File.join(@import_filedata[:zip_tmp],e.name) ::File.unlink target if ::File.exists?(target) # Yep. Deleted. data.extract(e,target) if target =~ /^.*.xml$/ target_data = ::File.open(target, "rb") {|f| f.read 1024} if import_filetype_detect(target_data) == :msf_xml @import_filedata[:zip_extracted_xml] = target #break end end end # This will kick the newly-extracted XML file through # the import_file process all over again. if @import_filedata[:zip_extracted_xml] new_args = args.dup new_args[:filename] = @import_filedata[:zip_extracted_xml] new_args[:data] = nil new_args[:ifd] = @import_filedata.dup if block import_file(new_args, &block) else import_file(new_args) end end # Kick down to all the MSFX ZIP specific items if block import_msf_collateral(new_args, &block) else import_msf_collateral(new_args) end end # Imports loot, tasks, and reports from an MSF ZIP report. # XXX: This function is stupidly long. It needs to be refactored. def import_msf_collateral(args={}, &block) data = ::File.open(args[:filename], "rb") {|f| f.read(f.stat.size)} wspace = args[:wspace] || args['wspace'] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] basedir = args[:basedir] || args['basedir'] || ::File.join(Msf::Config.install_root, "data", "msf") allow_yaml = false btag = nil doc = rexmlify(data) if doc.elements["MetasploitExpressV1"] m_ver = 1 allow_yaml = true btag = "MetasploitExpressV1" elsif doc.elements["MetasploitExpressV2"] m_ver = 2 allow_yaml = true btag = "MetasploitExpressV2" elsif doc.elements["MetasploitExpressV3"] m_ver = 3 btag = "MetasploitExpressV3" elsif doc.elements["MetasploitExpressV4"] m_ver = 4 btag = "MetasploitExpressV4" elsif doc.elements["MetasploitV4"] m_ver = 4 btag = "MetasploitV4" else m_ver = nil end unless m_ver and btag raise DBImportError.new("Unsupported Metasploit XML document format") end host_info = {} doc.elements.each("/#{btag}/hosts/host") do |host| host_info[host.elements["id"].text.to_s.strip] = nils_for_nulls(host.elements["address"].text.to_s.strip) end # Import Loot doc.elements.each("/#{btag}/loots/loot") do |loot| next if bl.include? host_info[loot.elements["host-id"].text.to_s.strip] loot_info = {} loot_info[:host] = host_info[loot.elements["host-id"].text.to_s.strip] loot_info[:workspace] = args[:wspace] loot_info[:ctype] = nils_for_nulls(loot.elements["content-type"].text.to_s.strip) loot_info[:info] = nils_for_nulls(unserialize_object(loot.elements["info"], allow_yaml)) loot_info[:ltype] = nils_for_nulls(loot.elements["ltype"].text.to_s.strip) loot_info[:name] = nils_for_nulls(loot.elements["name"].text.to_s.strip) loot_info[:created_at] = nils_for_nulls(loot.elements["created-at"].text.to_s.strip) loot_info[:updated_at] = nils_for_nulls(loot.elements["updated-at"].text.to_s.strip) loot_info[:name] = nils_for_nulls(loot.elements["name"].text.to_s.strip) loot_info[:orig_path] = nils_for_nulls(loot.elements["path"].text.to_s.strip) tmp = args[:ifd][:zip_tmp] loot_info[:orig_path].gsub!(/^\./,tmp) if loot_info[:orig_path] if !loot.elements["service-id"].text.to_s.strip.empty? unless loot.elements["service-id"].text.to_s.strip == "NULL" loot_info[:service] = loot.elements["service-id"].text.to_s.strip end end # Only report loot if we actually have it. # TODO: Copypasta. Seperate this out. if ::File.exists? loot_info[:orig_path] loot_dir = ::File.join(basedir,"loot") loot_file = ::File.split(loot_info[:orig_path]).last if ::File.exists? loot_dir unless (::File.directory?(loot_dir) && ::File.writable?(loot_dir)) raise DBImportError.new("Could not move files to #{loot_dir}") end else ::FileUtils.mkdir_p(loot_dir) end new_loot = ::File.join(loot_dir,loot_file) loot_info[:path] = new_loot if ::File.exists?(new_loot) ::File.unlink new_loot # Delete it, and don't report it. else report_loot(loot_info) # It's new, so report it. end ::FileUtils.copy(loot_info[:orig_path], new_loot) yield(:msf_loot, new_loot) if block end end # Import Tasks doc.elements.each("/#{btag}/tasks/task") do |task| task_info = {} task_info[:workspace] = args[:wspace] # Should user be imported (original) or declared (the importing user)? task_info[:user] = nils_for_nulls(task.elements["created-by"].text.to_s.strip) task_info[:desc] = nils_for_nulls(task.elements["description"].text.to_s.strip) task_info[:info] = nils_for_nulls(unserialize_object(task.elements["info"], allow_yaml)) task_info[:mod] = nils_for_nulls(task.elements["module"].text.to_s.strip) task_info[:options] = nils_for_nulls(task.elements["options"].text.to_s.strip) task_info[:prog] = nils_for_nulls(task.elements["progress"].text.to_s.strip).to_i task_info[:created_at] = nils_for_nulls(task.elements["created-at"].text.to_s.strip) task_info[:updated_at] = nils_for_nulls(task.elements["updated-at"].text.to_s.strip) if !task.elements["completed-at"].text.to_s.empty? task_info[:completed_at] = nils_for_nulls(task.elements["completed-at"].text.to_s.strip) end if !task.elements["error"].text.to_s.empty? task_info[:error] = nils_for_nulls(task.elements["error"].text.to_s.strip) end if !task.elements["result"].text.to_s.empty? task_info[:result] = nils_for_nulls(task.elements["result"].text.to_s.strip) end task_info[:orig_path] = nils_for_nulls(task.elements["path"].text.to_s.strip) tmp = args[:ifd][:zip_tmp] task_info[:orig_path].gsub!(/^\./,tmp) if task_info[:orig_path] # Only report a task if we actually have it. # TODO: Copypasta. Seperate this out. if ::File.exists? task_info[:orig_path] tasks_dir = ::File.join(basedir,"tasks") task_file = ::File.split(task_info[:orig_path]).last if ::File.exists? tasks_dir unless (::File.directory?(tasks_dir) && ::File.writable?(tasks_dir)) raise DBImportError.new("Could not move files to #{tasks_dir}") end else ::FileUtils.mkdir_p(tasks_dir) end new_task = ::File.join(tasks_dir,task_file) task_info[:path] = new_task if ::File.exists?(new_task) ::File.unlink new_task # Delete it, and don't report it. else report_task(task_info) # It's new, so report it. end ::FileUtils.copy(task_info[:orig_path], new_task) yield(:msf_task, new_task) if block end end # Import Reports doc.elements.each("/#{btag}/reports/report") do |report| report_info = {} report_info[:workspace] = args[:wspace] # Should user be imported (original) or declared (the importing user)? report_info[:user] = nils_for_nulls(report.elements["created-by"].text.to_s.strip) report_info[:options] = nils_for_nulls(report.elements["options"].text.to_s.strip) report_info[:rtype] = nils_for_nulls(report.elements["rtype"].text.to_s.strip) report_info[:created_at] = nils_for_nulls(report.elements["created-at"].text.to_s.strip) report_info[:updated_at] = nils_for_nulls(report.elements["updated-at"].text.to_s.strip) report_info[:orig_path] = nils_for_nulls(report.elements["path"].text.to_s.strip) tmp = args[:ifd][:zip_tmp] report_info[:orig_path].gsub!(/^\./,tmp) if report_info[:orig_path] # Only report a report if we actually have it. # TODO: Copypasta. Seperate this out. if ::File.exists? report_info[:orig_path] reports_dir = ::File.join(basedir,"reports") report_file = ::File.split(report_info[:orig_path]).last if ::File.exists? reports_dir unless (::File.directory?(reports_dir) && ::File.writable?(reports_dir)) raise DBImportError.new("Could not move files to #{reports_dir}") end else ::FileUtils.mkdir_p(reports_dir) end new_report = ::File.join(reports_dir,report_file) report_info[:path] = new_report if ::File.exists?(new_report) ::File.unlink new_report else report_report(report_info) end ::FileUtils.copy(report_info[:orig_path], new_report) yield(:msf_report, new_report) if block end end end # For each host, step through services, notes, and vulns, and import # them. # TODO: loot, tasks, and reports def import_msf_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] allow_yaml = false btag = nil doc = rexmlify(data) if doc.elements["MetasploitExpressV1"] m_ver = 1 allow_yaml = true btag = "MetasploitExpressV1" elsif doc.elements["MetasploitExpressV2"] m_ver = 2 allow_yaml = true btag = "MetasploitExpressV2" elsif doc.elements["MetasploitExpressV3"] m_ver = 3 btag = "MetasploitExpressV3" elsif doc.elements["MetasploitExpressV4"] m_ver = 4 btag = "MetasploitExpressV4" elsif doc.elements["MetasploitV4"] m_ver = 4 btag = "MetasploitV4" else m_ver = nil end unless m_ver and btag raise DBImportError.new("Unsupported Metasploit XML document format") end doc.elements.each("/#{btag}/hosts/host") do |host| host_data = {} host_data[:workspace] = wspace host_data[:host] = nils_for_nulls(host.elements["address"].text.to_s.strip) if bl.include? host_data[:host] next else yield(:address,host_data[:host]) if block end host_data[:mac] = nils_for_nulls(host.elements["mac"].text.to_s.strip) if host.elements["comm"].text host_data[:comm] = nils_for_nulls(host.elements["comm"].text.to_s.strip) end %W{created-at updated-at name state os-flavor os-lang os-name os-sp purpose}.each { |datum| if host.elements[datum].text host_data[datum.gsub('-','_')] = nils_for_nulls(host.elements[datum].text.to_s.strip) end } host_address = host_data[:host].dup # Preserve after report_host() deletes hobj = report_host(host_data) host.elements.each("host_details/host_detail") do |hdet| hdet_data = {} hdet.elements.each do |det| next if ["id", "host-id"].include?(det.name) if det.text hdet_data[det.name.gsub('-','_')] = nils_for_nulls(det.text.to_s.strip) end end report_host_details(hobj, hdet_data) end host.elements.each("exploit_attempts/exploit_attempt") do |hdet| hdet_data = {} hdet.elements.each do |det| next if ["id", "host-id", "session-id", "vuln-id", "service-id", "loot-id"].include?(det.name) if det.text hdet_data[det.name.gsub('-','_')] = nils_for_nulls(det.text.to_s.strip) end end report_exploit_attempt(hobj, hdet_data) end host.elements.each('services/service') do |service| service_data = {} service_data[:workspace] = wspace service_data[:host] = hobj service_data[:port] = nils_for_nulls(service.elements["port"].text.to_s.strip).to_i service_data[:proto] = nils_for_nulls(service.elements["proto"].text.to_s.strip) %W{created-at updated-at name state info}.each { |datum| if service.elements[datum].text if datum == "info" service_data["info"] = nils_for_nulls(unserialize_object(service.elements[datum], false)) else service_data[datum.gsub("-","_")] = nils_for_nulls(service.elements[datum].text.to_s.strip) end end } report_service(service_data) end host.elements.each('notes/note') do |note| note_data = {} note_data[:workspace] = wspace note_data[:host] = hobj note_data[:type] = nils_for_nulls(note.elements["ntype"].text.to_s.strip) note_data[:data] = nils_for_nulls(unserialize_object(note.elements["data"], allow_yaml)) if note.elements["critical"].text note_data[:critical] = true unless note.elements["critical"].text.to_s.strip == "NULL" end if note.elements["seen"].text note_data[:seen] = true unless note.elements["critical"].text.to_s.strip == "NULL" end %W{created-at updated-at}.each { |datum| if note.elements[datum].text note_data[datum.gsub("-","_")] = nils_for_nulls(note.elements[datum].text.to_s.strip) end } report_note(note_data) end host.elements.each('tags/tag') do |tag| tag_data = {} tag_data[:addr] = host_address tag_data[:wspace] = wspace tag_data[:name] = tag.elements["name"].text.to_s.strip tag_data[:desc] = tag.elements["desc"].text.to_s.strip if tag.elements["report-summary"].text tag_data[:summary] = tag.elements["report-summary"].text.to_s.strip end if tag.elements["report-detail"].text tag_data[:detail] = tag.elements["report-detail"].text.to_s.strip end if tag.elements["critical"].text tag_data[:crit] = true unless tag.elements["critical"].text.to_s.strip == "NULL" end report_host_tag(tag_data) end host.elements.each('vulns/vuln') do |vuln| vuln_data = {} vuln_data[:workspace] = wspace vuln_data[:host] = hobj vuln_data[:info] = nils_for_nulls(unserialize_object(vuln.elements["info"], allow_yaml)) vuln_data[:name] = nils_for_nulls(vuln.elements["name"].text.to_s.strip) %W{created-at updated-at exploited-at}.each { |datum| if vuln.elements[datum] and vuln.elements[datum].text vuln_data[datum.gsub("-","_")] = nils_for_nulls(vuln.elements[datum].text.to_s.strip) end } if vuln.elements["refs"] vuln_data[:refs] = [] vuln.elements.each("refs/ref") do |ref| vuln_data[:refs] << nils_for_nulls(ref.text.to_s.strip) end end vobj = report_vuln(vuln_data) vuln.elements.each("vuln_details/vuln_detail") do |vdet| vdet_data = {} vdet.elements.each do |det| next if ["id", "vuln-id"].include?(det.name) if det.text vdet_data[det.name.gsub('-','_')] = nils_for_nulls(det.text.to_s.strip) end end report_vuln_details(vobj, vdet_data) end vuln.elements.each("vuln_attempts/vuln_attempt") do |vdet| vdet_data = {} vdet.elements.each do |det| next if ["id", "vuln-id", "loot-id", "session-id"].include?(det.name) if det.text vdet_data[det.name.gsub('-','_')] = nils_for_nulls(det.text.to_s.strip) end end report_vuln_attempt(vobj, vdet_data) end end host.elements.each('creds/cred') do |cred| cred_data = {} cred_data[:workspace] = wspace cred_data[:host] = hobj %W{port ptype sname proto proof active user pass}.each {|datum| if cred.elements[datum].respond_to? :text cred_data[datum.intern] = nils_for_nulls(cred.elements[datum].text.to_s.strip) end } %W{created-at updated-at}.each { |datum| if cred.elements[datum].respond_to? :text cred_data[datum.gsub("-","_")] = nils_for_nulls(cred.elements[datum].text.to_s.strip) end } %W{source-type source-id}.each { |datum| if cred.elements[datum].respond_to? :text cred_data[datum.gsub("-","_").intern] = nils_for_nulls(cred.elements[datum].text.to_s.strip) end } if cred_data[:pass] == "" cred_data[:pass] = "" cred_data[:active] = false elsif cred_data[:pass] == "*BLANK PASSWORD*" cred_data[:pass] = "" end report_cred(cred_data) 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] = hobj %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)) if sess.elements["routes"] sess_data[:routes] = nils_for_nulls(unserialize_object(sess.elements["routes"], allow_yaml)) || [] end if not sess_data[:closed_at] # Fake a close if we don't already have one sess_data[:closed_at] = Time.now.utc sess_data[:close_reason] = "Imported at #{Time.now.utc}" 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) 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 = {} info[:workspace] = wspace info[:host] = nils_for_nulls(web.elements["host"].text.to_s.strip) if web.elements["host"].respond_to?(:text) 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| if web.elements[datum].respond_to? :text info[datum.intern] = nils_for_nulls(web.elements[datum].text.to_s.strip) end end info[:headers] = nils_for_nulls(unserialize_object(web.elements["headers"], allow_yaml)) when "form" %W{path query method}.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[:params] = nils_for_nulls(unserialize_object(web.elements["params"], allow_yaml)) when "vuln" %W{path query method pname proof risk name blame description category confidence}.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[:params] = nils_for_nulls(unserialize_object(web.elements["params"], allow_yaml)) 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 end # Convert the string "NULL" to actual nil def nils_for_nulls(str) str == "NULL" ? nil : str end def import_nexpose_simplexml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_nexpose_noko_stream(noko_args) {|type, data| yield type,data} else import_nexpose_noko_stream(noko_args) end return true end data = args[:data] doc = rexmlify(data) doc.elements.each('/NeXposeSimpleXML/devices/device') do |dev| addr = dev.attributes['address'].to_s if bl.include? addr next else yield(:address,addr) if block end fprint = {} dev.elements.each('fingerprint/description') do |str| fprint[:desc] = str.text.to_s.strip end dev.elements.each('fingerprint/vendor') do |str| fprint[:vendor] = str.text.to_s.strip end dev.elements.each('fingerprint/family') do |str| fprint[:family] = str.text.to_s.strip end dev.elements.each('fingerprint/product') do |str| fprint[:product] = str.text.to_s.strip end dev.elements.each('fingerprint/version') do |str| fprint[:version] = str.text.to_s.strip end dev.elements.each('fingerprint/architecture') do |str| fprint[:arch] = str.text.to_s.upcase.strip end conf = { :workspace => wspace, :host => addr, :state => Msf::HostState::Alive } host = report_host(conf) report_import_note(wspace, host) report_note( :workspace => wspace, :host => host, :type => 'host.os.nexpose_fingerprint', :data => fprint ) # Load vulnerabilities not associated with a service dev.elements.each('vulnerabilities/vulnerability') do |vuln| vid = vuln.attributes['id'].to_s.downcase refs = process_nexpose_data_sxml_refs(vuln) next if not refs report_vuln( :workspace => wspace, :host => host, :name => 'NEXPOSE-' + vid, :info => vid, :refs => refs ) end # Load the services dev.elements.each('services/service') do |svc| sname = svc.attributes['name'].to_s sprot = svc.attributes['protocol'].to_s.downcase sport = svc.attributes['port'].to_s.to_i next if sport == 0 name = sname.split('(')[0].strip info = '' svc.elements.each('fingerprint/description') do |str| info = str.text.to_s.strip end if(sname.downcase != '') report_service(:workspace => wspace, :host => host, :proto => sprot, :port => sport, :name => name, :info => info) else report_service(:workspace => wspace, :host => host, :proto => sprot, :port => sport, :info => info) end # Load vulnerabilities associated with this service svc.elements.each('vulnerabilities/vulnerability') do |vuln| vid = vuln.attributes['id'].to_s.downcase refs = process_nexpose_data_sxml_refs(vuln) next if not refs report_vuln( :workspace => wspace, :host => host, :port => sport, :proto => sprot, :name => 'NEXPOSE-' + vid, :info => vid, :refs => refs ) end end end end # # Nexpose Raw XML # def import_nexpose_rawxml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_nexpose_rawxml(args.merge(:data => data)) end def import_nexpose_rawxml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_nexpose_raw_noko_stream(noko_args) {|type, data| yield type,data} else import_nexpose_raw_noko_stream(noko_args) end return true end data = args[:data] # Use a stream parser instead of a tree parser so we can deal with # huge results files without running out of memory. parser = Rex::Parser::NexposeXMLStreamParser.new # Since all the Refs have to be in the database before we can use them # in a Vuln, we store all the hosts until we finish parsing and only # then put everything in the database. This is memory-intensive for # large files, but should be much less so than a tree parser. # # This method is also considerably faster than parsing through the tree # looking for references every time we hit a vuln. hosts = [] vulns = [] # The callback merely populates our in-memory table of hosts and vulns parser.callback = Proc.new { |type, value| case type when :host # XXX: Blacklist should be checked here instead of saving a # host we're just going to throw away later hosts.push(value) when :vuln value["id"] = value["id"].downcase if value["id"] vulns.push(value) end } REXML::Document.parse_stream(data, parser) vuln_refs = nexpose_refs_to_struct(vulns) hosts.each do |host| if bl.include? host["addr"] next else yield(:address,host["addr"]) if block end nexpose_host_from_rawxml(host, vuln_refs, wspace) end end # # Takes an array of vuln hashes, as returned by the NeXpose rawxml stream # parser, like: # [ # {"id"=>"winreg-notes-protocol-handler", severity="8", "refs"=>[{"source"=>"BID", "value"=>"10600"}, ...]} # {"id"=>"windows-zotob-c", severity="8", "refs"=>[{"source"=>"BID", "value"=>"14513"}, ...]} # ] # and transforms it into a struct, containing :id, :refs, :title, and :severity # # Other attributes can be added later, as needed. def nexpose_refs_to_struct(vulns) ret = [] vulns.each do |vuln| next if ret.map {|v| v.id}.include? vuln["id"] vstruct = Struct.new(:id, :refs, :title, :severity).new vstruct.id = vuln["id"] vstruct.title = vuln["title"] vstruct.severity = vuln["severity"] vstruct.refs = [] vuln["refs"].each do |ref| if ref['source'] == 'BID' vstruct.refs.push('BID-' + ref["value"]) elsif ref['source'] == 'CVE' # value is CVE-$ID vstruct.refs.push(ref["value"]) elsif ref['source'] == 'MS' vstruct.refs.push('MSB-' + ref["value"]) elsif ref['source'] == 'URL' vstruct.refs.push('URL-' + ref["value"]) end end ret.push vstruct end return ret end # Takes a Host object, an array of vuln structs (generated by nexpose_refs_to_struct()), # and a workspace, and reports the vulns on that host. def nexpose_host_from_rawxml(h, vstructs, wspace) hobj = nil data = {:workspace => wspace} if h["addr"] addr = h["addr"] else # Can't report it if it doesn't have an IP return end data[:host] = addr if (h["hardware-address"]) # Put colons between each octet of the MAC address data[:mac] = h["hardware-address"].gsub(':', '').scan(/../).join(':') end data[:state] = (h["status"] == "alive") ? Msf::HostState::Alive : Msf::HostState::Dead # Since we only have one name field per host in the database, just # take the first one. if (h["names"] and h["names"].first) data[:name] = h["names"].first end if (data[:state] != Msf::HostState::Dead) hobj = report_host(data) report_import_note(wspace, hobj) end if h["notes"] note = { :workspace => wspace, :host => (hobj || addr), :type => "host.vuln.nexpose_keys", :data => {}, :mode => :unique_data } h["notes"].each do |v,k| note[:data][v] ||= [] next if note[:data][v].include? k note[:data][v] << k end report_note(note) end if h["os_family"] note = { :workspace => wspace, :host => hobj || addr, :type => 'host.os.nexpose_fingerprint', :data => { :family => h["os_family"], :certainty => h["os_certainty"] } } note[:data][:vendor] = h["os_vendor"] if h["os_vendor"] note[:data][:product] = h["os_product"] if h["os_product"] note[:data][:version] = h["os_version"] if h["os_version"] note[:data][:arch] = h["arch"] if h["arch"] report_note(note) end h["endpoints"].each { |p| extra = "" extra << p["product"] + " " if p["product"] extra << p["version"] + " " if p["version"] # Skip port-0 endpoints next if p["port"].to_i == 0 # XXX This should probably be handled in a more standard way # extra << "(" + p["certainty"] + " certainty) " if p["certainty"] data = {} data[:workspace] = wspace data[:proto] = p["protocol"].downcase data[:port] = p["port"].to_i data[:state] = p["status"] data[:host] = hobj || addr data[:info] = extra if not extra.empty? if p["name"] != "" data[:name] = p["name"] end report_service(data) } h["vulns"].each_pair { |k,v| next if v["status"] !~ /^vulnerable/ vstruct = vstructs.select {|vs| vs.id.to_s.downcase == v["id"].to_s.downcase}.first next unless vstruct data = {} data[:workspace] = wspace data[:host] = hobj || addr data[:proto] = v["protocol"].downcase if v["protocol"] data[:port] = v["port"].to_i if v["port"] data[:name] = "NEXPOSE-" + v["id"] data[:info] = vstruct.title data[:refs] = vstruct.refs report_vuln(data) } end # # Retina XML # # Process a Retina XML file def import_retina_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_retina_xml(args.merge(:data => data)) end # Process Retina XML def import_retina_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] msg = "Warning: The Retina XML format does not associate vulnerabilities with the\n" msg << "specific service on which they were found.\n" msg << "This makes it impossible to correlate exploits to discovered vulnerabilities\n" msg << "in a reliable fashion." yield(:warning,msg) if block parser = Rex::Parser::RetinaXMLStreamParser.new parser.on_found_host = Proc.new do |host| hobj = nil data = {:workspace => wspace} addr = host['address'] next if not addr next if bl.include? addr data[:host] = addr if host['mac'] data[:mac] = host['mac'] end data[:state] = Msf::HostState::Alive if host['hostname'] data[:name] = host['hostname'] end if host['netbios'] data[:name] = host['netbios'] end yield(:address, data[:host]) if block # Import Host hobj = report_host(data) report_import_note(wspace, hobj) # Import OS fingerprint if host["os"] note = { :workspace => wspace, :host => addr, :type => 'host.os.retina_fingerprint', :data => { :os => host["os"] } } report_note(note) end # Import vulnerabilities host['vulns'].each do |vuln| refs = vuln['refs'].map{|v| v.join("-")} refs << "RETINA-#{vuln['rthid']}" if vuln['rthid'] vuln_info = { :workspace => wspace, :host => addr, :name => vuln['name'], :info => vuln['description'], :refs => refs } report_vuln(vuln_info) end end REXML::Document.parse_stream(data, parser) end # # NetSparker XML # # Process a NetSparker XML file def import_netsparker_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_netsparker_xml(args.merge(:data => data)) end # Process NetSparker XML def import_netsparker_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] addr = nil parser = Rex::Parser::NetSparkerXMLStreamParser.new parser.on_found_vuln = Proc.new do |vuln| data = {:workspace => wspace} # Parse the URL url = vuln['url'] return if not url # Crack the URL into a URI uri = URI(url) rescue nil return if not uri # Resolve the host and cache the IP if not addr baddr = Rex::Socket.addr_aton(uri.host) rescue nil if baddr addr = Rex::Socket.addr_ntoa(baddr) yield(:address, addr) if block end end # Bail early if we have no IP address if not addr raise Interrupt, "Not a valid IP address" end if bl.include?(addr) raise Interrupt, "IP address is on the blacklist" end data[:host] = addr data[:vhost] = uri.host data[:port] = uri.port data[:ssl] = (uri.scheme == "ssl") body = nil # First report a web page if vuln['response'] headers = {} code = 200 head,body = vuln['response'].to_s.split(/\r?\n\r?\n/, 2) if body if head =~ /^HTTP\d+\.\d+\s+(\d+)\s*/ code = $1.to_i end headers = {} head.split(/\r?\n/).each do |line| hname,hval = line.strip.split(/\s*:\s*/, 2) next if hval.to_s.strip.empty? headers[hname.downcase] ||= [] headers[hname.downcase] << hval end info = { :path => uri.path, :query => uri.query, :code => code, :body => body, :headers => headers } info.merge!(data) if headers['content-type'] info[:ctype] = headers['content-type'][0] end if headers['set-cookie'] info[:cookie] = headers['set-cookie'].join("\n") end if headers['authorization'] info[:auth] = headers['authorization'].join("\n") end if headers['location'] info[:location] = headers['location'][0] end if headers['last-modified'] info[:mtime] = headers['last-modified'][0] end # Report the web page to the database report_web_page(info) yield(:web_page, url) if block end end # End web_page reporting details = netsparker_vulnerability_map(vuln) method = netsparker_method_map(vuln) pname = netsparker_pname_map(vuln) params = netsparker_params_map(vuln) proof = '' if vuln['info'] and vuln['info'].length > 0 proof << vuln['info'].map{|x| "#{x[0]}: #{x[1]}\n" }.join + "\n" end if proof.empty? if body proof << body + "\n" else proof << vuln['response'].to_s + "\n" end end if params.empty? and pname params = [[pname, vuln['vparam_name'].to_s]] end info = { # XXX: There is a :request attr in the model, but report_web_vuln # doesn't seem to know about it, so this gets ignored. #:request => vuln['request'], :path => uri.path, :query => uri.query, :method => method, :params => params, :pname => pname.to_s, :proof => proof, :risk => details[:risk], :name => details[:name], :blame => details[:blame], :category => details[:category], :description => details[:description], :confidence => details[:confidence], } info.merge!(data) next if vuln['type'].to_s.empty? report_web_vuln(info) yield(:web_vuln, url) if block end # We throw interrupts in our parser when the job is hopeless begin REXML::Document.parse_stream(data, parser) rescue ::Interrupt => e wlog("The netsparker_xml_import() job was interrupted: #{e}") end end def netsparker_method_map(vuln) case vuln['vparam_type'] when "FullQueryString" "GET" when "Querystring" "GET" when "Post" "POST" when "RawUrlInjection" "GET" else "GET" end end def netsparker_pname_map(vuln) case vuln['vparam_name'] when "URI-BASED", "Query Based" "PATH" else vuln['vparam_name'] end end def netsparker_params_map(vuln) [] end def netsparker_vulnerability_map(vuln) res = { :risk => 1, :name => 'Information Disclosure', :blame => 'System Administrator', :category => 'info', :description => "This is an information leak", :confidence => 100 } # Risk is a value from 1-5 indicating the severity of the issue # Examples: 1, 4, 5 # Name is a descriptive name for this vulnerability. # Examples: XSS, ReflectiveXSS, PersistentXSS # Blame indicates who is at fault for the vulnerability # Examples: App Developer, Server Developer, System Administrator # Category indicates the general class of vulnerability # Examples: info, xss, sql, rfi, lfi, cmd # Description is a textual summary of the vulnerability # Examples: "A reflective cross-site scripting attack" # "The web server leaks the internal IP address" # "The cookie is not set to HTTP-only" # # Confidence is a value from 1 to 100 indicating how confident the # software is that the results are valid. # Examples: 100, 90, 75, 15, 10, 0 case vuln['type'].to_s when "ApacheDirectoryListing" res = { :risk => 1, :name => 'Directory Listing', :blame => 'System Administrator', :category => 'info', :description => "", :confidence => 100 } when "ApacheMultiViewsEnabled" res = { :risk => 1, :name => 'Apache MultiViews Enabled', :blame => 'System Administrator', :category => 'info', :description => "", :confidence => 100 } when "ApacheVersion" res = { :risk => 1, :name => 'Web Server Version', :blame => 'System Administrator', :category => 'info', :description => "", :confidence => 100 } when "PHPVersion" res = { :risk => 1, :name => 'PHP Module Version', :blame => 'System Administrator', :category => 'info', :description => "", :confidence => 100 } when "AutoCompleteEnabled" res = { :risk => 1, :name => 'Form AutoComplete Enabled', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "CookieNotMarkedAsHttpOnly" res = { :risk => 1, :name => 'Cookie Not HttpOnly', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "EmailDisclosure" res = { :risk => 1, :name => 'Email Address Disclosure', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "ForbiddenResource" res = { :risk => 1, :name => 'Forbidden Resource', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "FileUploadFound" res = { :risk => 1, :name => 'File Upload Form', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "PasswordOverHTTP" res = { :risk => 2, :name => 'Password Over HTTP', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "MySQL5Identified" res = { :risk => 1, :name => 'MySQL 5 Identified', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "PossibleInternalWindowsPathLeakage" res = { :risk => 1, :name => 'Path Leakage - Windows', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "PossibleInternalUnixPathLeakage" res = { :risk => 1, :name => 'Path Leakage - Unix', :blame => 'App Developer', :category => 'info', :description => "", :confidence => 100 } when "PossibleXSS", "LowPossibilityPermanentXSS", "XSS", "PermanentXSS" conf = 100 conf = 25 if vuln['type'].to_s == "LowPossibilityPermanentXSS" conf = 50 if vuln['type'].to_s == "PossibleXSS" res = { :risk => 3, :name => 'Cross-Site Scripting', :blame => 'App Developer', :category => 'xss', :description => "", :confidence => conf } when "ConfirmedBlindSQLInjection", "ConfirmedSQLInjection", "HighlyPossibleSqlInjection", "DatabaseErrorMessages" conf = 100 conf = 90 if vuln['type'].to_s == "HighlyPossibleSqlInjection" conf = 25 if vuln['type'].to_s == "DatabaseErrorMessages" res = { :risk => 5, :name => 'SQL Injection', :blame => 'App Developer', :category => 'sql', :description => "", :confidence => conf } else conf = 100 res = { :risk => 1, :name => vuln['type'].to_s, :blame => 'App Developer', :category => 'info', :description => "", :confidence => conf } end res end def import_fusionvm_xml(args={}) args[:wspace] ||= workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] doc = Rex::Parser::FusionVMDocument.new(args,self) parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end # # Import Nmap's -oX xml output # def import_nmap_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_nmap_xml(args.merge(:data => data)) end def import_nexpose_raw_noko_stream(args, &block) if block doc = Rex::Parser::NexposeRawDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::NexposeRawDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_nexpose_noko_stream(args, &block) if block doc = Rex::Parser::NexposeSimpleDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::NexposeSimpleDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_nmap_noko_stream(args, &block) if block doc = Rex::Parser::NmapDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::NmapDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end # If you have Nokogiri installed, you'll be shunted over to # that. Otherwise, you'll hit the old NmapXMLStreamParser. def import_nmap_xml(args={}, &block) return nil if args[:data].nil? or args[:data].empty? wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] if Rex::Parser.nokogiri_loaded noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, "Nokogiri v#{::Nokogiri::VERSION}") import_nmap_noko_stream(noko_args) {|type, data| yield type,data } else import_nmap_noko_stream(noko_args) end return true end # XXX: Legacy nmap xml parser starts here. fix_services = args[:fix_services] data = args[:data] # Use a stream parser instead of a tree parser so we can deal with # huge results files without running out of memory. parser = Rex::Parser::NmapXMLStreamParser.new yield(:parser, parser.class.name) if block # Whenever the parser pulls a host out of the nmap results, store # it, along with any associated services, in the database. parser.on_found_host = Proc.new { |h| hobj = nil data = {:workspace => wspace} if (h["addrs"].has_key?("ipv4")) addr = h["addrs"]["ipv4"] elsif (h["addrs"].has_key?("ipv6")) addr = h["addrs"]["ipv6"] else # Can't report it if it doesn't have an IP raise RuntimeError, "At least one IPv4 or IPv6 address is required" end next if bl.include? addr data[:host] = addr if (h["addrs"].has_key?("mac")) data[:mac] = h["addrs"]["mac"] end data[:state] = (h["status"] == "up") ? Msf::HostState::Alive : Msf::HostState::Dead if ( h["reverse_dns"] ) data[:name] = h["reverse_dns"] end # Only report alive hosts with ports to speak of. if(data[:state] != Msf::HostState::Dead) if h["ports"].size > 0 if fix_services port_states = h["ports"].map {|p| p["state"]}.reject {|p| p == "filtered"} next if port_states.compact.empty? end yield(:address,data[:host]) if block hobj = report_host(data) report_import_note(wspace,hobj) end end if( h["os_vendor"] ) note = { :workspace => wspace, :host => hobj || addr, :type => 'host.os.nmap_fingerprint', :data => { :os_vendor => h["os_vendor"], :os_family => h["os_family"], :os_version => h["os_version"], :os_accuracy => h["os_accuracy"] } } if(h["os_match"]) note[:data][:os_match] = h['os_match'] end report_note(note) end if (h["last_boot"]) report_note( :workspace => wspace, :host => hobj || addr, :type => 'host.last_boot', :data => { :time => h["last_boot"] } ) end if (h["trace"]) hops = [] h["trace"]["hops"].each do |hop| hops << { "ttl" => hop["ttl"].to_i, "address" => hop["ipaddr"].to_s, "rtt" => hop["rtt"].to_f, "name" => hop["host"].to_s } end report_note( :workspace => wspace, :host => hobj || addr, :type => 'host.nmap.traceroute', :data => { 'port' => h["trace"]["port"].to_i, 'proto' => h["trace"]["proto"].to_s, 'hops' => hops } ) end # Put all the ports, regardless of state, into the db. h["ports"].each { |p| # Localhost port results are pretty unreliable -- if it's # unknown, it's no good (possibly Windows-only) if ( p["state"] == "unknown" && h["status_reason"] == "localhost-response" ) next end extra = "" extra << p["product"] + " " if p["product"] extra << p["version"] + " " if p["version"] extra << p["extrainfo"] + " " if p["extrainfo"] data = {} data[:workspace] = wspace if fix_services data[:proto] = nmap_msf_service_map(p["protocol"]) else data[:proto] = p["protocol"].downcase end data[:port] = p["portid"].to_i data[:state] = p["state"] data[:host] = hobj || addr data[:info] = extra if not extra.empty? if p["name"] != "unknown" data[:name] = p["name"] end report_service(data) } #Parse the scripts output if h["scripts"] h["scripts"].each do |key,val| if key == "smb-check-vulns" if val =~ /MS08-067: VULNERABLE/ vuln_info = { :workspace => wspace, :host => hobj || addr, :port => 445, :proto => 'tcp', :name => 'MS08-067', :info => 'Microsoft Windows Server Service Crafted RPC Request Handling Unspecified Remote Code Execution', :refs =>['CVE-2008-4250', 'BID-31874', 'OSVDB-49243', 'CWE-94', 'MSFT-MS08-067', 'MSF-Microsoft Server Service Relative Path Stack Corruption', 'NSS-34476'] } report_vuln(vuln_info) end if val =~ /MS06-025: VULNERABLE/ vuln_info = { :workspace => wspace, :host => hobj || addr, :port => 445, :proto => 'tcp', :name => 'MS06-025', :info => 'Vulnerability in Routing and Remote Access Could Allow Remote Code Execution', :refs =>['CVE-2006-2370', 'CVE-2006-2371', 'BID-18325', 'BID-18358', 'BID-18424', 'OSVDB-26436', 'OSVDB-26437', 'MSFT-MS06-025', 'MSF-Microsoft RRAS Service RASMAN Registry Overflow', 'NSS-21689'] } report_vuln(vuln_info) end # This one has NOT been Tested , remove this comment if confirmed working if val =~ /MS07-029: VULNERABLE/ vuln_info = { :workspace => wspace, :host => hobj || addr, :port => 445, :proto => 'tcp', :name => 'MS07-029', :info => 'Vulnerability in Windows DNS RPC Interface Could Allow Remote Code Execution', # Add more refs based on nessus/nexpose .. results :refs =>['CVE-2007-1748', 'OSVDB-34100', 'MSF-Microsoft DNS RPC Service extractQuotedChar()', 'NSS-25168'] } report_vuln(vuln_info) end end end end } # XXX: Legacy nmap xml parser ends here. REXML::Document.parse_stream(data, parser) end def nmap_msf_service_map(proto) service_name_map(proto) end # # This method normalizes an incoming service name to one of the # the standard ones recognized by metasploit # def service_name_map(proto) return proto unless proto.kind_of? String case proto.downcase when "msrpc", "nfs-or-iis", "dce endpoint resolution" "dcerpc" when "ms-sql-s", "tds" "mssql" when "ms-sql-m","microsoft sql monitor" "mssql-m" when "postgresql"; "postgres" when "http-proxy"; "http" when "iiimsf"; "db2" when "oracle-tns"; "oracle" when "quickbooksrds"; "metasploit" when "microsoft remote display protocol" "rdp" when "vmware authentication daemon" "vmauthd" when "netbios-ns", "cifs name service" "netbios" when "netbios-ssn", "microsoft-ds", "cifs" "smb" when "remote shell" "shell" when "remote login" "login" when "nfs lockd" "lockd" when "hp jetdirect" "jetdirect" when "dhcp server" "dhcp" when /^dns-(udp|tcp)$/; "dns" when /^dce[\s+]rpc$/; "dcerpc" else proto.downcase.gsub(/\s*\(.*/, '') # "service (some service)" end end def report_import_note(wspace,addr) if @import_filedata.kind_of?(Hash) && @import_filedata[:filename] && @import_filedata[:filename] !~ /msfe-nmap[0-9]{8}/ report_note( :workspace => wspace, :host => addr, :type => 'host.imported', :data => @import_filedata.merge(:time=> Time.now.utc) ) end end # # Import Nessus NBE files # def import_nessus_nbe_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_nessus_nbe(args.merge(:data => data)) end # There is no place the NBE actually stores the plugin name used to # scan. You get "Security Note" or "Security Warning," and that's it. def import_nessus_nbe(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] nbe_copy = data.dup # First pass, just to build the address map. addr_map = {} # Cache host objects before passing into handle_nessus() hobj_map = {} nbe_copy.each_line do |line| r = line.split('|') next if r[0] != 'results' next if r[4] != "12053" data = r[6] addr,hname = data.match(/([0-9\x2e]+) resolves as (.+)\x2e\\n/)[1,2] addr_map[hname] = addr end data.each_line do |line| r = line.split('|') next if r[0] != 'results' hname = r[2] if addr_map[hname] addr = addr_map[hname] else addr = hname # Must be unresolved, probably an IP address. end port = r[3] nasl = r[4] type = r[5] data = r[6] # If there's no resolution, or if it's malformed, skip it. next unless ipv46_validator(addr) if bl.include? addr next else yield(:address,addr) if block end hobj_map[ addr ] ||= report_host(:host => addr, :workspace => wspace) # Match the NBE types with the XML severity ratings case type # log messages don't actually have any data, they are just # complaints about not being able to perform this or that test # because such-and-such was missing when "Log Message"; next when "Security Hole"; severity = 3 when "Security Warning"; severity = 2 when "Security Note"; severity = 1 # a severity 0 means there's no extra data, it's just an open port else; severity = 0 end if nasl == "11936" os = data.match(/The remote host is running (.*)\\n/)[1] report_note( :workspace => wspace, :host => hobj_map[ addr ], :type => 'host.os.nessus_fingerprint', :data => { :os => os.to_s.strip } ) end next if nasl.to_s.strip.empty? plugin_name = nil # NBE doesn't ever populate this handle_nessus(wspace, hobj_map[ addr ], port, nasl, plugin_name, severity, data) end end # # Of course they had to change the nessus format. # def import_openvas_xml(args={}, &block) filename = args[:filename] wspace = args[:wspace] || workspace raise DBImportError.new("No OpenVAS XML support. Please submit a patch to msfdev[at]metasploit.com") end # # Import IP360 XML v3 output # def import_ip360_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_ip360_xml_v3(args.merge(:data => data)) end # # Import Nessus XML v1 and v2 output # # Old versions of openvas exported this as well # def import_nessus_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end if data.index("NessusClientData_v2") import_nessus_xml_v2(args.merge(:data => data)) else import_nessus_xml(args.merge(:data => data)) end end def import_nessus_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] doc = rexmlify(data) doc.elements.each('/NessusClientData/Report/ReportHost') do |host| hobj = nil addr = nil hname = nil os = nil # If the name is resolved, the Nessus plugin for DNS # resolution should be there. If not, fall back to the # HostName host.elements.each('ReportItem') do |item| next unless item.elements['pluginID'].text == "12053" addr = item.elements['data'].text.match(/([0-9\x2e]+) resolves as/)[1] hname = host.elements['HostName'].text end addr ||= host.elements['HostName'].text next unless ipv46_validator(addr) # Skip resolved names and SCAN-ERROR. if bl.include? addr next else yield(:address,addr) if block end hinfo = { :workspace => wspace, :host => addr } # Record the hostname hinfo.merge!(:name => hname.to_s.strip) if hname hobj = report_host(hinfo) report_import_note(wspace,hobj) # Record the OS os ||= host.elements["os_name"] if os report_note( :workspace => wspace, :host => hobj, :type => 'host.os.nessus_fingerprint', :data => { :os => os.text.to_s.strip } ) end host.elements.each('ReportItem') do |item| nasl = item.elements['pluginID'].text plugin_name = item.elements['pluginName'].text port = item.elements['port'].text data = item.elements['data'].text severity = item.elements['severity'].text handle_nessus(wspace, hobj, port, nasl, plugin_name, severity, data) end end end def import_nessus_xml_v2(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] #@host = { #'hname' => nil, #'addr' => nil, #'mac' => nil, #'os' => nil, #'ports' => [ 'port' => { 'port' => nil, # 'svc_name' => nil, # 'proto' => nil, # 'severity' => nil, # 'nasl' => nil, # 'description' => nil, # 'cve' => [], # 'bid' => [], # 'xref' => [] # } # ] #} parser = Rex::Parser::NessusXMLStreamParser.new parser.on_found_host = Proc.new { |host| hobj = nil addr = host['addr'] || host['hname'] next unless ipv46_validator(addr) # Catches SCAN-ERROR, among others. if bl.include? addr next else yield(:address,addr) if block end os = host['os'] hname = host['hname'] mac = host['mac'] host_info = { :workspace => wspace, :host => addr, } host_info[:name] = hname.to_s.strip if hname # Short mac, protect against Nessus's habit of saving multiple macs # We can't use them anyway, so take just the first. host_info[:mac] = mac.to_s.strip.upcase.split(/\s+/).first if mac hobj = report_host(host_info) report_import_note(wspace,hobj) os = host['os'] yield(:os,os) if block if os report_note( :workspace => wspace, :host => hobj, :type => 'host.os.nessus_fingerprint', :data => { :os => os.to_s.strip } ) end host['ports'].each do |item| next if item['port'] == 0 msf = nil nasl = item['nasl'].to_s nasl_name = item['nasl_name'].to_s port = item['port'].to_s proto = item['proto'] || "tcp" sname = item['svc_name'] severity = item['severity'] description = item['description'] cve = item['cve'] bid = item['bid'] xref = item['xref'] msf = item['msf'] yield(:port,port) if block handle_nessus_v2(wspace, hobj, port, proto, sname, nasl, nasl_name, severity, description, cve, bid, xref, msf) end yield(:end,hname) if block } REXML::Document.parse_stream(data, parser) end def import_mbsa_xml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_mbsa_noko_stream(noko_args) {|type, data| yield type,data} else import_mbsa_noko_stream(noko_args) end return true else # Sorry raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.") end end def import_mbsa_noko_stream(args={},&block) if block doc = Rex::Parser::MbsaDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::MbsaDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_foundstone_xml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_foundstone_noko_stream(noko_args) {|type, data| yield type,data} else import_foundstone_noko_stream(noko_args) end return true else # Sorry raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.") end end def import_foundstone_noko_stream(args={},&block) if block doc = Rex::Parser::FoundstoneDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::FoundstoneDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_acunetix_xml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_acunetix_noko_stream(noko_args) {|type, data| yield type,data} else import_acunetix_noko_stream(noko_args) end return true else # Sorry raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.") end end def import_ci_xml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_ci_noko_stream(noko_args) {|type, data| yield type,data} else import_ci_noko_stream(noko_args) end return true else # Sorry raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.") end end def import_acunetix_noko_stream(args={},&block) if block doc = Rex::Parser::AcunetixDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::AcunetixFoundstoneDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_appscan_xml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_appscan_noko_stream(noko_args) {|type, data| yield type,data} else import_appscan_noko_stream(noko_args) end return true else # Sorry raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.") end end def import_appscan_noko_stream(args={},&block) if block doc = Rex::Parser::AppscanDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::AppscanDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def import_burp_session_xml(args={}, &block) bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] wspace = args[:wspace] || workspace if Rex::Parser.nokogiri_loaded # Rex::Parser.reload("burp_session_nokogiri.rb") parser = "Nokogiri v#{::Nokogiri::VERSION}" noko_args = args.dup noko_args[:blacklist] = bl noko_args[:wspace] = wspace if block yield(:parser, parser) import_burp_session_noko_stream(noko_args) {|type, data| yield type,data} else import_burp_session_noko_stream(noko_args) end return true else # Sorry raise DBImportError.new("Could not import due to missing Nokogiri parser. Try 'gem install nokogiri'.") end end def import_burp_session_noko_stream(args={},&block) if block doc = Rex::Parser::BurpSessionDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::BurpSessionDocument.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end # # Import IP360's ASPL database # def import_ip360_aspl_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] if not data.index(" {'name' => { }, 'cve' => { }, 'bid' => { } } # 'oses' => {'name' } } aspl_path = nil aspl_paths = [ ::File.join(Msf::Config.config_directory, "data", "ncircle", "ip360.aspl"), ::File.join(Msf::Config.data_directory, "ncircle", "ip360.aspl") ] aspl_paths.each do |tpath| next if not (::File.exist?(tpath) and ::File.readable?(tpath)) aspl_path = tpath break end if not aspl_path raise DBImportError.new("The nCircle IP360 ASPL file is not present.\n Download ASPL from nCircle VNE | Administer | Support | Resources, unzip it, and import it first") end # parse nCircle ASPL file aspl = "" ::File.open(aspl_path, "rb") do |f| aspl = f.read(f.stat.size) end @asplhash = nil parser = Rex::Parser::IP360ASPLXMLStreamParser.new parser.on_found_aspl = Proc.new { |asplh| @asplhash = asplh } REXML::Document.parse_stream(aspl, parser) # nCircle has some quotes escaped which causes the parser to break # we don't need these lines so just replace \" with " data.gsub!(/\\"/,'"') # parse nCircle Scan Output parser = Rex::Parser::IP360XMLStreamParser.new parser.on_found_host = Proc.new { |host| hobj = nil addr = host['addr'] || host['hname'] next unless ipv46_validator(addr) # Catches SCAN-ERROR, among others. if bl.include? addr next else yield(:address,addr) if block end os = host['os'] hname = host['hname'] mac = host['mac'] host_hash = { :workspace => wspace, :host => addr, } host_hash[:name] = hname.to_s.strip if hname host_hash[:mac] = mac.to_s.strip.upcase if mac hobj = report_host(host_hash) yield(:os, os) if block if os report_note( :workspace => wspace, :host => hobj, :type => 'host.os.ip360_fingerprint', :data => { :os => @asplhash['oses'][os].to_s.strip } ) end host['apps'].each do |item| port = item['port'].to_s proto = item['proto'].to_s handle_ip360_v3_svc(wspace, hobj, port, proto, hname) end host['vulns'].each do |item| vulnid = item['vulnid'].to_s port = item['port'].to_s proto = item['proto'] || "tcp" vulnname = @asplhash['vulns']['name'][vulnid] cves = @asplhash['vulns']['cve'][vulnid] bids = @asplhash['vulns']['bid'][vulnid] yield(:port, port) if block handle_ip360_v3_vuln(wspace, hobj, port, proto, hname, vulnid, vulnname, cves, bids) end yield(:end, hname) if block } REXML::Document.parse_stream(data, parser) end def find_qualys_asset_vuln_refs(doc) vuln_refs = {} doc.elements.each("/ASSET_DATA_REPORT/GLOSSARY/VULN_DETAILS_LIST/VULN_DETAILS") do |vuln| next unless vuln.elements['QID'] && vuln.elements['QID'].first qid = vuln.elements['QID'].first.to_s vuln_refs[qid] ||= [] vuln.elements.each('CVE_ID_LIST/CVE_ID') do |ref| vuln_refs[qid].push('CVE-' + /C..-([0-9\-]{9})/.match(ref.elements['ID'].text.to_s)[1]) end vuln.elements.each('BUGTRAQ_ID_LIST/BUGTRAQ_ID') do |ref| vuln_refs[qid].push('BID-' + ref.elements['ID'].text.to_s) end end return vuln_refs end # Pull out vulnerabilities that have at least one matching # ref -- many "vulns" are not vulns, just audit information. def find_qualys_asset_vulns(host,wspace,hobj,vuln_refs,&block) host.elements.each("VULN_INFO_LIST/VULN_INFO") do |vi| next unless vi.elements["QID"] vi.elements.each("QID") do |qid| next if vuln_refs[qid.text].nil? || vuln_refs[qid.text].empty? handle_qualys(wspace, hobj, nil, nil, qid.text, nil, vuln_refs[qid.text], nil) end end end # Takes QID numbers and finds the discovered services in # a qualys_asset_xml. def find_qualys_asset_ports(i,host,wspace,hobj) return unless (i == 82023 || i == 82004) proto = i == 82023 ? 'tcp' : 'udp' qid = host.elements["VULN_INFO_LIST/VULN_INFO/QID[@id='qid_#{i}']"] qid_result = qid.parent.elements["RESULT[@format='table']"] if qid hports = qid_result.first.to_s if qid_result if hports hports.scan(/([0-9]+)\t(.*?)\t.*?\t([^\t\n]*)/) do |match| if match[2] == nil or match[2].strip == 'unknown' name = match[1].strip else name = match[2].strip end handle_qualys(wspace, hobj, match[0].to_s, proto, 0, nil, nil, name) end end end # # Import Qualys's Asset Data Report format # def import_qualys_asset_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] doc = rexmlify(data) vuln_refs = find_qualys_asset_vuln_refs(doc) # 2nd pass, actually grab the hosts. doc.elements.each("/ASSET_DATA_REPORT/HOST_LIST/HOST") do |host| hobj = nil addr = host.elements["IP"].text if host.elements["IP"] next unless validate_ips(addr) if bl.include? addr next else yield(:address,addr) if block end hname = ( # Prefer NetBIOS over DNS (host.elements["NETBIOS"].text if host.elements["NETBIOS"]) || (host.elements["DNS"].text if host.elements["DNS"]) || "" ) hobj = report_host(:workspace => wspace, :host => addr, :name => hname, :state => Msf::HostState::Alive) report_import_note(wspace,hobj) if host.elements["OPERATING_SYSTEM"] hos = host.elements["OPERATING_SYSTEM"].text report_note( :workspace => wspace, :host => hobj, :type => 'host.os.qualys_fingerprint', :data => { :os => hos } ) end # Report open ports. find_qualys_asset_ports(82023,host,wspace,hobj) # TCP find_qualys_asset_ports(82004,host,wspace,hobj) # UDP # Report vulns find_qualys_asset_vulns(host,wspace,hobj,vuln_refs,&block) end # host end # # Import Qualys' Scan xml output # def import_qualys_scan_xml_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_qualys_scan_xml(args.merge(:data => data)) end def import_qualys_scan_xml(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] doc = rexmlify(data) doc.elements.each('/SCAN/IP') do |host| hobj = nil addr = host.attributes['value'] if bl.include? addr next else yield(:address,addr) if block end hname = host.attributes['name'] || '' hobj = report_host(:workspace => wspace, :host => addr, :name => hname, :state => Msf::HostState::Alive) report_import_note(wspace,hobj) if host.elements["OS"] hos = host.elements["OS"].text report_note( :workspace => wspace, :host => hobj, :type => 'host.os.qualys_fingerprint', :data => { :os => hos } ) end # Open TCP Services List (Qualys ID 82023) services_tcp = host.elements["SERVICES/CAT/SERVICE[@number='82023']/RESULT"] if services_tcp services_tcp.text.scan(/([0-9]+)\t(.*?)\t.*?\t([^\t\n]*)/) do |match| if match[2] == nil or match[2].strip == 'unknown' name = match[1].strip else name = match[2].strip end handle_qualys(wspace, hobj, match[0].to_s, 'tcp', 0, nil, nil, name) end end # Open UDP Services List (Qualys ID 82004) services_udp = host.elements["SERVICES/CAT/SERVICE[@number='82004']/RESULT"] if services_udp services_udp.text.scan(/([0-9]+)\t(.*?)\t.*?\t([^\t\n]*)/) do |match| if match[2] == nil or match[2].strip == 'unknown' name = match[1].strip else name = match[2].strip end handle_qualys(wspace, hobj, match[0].to_s, 'udp', 0, nil, nil, name) end end # VULNS are confirmed, PRACTICES are unconfirmed vulnerabilities host.elements.each('VULNS/CAT | PRACTICES/CAT') do |cat| port = cat.attributes['port'] protocol = cat.attributes['protocol'] cat.elements.each('VULN | PRACTICE') do |vuln| refs = [] qid = vuln.attributes['number'] severity = vuln.attributes['severity'] title = vuln.elements['TITLE'].text.to_s vuln.elements.each('VENDOR_REFERENCE_LIST/VENDOR_REFERENCE') do |ref| refs.push(ref.elements['ID'].text.to_s) end vuln.elements.each('CVE_ID_LIST/CVE_ID') do |ref| refs.push('CVE-' + /C..-([0-9\-]{9})/.match(ref.elements['ID'].text.to_s)[1]) end vuln.elements.each('BUGTRAQ_ID_LIST/BUGTRAQ_ID') do |ref| refs.push('BID-' + ref.elements['ID'].text.to_s) end handle_qualys(wspace, hobj, port, protocol, qid, severity, refs, nil,title) end end end end def import_ip_list_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end import_ip_list(args.merge(:data => data)) end def import_ip_list(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] data.each_line do |ip| ip.strip! if bl.include? ip next else yield(:address,ip) if block end host = find_or_create_host(:workspace => wspace, :host=> ip, :state => Msf::HostState::Alive) end end def import_amap_log_file(args={}) filename = args[:filename] wspace = args[:wspace] || workspace data = "" ::File.open(filename, 'rb') do |f| data = f.read(f.stat.size) end case import_filetype_detect(data) when :amap_log import_amap_log(args.merge(:data => data)) when :amap_mlog import_amap_mlog(args.merge(:data => data)) else raise DBImportError.new("Could not determine file type") end end def import_amap_log(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] data.each_line do |line| next if line =~ /^#/ next if line !~ /^Protocol on ([^:]+):([^\x5c\x2f]+)[\x5c\x2f](tcp|udp) matches (.*)$/ addr = $1 next if bl.include? addr port = $2.to_i proto = $3.downcase name = $4 host = find_or_create_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive) next if not host yield(:address,addr) if block info = { :workspace => wspace, :host => host, :proto => proto, :port => port } if name != "unidentified" info[:name] = name end service = find_or_create_service(info) end end def import_amap_mlog(args={}, &block) data = args[:data] wspace = args[:wspace] || workspace bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : [] data.each_line do |line| next if line =~ /^#/ r = line.split(':') next if r.length < 6 addr = r[0] next if bl.include? addr port = r[1].to_i proto = r[2].downcase status = r[3] name = r[5] next if status != "open" host = find_or_create_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive) next if not host yield(:address,addr) if block info = { :workspace => wspace, :host => host, :proto => proto, :port => port } if name != "unidentified" info[:name] = name end service = find_or_create_service(info) end end def import_ci_noko_stream(args, &block) if block doc = Rex::Parser::CIDocument.new(args,framework.db) {|type, data| yield type,data } else doc = Rex::Parser::CI.new(args,self) end parser = ::Nokogiri::XML::SAX::Parser.new(doc) parser.parse(args[:data]) end def unserialize_object(xml_elem, allow_yaml = false) return nil unless xml_elem string = xml_elem.text.to_s.strip return string unless string.is_a?(String) return nil if (string.empty? || string.nil?) begin # Validate that it is properly formed base64 first if string.gsub(/\s+/, '') =~ /^([a-z0-9A-Z\+\/=]+)$/ Marshal.load($1.unpack("m")[0]) else if allow_yaml begin YAML.load(string) rescue dlog("Badly formatted YAML: '#{string}'") string end else string end end rescue ::Exception => e if allow_yaml YAML.load(string) rescue string else string end 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) return host if host.kind_of? ::Mdm::Host norm_host = nil if (host.kind_of? String) if Rex::Socket.is_ipv4?(host) # If it's an IPv4 addr with a port on the end, strip the port if host =~ /((\d{1,3}\.){3}\d{1,3}):\d+/ norm_host = $1 else norm_host = host end elsif Rex::Socket.is_ipv6?(host) # If it's an IPv6 addr, drop the scope address, scope = host.split('%', 2) norm_host = address else norm_host = Rex::Socket.getaddress(host, true) end elsif host.kind_of? ::Mdm::Session norm_host = host.host elsif host.respond_to?(:session_host) # Then it's an Msf::Session object thost = host.session_host norm_host = thost end # 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 # A way to sneak the yield back into the db importer. # Used by the SAX parsers. def emit(sym,data,&block) yield(sym,data) end protected # # This holds all of the shared parsing/handling used by the # Nessus NBE and NESSUS v1 methods # def handle_nessus(wspace, hobj, port, nasl, plugin_name, severity, data) addr = hobj.address # The port section looks like: # http (80/tcp) p = port.match(/^([^\(]+)\((\d+)\/([^\)]+)\)/) return if not p # Unnecessary as the caller should already have reported this host #report_host(:workspace => wspace, :host => addr, :state => Msf::HostState::Alive) name = p[1].strip port = p[2].to_i proto = p[3].downcase info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto } if name != "unknown" and name[-1,1] != "?" info[:name] = name end report_service(info) if nasl.nil? || nasl.empty? || nasl == 0 || nasl == "0" return end data.gsub!("\\n", "\n") refs = [] if (data =~ /^CVE : (.*)$/) $1.gsub(/C(VE|AN)\-/, '').split(',').map { |r| r.strip }.each do |r| refs.push('CVE-' + r) end end if (data =~ /^BID : (.*)$/) $1.split(',').map { |r| r.strip }.each do |r| refs.push('BID-' + r) end end if (data =~ /^Other references : (.*)$/) $1.split(',').map { |r| r.strip }.each do |r| ref_id, ref_val = r.split(':') ref_val ? refs.push(ref_id + '-' + ref_val) : refs.push(ref_id) end end nss = 'NSS-' + nasl.to_s.strip refs << nss unless plugin_name.to_s.strip.empty? vuln_name = plugin_name else vuln_name = nss end vuln_info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto, :name => vuln_name, :info => data, :refs => refs } report_vuln(vuln_info) end # # NESSUS v2 file format has a dramatically different layout # for ReportItem data # def handle_nessus_v2(wspace,hobj,port,proto,name,nasl,nasl_name,severity,description,cve,bid,xref,msf) addr = hobj.address info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto } unless name =~ /^unknown$|\?$/ info[:name] = name end if port.to_i != 0 report_service(info) end if nasl.nil? || nasl.empty? || nasl == 0 || nasl == "0" return end refs = [] cve.each do |r| r.to_s.gsub!(/C(VE|AN)\-/, '') refs.push('CVE-' + r.to_s) end if cve bid.each do |r| refs.push('BID-' + r.to_s) end if bid xref.each do |r| ref_id, ref_val = r.to_s.split(':') ref_val ? refs.push(ref_id + '-' + ref_val) : refs.push(ref_id) end if xref msfref = "MSF-" << msf if msf refs.push msfref if msfref nss = 'NSS-' + nasl if nasl_name.nil? || nasl_name.empty? vuln_name = nss else vuln_name = nasl_name end refs << nss.strip vuln = { :workspace => wspace, :host => hobj, :name => vuln_name, :info => description ? description : "", :refs => refs } if port.to_i != 0 vuln[:port] = port vuln[:proto] = proto end report_vuln(vuln) end # # IP360 v3 vuln # def handle_ip360_v3_svc(wspace,hobj,port,proto,hname) addr = hobj.address report_host(:workspace => wspace, :host => hobj, :state => Msf::HostState::Alive) info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto } if hname != "unknown" and hname[-1,1] != "?" info[:name] = hname end if port.to_i != 0 report_service(info) end end #handle_ip360_v3_svc # # IP360 v3 vuln # def handle_ip360_v3_vuln(wspace,hobj,port,proto,hname,vulnid,vulnname,cves,bids) info = { :workspace => wspace, :host => hobj, :port => port, :proto => proto } if hname != "unknown" and hname[-1,1] != "?" info[:name] = hname end if port.to_i != 0 report_service(info) end refs = [] cves.split(/,/).each do |cve| refs.push(cve.to_s) end if cves bids.split(/,/).each do |bid| refs.push('BID-' + bid.to_s) end if bids description = nil # not working yet vuln = { :workspace => wspace, :host => hobj, :name => vulnname, :info => description ? description : "", :refs => refs } if port.to_i != 0 vuln[:port] = port vuln[:proto] = proto end report_vuln(vuln) end #handle_ip360_v3_vuln # # Qualys report parsing/handling # def handle_qualys(wspace, hobj, port, protocol, qid, severity, refs, name=nil, title=nil) addr = hobj.address port = port.to_i if port info = { :workspace => wspace, :host => hobj, :port => port, :proto => protocol } if name and name != 'unknown' and name != 'No registered hostname' info[:name] = name end if info[:host] && info[:port] && info[:proto] report_service(info) end fixed_refs = [] if refs refs.each do |ref| case ref when /^MS[0-9]{2}-[0-9]{3}/ fixed_refs << "MSB-#{ref}" else fixed_refs << ref end end end return if qid == 0 title = 'QUALYS-' + qid if title.nil? or title.empty? if addr report_vuln( :workspace => wspace, :host => hobj, :port => port, :proto => protocol, :name => title, :refs => fixed_refs ) end end def process_nexpose_data_sxml_refs(vuln) refs = [] vid = vuln.attributes['id'].to_s.downcase vry = vuln.attributes['resultCode'].to_s.upcase # Only process vuln-exploitable and vuln-version statuses return if vry !~ /^V[VE]$/ refs = [] vuln.elements.each('id') do |ref| rtyp = ref.attributes['type'].to_s.upcase rval = ref.text.to_s.strip case rtyp when 'CVE' refs << rval.gsub('CAN', 'CVE') when 'MS' # obsolete? refs << "MSB-MS-#{rval}" else refs << "#{rtyp}-#{rval}" end end refs << "NEXPOSE-#{vid}" refs end end end