Reimplement HTTP fingerprinting, backwards compatible
This commit changes the internals of HTTP fingerprinting to store a whole trove of data about the HTTP response using a hash. The current API is backwards compatible and has been tested with a number of modules that depend on HttpFingerprint being sent. In addition, this change paves the way for advanced fingerprints that take advantage of the HTTP body and other headers. This is a requested addition documented across various module comments. Finally, this commit completes the closed loop for OS identification by connecting MSF to MDM to Recog and applying Recog databases for HTTP Servers, HTTP Cookies, and HTTP Authentication headers to the results of HTTP fingerprinting runs. For example, with the appropriate version of MDM/Recog in place, a http_version scan of Microsoft-IIS/7.0 server will update the host.os_name field to 'Windows 2008'.bug/bundler_fix
parent
506c354722
commit
f349f85a70
|
@ -439,6 +439,43 @@ module Exploit::Remote::HttpClient
|
|||
datastore['Proxies']
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Lookup HTTP fingerprints from the database that match the current
|
||||
# destination host and port. This method falls back to using the old
|
||||
# service.info field to represent the HTTP Server header.
|
||||
#
|
||||
# Options:
|
||||
# :uri an HTTP URI to request in order to generate a fingerprint
|
||||
# :method an HTTP method to use in the fingerprint request
|
||||
#
|
||||
def lookup_http_fingerprints(opts={})
|
||||
uri = opts[:uri] || '/'
|
||||
method = opts[:method] || 'GET'
|
||||
fprints = []
|
||||
|
||||
return fprints unless framework.db.active
|
||||
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
wspace = datastore['WORKSPACE'] ?
|
||||
framework.db.find_workspace(datastore['WORKSPACE']) : framework.db.workspace
|
||||
|
||||
service = framework.db.get_service(wspace, rhost, 'tcp', rport)
|
||||
return fprints unless service
|
||||
|
||||
# Order by note_id descending so the first value is the most recent
|
||||
service.notes.where(:ntype => 'http.fingerprint').order("notes.id DESC").each do |n|
|
||||
next unless n.data and n.data.kind_of?(::Hash)
|
||||
next unless n.data[:uri] == uri and n.data[:method] == method
|
||||
|
||||
# Append additional fingerprints to the results as found
|
||||
fprints.unshift n.data.dup
|
||||
end
|
||||
}
|
||||
|
||||
fprints
|
||||
end
|
||||
|
||||
#
|
||||
# Record various things about an HTTP server that we can glean from the
|
||||
# response to a single request. If this method is passed a response, it
|
||||
|
@ -447,33 +484,38 @@ module Exploit::Remote::HttpClient
|
|||
#
|
||||
# Options:
|
||||
# :response an Http::Packet as returned from any of the send_* methods
|
||||
# :uri an HTTP URI to request in order to generate a fingerprint
|
||||
# :method an HTTP method to use in the fingerprint request
|
||||
# :full request the full HTTP fingerprint, not just the signature
|
||||
#
|
||||
# Other options are passed directly to +connect+ if :response is not given
|
||||
#
|
||||
def http_fingerprint(opts={})
|
||||
res = nil
|
||||
uri = opts[:uri] || '/'
|
||||
method = opts[:method] || 'GET'
|
||||
|
||||
if (opts[:response])
|
||||
# Short-circuit the fingerprint lookup and HTTP request if a response has
|
||||
# already been provided by the caller.
|
||||
if opts[:response]
|
||||
res = opts[:response]
|
||||
else
|
||||
# Check to see if we already have a fingerprint before going out to
|
||||
# the network.
|
||||
if (framework.db.active)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
wspace = framework.db.workspace
|
||||
if datastore['WORKSPACE']
|
||||
wspace = framework.db.find_workspace(datastore['WORKSPACE'])
|
||||
end
|
||||
fprints = lookup_http_fingerprints(opts)
|
||||
|
||||
s = framework.db.get_service(wspace, rhost, 'tcp', rport)
|
||||
if (s and s.info)
|
||||
return s.info
|
||||
end
|
||||
}
|
||||
if fprints.length > 0
|
||||
|
||||
# Grab the most recent fingerprint available for this service, uri, and method
|
||||
fprint = fprints.last
|
||||
|
||||
# Return the full HTTP fingerprint if requested by the caller
|
||||
return fprint if opts[:full]
|
||||
|
||||
# Otherwise just return the signature string for compatibility
|
||||
fprint[:signature]
|
||||
end
|
||||
|
||||
# Go ahead and send a request to the target for fingerprinting
|
||||
connect(opts)
|
||||
uri = opts[:uri] || '/'
|
||||
method = opts[:method] || 'GET'
|
||||
res = send_request_raw(
|
||||
{
|
||||
'uri' => uri,
|
||||
|
@ -481,13 +523,15 @@ module Exploit::Remote::HttpClient
|
|||
})
|
||||
end
|
||||
|
||||
# Bail if we don't have anything to fingerprint
|
||||
# Bail if the request did not receive a readable response
|
||||
return if not res
|
||||
|
||||
# From here to the end simply does some pre-canned combining and custom matches
|
||||
# to build a human-readable string to store in service.info
|
||||
# This section handles a few simple cases of pattern matching and service
|
||||
# classification. This logic should be deprecated in favor of Recog-based
|
||||
# fingerprint databases, but has been left in place for backward compat.
|
||||
|
||||
extras = []
|
||||
|
||||
|
||||
if res.headers['Set-Cookie'] =~ /^vmware_soap_session/
|
||||
extras << "VMWare Web Services"
|
||||
end
|
||||
|
@ -537,6 +581,11 @@ module Exploit::Remote::HttpClient
|
|||
end
|
||||
end
|
||||
|
||||
#
|
||||
# This HTTP response code tracking is used by a few modules and the MSP logic
|
||||
# to identify and bruteforce certain types of servers. In the long run we
|
||||
# should deprecate this and use the http.fingerprint fields instead.
|
||||
#
|
||||
case res.code
|
||||
when 301,302
|
||||
extras << "#{res.code}-#{res.headers['Location']}"
|
||||
|
@ -548,12 +597,43 @@ module Exploit::Remote::HttpClient
|
|||
extras << "#{res.code}-#{res.message}"
|
||||
end
|
||||
|
||||
info = "#{res.headers['Server']}"
|
||||
# Build a human-readable string to store in service.info and http.fingerprint[:signature]
|
||||
info = res.headers['Server'].to_s.dup
|
||||
info << " ( #{extras.join(", ")} )" if extras.length > 0
|
||||
|
||||
# Create a new fingerprint structure to track this response
|
||||
fprint = {
|
||||
:uri => uri, :method => method,
|
||||
:code => res.code.to_s, :message => res.message.to_s,
|
||||
:signature => info
|
||||
}
|
||||
|
||||
res.headers.each_pair do |k,v|
|
||||
hname = k.to_s.downcase.gsub('-', '_').gsub(/[^a-z0-9_]+/, '')
|
||||
next unless hname.length > 0
|
||||
|
||||
# Set-Cookie > :header_set_cookie => JSESSIONID=AAASD23423452
|
||||
# Server > :header_server => Apache/1.3.37
|
||||
# WWW-Authenticate > :header_www_authenticate => basic realm='www'
|
||||
|
||||
fprint["header_#{hname}".intern] = v
|
||||
end
|
||||
|
||||
# Store the first 64k of the HTTP body as well
|
||||
fprint[:content] = res.body.to_s[0,65535]
|
||||
|
||||
# Report a new http.fingerprint note
|
||||
report_note(:host => rhost, :port => rport, :ntype => 'http.fingerprint', :data => fprint, :update => :unique_data)
|
||||
|
||||
# Report here even if info is empty since the fact that we didn't
|
||||
# return early means we at least got a connection and the service is up
|
||||
report_web_site(:host => rhost, :port => rport, :ssl => ssl, :vhost => vhost, :info => info.dup)
|
||||
info
|
||||
|
||||
# Return the full HTTP fingerprint if requested by the caller
|
||||
return fprint if opts[:full]
|
||||
|
||||
# Otherwise just return the signature string for compatibility
|
||||
fprint[:signature]
|
||||
end
|
||||
|
||||
def make_cnonce
|
||||
|
@ -566,4 +646,4 @@ protected
|
|||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue