708 lines
22 KiB
Ruby
708 lines
22 KiB
Ruby
# -*- coding: binary -*-
|
|
require 'uri'
|
|
require 'digest'
|
|
require 'rex/proto/ntlm/crypt'
|
|
require 'rex/proto/ntlm/constants'
|
|
require 'rex/proto/ntlm/utils'
|
|
require 'rex/proto/ntlm/exceptions'
|
|
module Msf
|
|
|
|
###
|
|
#
|
|
# This module provides methods for acting as an HTTP client when
|
|
# exploiting an HTTP server.
|
|
#
|
|
###
|
|
module Exploit::Remote::HttpClient
|
|
include Msf::Auxiliary::Report
|
|
include Exploit::Remote::NTLM::Client
|
|
|
|
#
|
|
# Constants
|
|
#
|
|
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
|
|
NTLM_CONST = Rex::Proto::NTLM::Constants
|
|
NTLM_UTILS = Rex::Proto::NTLM::Utils
|
|
NTLM_XCEPT = Rex::Proto::NTLM::Exceptions
|
|
|
|
#
|
|
# Initializes an exploit module that exploits a vulnerability in an HTTP
|
|
# server.
|
|
#
|
|
def initialize(info = {})
|
|
super
|
|
|
|
register_options(
|
|
[
|
|
Opt::RHOST,
|
|
Opt::RPORT(80),
|
|
OptString.new('VHOST', [ false, "HTTP server virtual host" ]),
|
|
Opt::Proxies
|
|
], self.class
|
|
)
|
|
|
|
register_advanced_options(
|
|
[
|
|
OptString.new('UserAgent', [false, 'The User-Agent header to use for all requests',
|
|
Rex::Proto::Http::Client::DefaultUserAgent
|
|
]),
|
|
OptString.new('BasicAuthUser', [false, 'The HTTP username to specify for basic authentication']),
|
|
OptString.new('BasicAuthPass', [false, 'The HTTP password to specify for basic authentication']),
|
|
OptString.new('DigestAuthUser', [false, 'The HTTP username to specify for digest authentication']),
|
|
OptString.new('DigestAuthPassword', [false, 'The HTTP password to specify for digest authentication']),
|
|
OptBool.new('DigestAuthIIS', [false, 'Conform to IIS, should work for most servers. Only set to false for non-IIS servers', true]),
|
|
OptBool.new('SSL', [ false, 'Negotiate SSL for outgoing connections', false]),
|
|
OptEnum.new('SSLVersion', [ false, 'Specify the version of SSL that should be used', 'SSL3', ['SSL2', 'SSL3', 'TLS1']]),
|
|
OptBool.new('FingerprintCheck', [ false, 'Conduct a pre-exploit fingerprint verification', true]),
|
|
OptString.new('DOMAIN', [ true, 'The domain to use for windows authentification', 'WORKSTATION'])
|
|
], self.class
|
|
)
|
|
|
|
register_evasion_options(
|
|
[
|
|
OptEnum.new('HTTP::uri_encode_mode', [false, 'Enable URI encoding', 'hex-normal', ['none', 'hex-normal', 'hex-all', 'hex-random', 'u-normal', 'u-all', 'u-random']]),
|
|
OptBool.new('HTTP::uri_full_url', [false, 'Use the full URL for all HTTP requests', false]),
|
|
OptInt.new('HTTP::pad_method_uri_count', [false, 'How many whitespace characters to use between the method and uri', 1]),
|
|
OptInt.new('HTTP::pad_uri_version_count', [false, 'How many whitespace characters to use between the uri and version', 1]),
|
|
OptEnum.new('HTTP::pad_method_uri_type', [false, 'What type of whitespace to use between the method and uri', 'space', ['space', 'tab', 'apache']]),
|
|
OptEnum.new('HTTP::pad_uri_version_type', [false, 'What type of whitespace to use between the uri and version', 'space', ['space', 'tab', 'apache']]),
|
|
OptBool.new('HTTP::method_random_valid', [false, 'Use a random, but valid, HTTP method for request', false]),
|
|
OptBool.new('HTTP::method_random_invalid', [false, 'Use a random invalid, HTTP method for request', false]),
|
|
OptBool.new('HTTP::method_random_case', [false, 'Use random casing for the HTTP method', false]),
|
|
OptBool.new('HTTP::uri_dir_self_reference', [false, 'Insert self-referential directories into the uri', false]),
|
|
OptBool.new('HTTP::uri_dir_fake_relative', [false, 'Insert fake relative directories into the uri', false]),
|
|
OptBool.new('HTTP::uri_use_backslashes', [false, 'Use back slashes instead of forward slashes in the uri ', false]),
|
|
OptBool.new('HTTP::pad_fake_headers', [false, 'Insert random, fake headers into the HTTP request', false]),
|
|
OptInt.new('HTTP::pad_fake_headers_count', [false, 'How many fake headers to insert into the HTTP request', 0]),
|
|
OptBool.new('HTTP::pad_get_params', [false, 'Insert random, fake query string variables into the request', false]),
|
|
OptInt.new('HTTP::pad_get_params_count', [false, 'How many fake query string variables to insert into the request', 16]),
|
|
OptBool.new('HTTP::pad_post_params', [false, 'Insert random, fake post variables into the request', false]),
|
|
OptInt.new('HTTP::pad_post_params_count', [false, 'How many fake post variables to insert into the request', 16]),
|
|
OptBool.new('HTTP::uri_fake_end', [false, 'Add a fake end of URI (eg: /%20HTTP/1.0/../../)', false]),
|
|
OptBool.new('HTTP::uri_fake_params_start', [false, 'Add a fake start of params to the URI (eg: /%3fa=b/../)', false]),
|
|
OptBool.new('HTTP::header_folding', [false, 'Enable folding of HTTP headers', false])
|
|
#
|
|
# Remaining evasions to implement
|
|
#
|
|
# OptBool.new('HTTP::chunked', [false, 'Enable chunking of HTTP request via "Transfer-Encoding: chunked"', 'false']),
|
|
# OptInt.new('HTTP::junk_pipeline', [true, 'Insert the specified number of junk pipeline requests', 0]),
|
|
], self.class
|
|
)
|
|
register_autofilter_ports([ 80, 8080, 443, 8000, 8888, 8880, 8008, 3000, 8443 ])
|
|
register_autofilter_services(%W{ http https })
|
|
|
|
# Used by digest auth
|
|
@cnonce = make_cnonce
|
|
@nonce_count = -1
|
|
end
|
|
|
|
|
|
#
|
|
# For HTTP Client exploits, we often want to verify that the server info matches some regex before
|
|
# firing a giant binary exploit blob at it. We override setup() here to accomplish that.
|
|
#
|
|
def setup
|
|
validate_fingerprint
|
|
super
|
|
end
|
|
|
|
|
|
#
|
|
# This method is meant to be overriden in the exploit module to specify a set of regexps to
|
|
# attempt to match against. A failure to match any of them results in a RuntimeError exception
|
|
# being raised.
|
|
#
|
|
def validate_fingerprint()
|
|
# Don't bother checking if there's no database active.
|
|
if (framework.db.active and
|
|
datastore['FingerprintCheck'] and
|
|
self.class.const_defined?('HttpFingerprint'))
|
|
# Get the module-specific config
|
|
opts = self.class.const_get('HttpFingerprint')
|
|
#
|
|
# XXX: Ideally we could have more structured matches, but doing that requires
|
|
# a more structured response cache.
|
|
#
|
|
info = http_fingerprint(opts)
|
|
if info and opts[:pattern]
|
|
opts[:pattern].each do |re|
|
|
if not re.match(info)
|
|
err = "The target server fingerprint \"#{info}\" does not match \"#{re.to_s}\", use 'set FingerprintCheck false' to disable this check."
|
|
fail_with(Msf::Exploit::Failure::NotFound, err)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
#
|
|
# Connects to an HTTP server.
|
|
#
|
|
def connect(opts={})
|
|
dossl = false
|
|
if(opts.has_key?('SSL'))
|
|
dossl = opts['SSL']
|
|
else
|
|
dossl = ssl
|
|
end
|
|
|
|
nclient = Rex::Proto::Http::Client.new(
|
|
rhost,
|
|
rport.to_i,
|
|
{
|
|
'Msf' => framework,
|
|
'MsfExploit' => self,
|
|
},
|
|
dossl,
|
|
ssl_version,
|
|
proxies
|
|
)
|
|
|
|
# Configure the HTTP client with the supplied parameter
|
|
nclient.set_config(
|
|
'vhost' => self.vhost(),
|
|
'agent' => datastore['UserAgent'],
|
|
'basic_auth' => self.basic_auth,
|
|
'uri_encode_mode' => datastore['HTTP::uri_encode_mode'],
|
|
'uri_full_url' => datastore['HTTP::uri_full_url'],
|
|
'pad_method_uri_count' => datastore['HTTP::pad_method_uri_count'],
|
|
'pad_uri_version_count' => datastore['HTTP::pad_uri_version_count'],
|
|
'pad_method_uri_type' => datastore['HTTP::pad_method_uri_type'],
|
|
'pad_uri_version_type' => datastore['HTTP::pad_uri_version_type'],
|
|
'method_random_valid' => datastore['HTTP::method_random_valid'],
|
|
'method_random_invalid' => datastore['HTTP::method_random_invalid'],
|
|
'method_random_case' => datastore['HTTP::method_random_case'],
|
|
'uri_dir_self_reference' => datastore['HTTP::uri_dir_self_reference'],
|
|
'uri_dir_fake_relative' => datastore['HTTP::uri_dir_fake_relative'],
|
|
'uri_use_backslashes' => datastore['HTTP::uri_use_backslashes'],
|
|
'pad_fake_headers' => datastore['HTTP::pad_fake_headers'],
|
|
'pad_fake_headers_count' => datastore['HTTP::pad_fake_headers_count'],
|
|
'pad_get_params' => datastore['HTTP::pad_get_params'],
|
|
'pad_get_params_count' => datastore['HTTP::pad_get_params_count'],
|
|
'pad_post_params' => datastore['HTTP::pad_post_params'],
|
|
'pad_post_params_count' => datastore['HTTP::pad_post_params_count'],
|
|
'uri_fake_end' => datastore['HTTP::uri_fake_end'],
|
|
'uri_fake_params_start' => datastore['HTTP::uri_fake_params_start'],
|
|
'header_folding' => datastore['HTTP::header_folding']
|
|
)
|
|
|
|
# If this connection is global, persist it
|
|
# Required for findsock on these sockets
|
|
if (opts['global'])
|
|
if (self.client)
|
|
disconnect
|
|
end
|
|
|
|
self.client = nclient
|
|
end
|
|
|
|
return nclient
|
|
end
|
|
|
|
#
|
|
# Passes the client connection down to the handler to see if it's of any
|
|
# use.
|
|
#
|
|
def handler(nsock = nil)
|
|
# If no socket was provided, try the global one.
|
|
if ((!nsock) and (self.client))
|
|
nsock = self.client.conn
|
|
end
|
|
|
|
# If the parent claims the socket associated with the HTTP client, then
|
|
# we rip the socket out from under the HTTP client.
|
|
if (((rv = super(nsock)) == Handler::Claimed) and
|
|
(self.client) and
|
|
(nsock == self.client.conn))
|
|
self.client.conn = nil
|
|
end
|
|
|
|
rv
|
|
end
|
|
|
|
#
|
|
# Disconnects the HTTP client
|
|
#
|
|
def disconnect(nclient = self.client)
|
|
if (nclient)
|
|
nclient.close
|
|
end
|
|
|
|
if (nclient == self.client)
|
|
self.client = nil
|
|
end
|
|
end
|
|
|
|
#
|
|
# Performs cleanup as necessary, disconnecting the HTTP client if it's
|
|
# still established.
|
|
#
|
|
def cleanup
|
|
super
|
|
disconnect
|
|
end
|
|
|
|
#
|
|
# Connects to the server, creates a request, sends the request, reads the response
|
|
#
|
|
# Passes +opts+ through directly to Rex::Proto::Http::Client#request_raw.
|
|
#
|
|
def send_request_raw(opts={}, timeout = 20)
|
|
begin
|
|
c = connect(opts)
|
|
r = c.request_raw(opts)
|
|
c.send_recv(r, opts[:timeout] ? opts[:timeout] : timeout)
|
|
rescue ::Errno::EPIPE, ::Timeout::Error
|
|
nil
|
|
end
|
|
end
|
|
|
|
#
|
|
# Connects to the server, creates a request, sends the request, reads the response
|
|
#
|
|
# Passes +opts+ through directly to Rex::Proto::Http::Client#request_cgi.
|
|
#
|
|
def send_request_cgi(opts={}, timeout = 20)
|
|
begin
|
|
c = connect(opts)
|
|
r = c.request_cgi(opts)
|
|
c.send_recv(r, opts[:timeout] ? opts[:timeout] : timeout)
|
|
rescue ::Errno::EPIPE, ::Timeout::Error
|
|
nil
|
|
end
|
|
end
|
|
|
|
#
|
|
# Combine the user/pass into an auth string for the HTTP Client
|
|
#
|
|
def basic_auth
|
|
return if not datastore['BasicAuthUser']
|
|
datastore['BasicAuthUser'] + ":" + (datastore['BasicAuthPass'] || '')
|
|
end
|
|
|
|
#
|
|
# Connect to the server, and perform NTLM authentication for this session.
|
|
# Note the return value is [resp,c], so the caller can have access to both
|
|
# the last response, and the connection itself -- this is important since
|
|
# NTLM auth is bound to this particular TCP session.
|
|
#
|
|
# TODO: Fix up error messaging a lot more -- right now it's pretty hard
|
|
# to tell what all went wrong.
|
|
#
|
|
def send_http_auth_ntlm(opts={}, timeout = 20)
|
|
#ntlm_message_1 = "NTLM TlRMTVNTUAABAAAAB4IIAAAAAAAAAAAAAAAAAAAAAAA="
|
|
ntlm_options = {
|
|
:signing => false,
|
|
:usentlm2_session => datastore['NTLM::UseNTLM2_session'],
|
|
:use_ntlmv2 => datastore['NTLM::UseNTLMv2'],
|
|
:send_lm => datastore['NTLM::SendLM'],
|
|
:send_ntlm => datastore['NTLM::SendNTLM']
|
|
}
|
|
|
|
ntlmssp_flags = NTLM_UTILS.make_ntlm_flags(ntlm_options)
|
|
workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)
|
|
domain_name = datastore['DOMAIN']
|
|
|
|
ntlm_message_1 = "NTLM " + Rex::Text::encode_base64(NTLM_UTILS::make_ntlmssp_blob_init( domain_name,
|
|
workstation_name,
|
|
ntlmssp_flags))
|
|
to = opts[:timeout] || timeout
|
|
begin
|
|
c = connect(opts)
|
|
|
|
# First request to get the challenge
|
|
r = c.request_cgi(opts.merge({
|
|
'uri' => opts['uri'],
|
|
'method' => 'GET',
|
|
'headers' => { 'Authorization' => ntlm_message_1 }}))
|
|
resp = c.send_recv(r, to)
|
|
unless resp.kind_of? Rex::Proto::Http::Response
|
|
return [nil,nil]
|
|
end
|
|
return [nil,nil] if resp.code == 404
|
|
return [nil,nil] unless resp.code == 401 && resp.headers['WWW-Authenticate']
|
|
|
|
# Get the challenge and craft the response
|
|
ntlm_challenge = resp.headers['WWW-Authenticate'].match(/NTLM ([A-Z0-9\x2b\x2f=]+)/i)[1]
|
|
return [nil,nil] unless ntlm_challenge
|
|
|
|
|
|
#old and simplier method but not compatible with windows 7/2008r2
|
|
#ntlm_message_2 = Rex::Proto::NTLM::Message.decode64(ntlm_challenge)
|
|
#ntlm_message_3 = ntlm_message_2.response( {:user => opts['username'],:password => opts['password']}, {:ntlmv2 => true})
|
|
|
|
ntlm_message_2 = Rex::Text::decode_base64(ntlm_challenge)
|
|
blob_data = NTLM_UTILS.parse_ntlm_type_2_blob(ntlm_message_2)
|
|
|
|
challenge_key = blob_data[:challenge_key]
|
|
server_ntlmssp_flags = blob_data[:server_ntlmssp_flags] #else should raise an error
|
|
#netbios name
|
|
default_name = blob_data[:default_name] || ''
|
|
#netbios domain
|
|
default_domain = blob_data[:default_domain] || ''
|
|
#dns name
|
|
dns_host_name = blob_data[:dns_host_name] || ''
|
|
#dns domain
|
|
dns_domain_name = blob_data[:dns_domain_name] || ''
|
|
#Client time
|
|
chall_MsvAvTimestamp = blob_data[:chall_MsvAvTimestamp] || ''
|
|
|
|
spnopt = {:use_spn => datastore['NTLM::SendSPN'], :name => self.rhost}
|
|
|
|
resp_lm,
|
|
resp_ntlm,
|
|
client_challenge,
|
|
ntlm_cli_challenge = NTLM_UTILS.create_lm_ntlm_responses(opts['username'], opts['password'], challenge_key,
|
|
domain_name, default_name, default_domain,
|
|
dns_host_name, dns_domain_name, chall_MsvAvTimestamp,
|
|
spnopt, ntlm_options)
|
|
|
|
ntlm_message_3 = NTLM_UTILS.make_ntlmssp_blob_auth(domain_name, workstation_name, opts['username'],
|
|
resp_lm, resp_ntlm, '', ntlmssp_flags)
|
|
ntlm_message_3 = Rex::Text::encode_base64(ntlm_message_3)
|
|
|
|
# Send the response
|
|
r = c.request_cgi(opts.merge({
|
|
'uri' => opts['uri'],
|
|
'method' => 'GET',
|
|
'headers' => { 'Authorization' => "NTLM #{ntlm_message_3}"}}))
|
|
resp = c.send_recv(r, to, true)
|
|
unless resp.kind_of? Rex::Proto::Http::Response
|
|
return [nil,nil]
|
|
end
|
|
return [nil,nil] if resp.code == 404
|
|
return [resp,c]
|
|
|
|
rescue ::Errno::EPIPE, ::Timeout::Error
|
|
end
|
|
end
|
|
|
|
def send_digest_request_cgi(opts={}, timeout=20)
|
|
@nonce_count = 0
|
|
|
|
return [nil,nil] if not (datastore['DigestAuthUser'] or opts['DigestAuthUser'])
|
|
to = opts['timeout'] || timeout
|
|
|
|
digest_user = datastore['DigestAuthUser'] || opts['DigestAuthUser'] || ""
|
|
digest_password = datastore['DigestAuthPassword'] || opts['DigestAuthPassword'] || ""
|
|
|
|
method = opts['method']
|
|
path = opts['uri']
|
|
iis = true
|
|
if (opts['DigestAuthIIS'] == false or datastore['DigestAuthIIS'] == false)
|
|
iis = false
|
|
end
|
|
|
|
begin
|
|
@nonce_count += 1
|
|
|
|
resp = opts['response']
|
|
|
|
if not resp
|
|
# Get authentication-challenge from server, and read out parameters required
|
|
c = connect(opts)
|
|
r = c.request_cgi(opts.merge({
|
|
'uri' => path,
|
|
'method' => method }))
|
|
resp = c.send_recv(r, to)
|
|
unless resp.kind_of? Rex::Proto::Http::Response
|
|
return [nil,nil]
|
|
end
|
|
return [nil,nil] if resp.code == 404
|
|
if resp.code != 401
|
|
return resp
|
|
end
|
|
return [nil,nil] unless resp.headers['WWW-Authenticate']
|
|
end
|
|
|
|
# Don't anchor this regex to the beginning of string because header
|
|
# folding makes it appear later when the server presents multiple
|
|
# WWW-Authentication options (such as is the case with IIS configured
|
|
# for Digest or NTLM).
|
|
resp['www-authenticate'] =~ /Digest (.*)/
|
|
|
|
parameters = {}
|
|
$1.split(/,[[:space:]]*/).each do |p|
|
|
k, v = p.split("=", 2)
|
|
parameters[k] = v.gsub('"', '')
|
|
end
|
|
|
|
qop = parameters['qop']
|
|
|
|
if parameters['algorithm'] =~ /(.*?)(-sess)?$/
|
|
algorithm = case $1
|
|
when 'MD5' then Digest::MD5
|
|
when 'SHA1' then Digest::SHA1
|
|
when 'SHA2' then Digest::SHA2
|
|
when 'SHA256' then Digest::SHA256
|
|
when 'SHA384' then Digest::SHA384
|
|
when 'SHA512' then Digest::SHA512
|
|
when 'RMD160' then Digest::RMD160
|
|
else raise Error, "unknown algorithm \"#{$1}\""
|
|
end
|
|
algstr = parameters["algorithm"]
|
|
sess = $2
|
|
else
|
|
algorithm = Digest::MD5
|
|
algstr = "MD5"
|
|
sess = false
|
|
end
|
|
|
|
a1 = if sess then
|
|
[
|
|
algorithm.hexdigest("#{digest_user}:#{parameters['realm']}:#{digest_password}"),
|
|
parameters['nonce'],
|
|
@cnonce
|
|
].join ':'
|
|
else
|
|
"#{digest_user}:#{parameters['realm']}:#{digest_password}"
|
|
end
|
|
|
|
ha1 = algorithm.hexdigest(a1)
|
|
ha2 = algorithm.hexdigest("#{method}:#{path}")
|
|
|
|
request_digest = [ha1, parameters['nonce']]
|
|
request_digest.push(('%08x' % @nonce_count), @cnonce, qop) if qop
|
|
request_digest << ha2
|
|
request_digest = request_digest.join ':'
|
|
|
|
# Same order as IE7
|
|
auth = [
|
|
"Digest username=\"#{digest_user}\"",
|
|
"realm=\"#{parameters['realm']}\"",
|
|
"nonce=\"#{parameters['nonce']}\"",
|
|
"uri=\"#{path}\"",
|
|
"cnonce=\"#{@cnonce}\"",
|
|
"nc=#{'%08x' % @nonce_count}",
|
|
"algorithm=#{algstr}",
|
|
"response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",
|
|
# The spec says the qop value shouldn't be enclosed in quotes, but
|
|
# some versions of IIS require it and Apache accepts it. Chrome
|
|
# and Firefox both send it without quotes but IE does it this way.
|
|
# Use the non-compliant-but-everybody-does-it to be as compatible
|
|
# as possible by default. The user can override if they don't like
|
|
# it.
|
|
if qop.nil? then
|
|
elsif iis then
|
|
"qop=\"#{qop}\""
|
|
else
|
|
"qop=#{qop}"
|
|
end,
|
|
if parameters.key? 'opaque' then
|
|
"opaque=\"#{parameters['opaque']}\""
|
|
end
|
|
].compact
|
|
|
|
headers ={ 'Authorization' => auth.join(', ') }
|
|
headers.merge!(opts['headers']) if opts['headers']
|
|
|
|
|
|
# Send main request with authentication
|
|
r = c.request_cgi(opts.merge({
|
|
'uri' => path,
|
|
'method' => method,
|
|
'headers' => headers }))
|
|
resp = c.send_recv(r, to)
|
|
unless resp.kind_of? Rex::Proto::Http::Response
|
|
return [nil,nil]
|
|
end
|
|
|
|
return [resp,c]
|
|
|
|
rescue ::Errno::EPIPE, ::Timeout::Error
|
|
end
|
|
end
|
|
|
|
##
|
|
#
|
|
# Wrappers for getters
|
|
#
|
|
##
|
|
|
|
#
|
|
# Returns the target URI
|
|
#
|
|
def target_uri
|
|
begin
|
|
# In case TARGETURI is empty, at least we default to '/'
|
|
u = datastore['TARGETURI']
|
|
u = "/" if u.nil? or u.empty?
|
|
URI(u)
|
|
rescue ::URI::InvalidURIError
|
|
print_error "Invalid URI: #{datastore['TARGETURI'].inspect}"
|
|
raise Msf::OptionValidateError.new(['TARGETURI'])
|
|
end
|
|
end
|
|
|
|
#
|
|
# Returns the target host
|
|
#
|
|
def rhost
|
|
datastore['RHOST']
|
|
end
|
|
|
|
#
|
|
# Returns the remote port
|
|
#
|
|
def rport
|
|
datastore['RPORT']
|
|
end
|
|
|
|
#
|
|
# Returns the VHOST of the HTTP server.
|
|
#
|
|
def vhost
|
|
datastore['VHOST'] || datastore['RHOST']
|
|
end
|
|
|
|
#
|
|
# Returns the boolean indicating SSL
|
|
#
|
|
def ssl
|
|
((datastore.default?('SSL') and [443,3790].include?(rport.to_i)) or datastore['SSL'])
|
|
end
|
|
|
|
#
|
|
# Returns the string indicating SSL version
|
|
#
|
|
def ssl_version
|
|
datastore['SSLVersion']
|
|
end
|
|
|
|
#
|
|
# Returns the configured proxy list
|
|
#
|
|
def proxies
|
|
datastore['Proxies']
|
|
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
|
|
# will use it directly, otherwise it will check the database for a previous
|
|
# fingerprint. Failing that, it will make a request for /.
|
|
#
|
|
# Options:
|
|
# :response an Http::Packet as returned from any of the send_* methods
|
|
#
|
|
# Other options are passed directly to +connect+ if :response is not given
|
|
#
|
|
def http_fingerprint(opts={})
|
|
|
|
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
|
|
|
|
s = framework.db.get_service(wspace, rhost, 'tcp', rport)
|
|
if (s and s.info)
|
|
return s.info
|
|
end
|
|
}
|
|
end
|
|
|
|
connect(opts)
|
|
uri = opts[:uri] || '/'
|
|
method = opts[:method] || 'GET'
|
|
res = send_request_raw(
|
|
{
|
|
'uri' => uri,
|
|
'method' => method
|
|
})
|
|
end
|
|
|
|
# Bail if we don't have anything to fingerprint
|
|
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
|
|
extras = []
|
|
|
|
if res.headers['Set-Cookie'] =~ /^vmware_soap_session/
|
|
extras << "VMWare Web Services"
|
|
end
|
|
|
|
if (res.headers['X-Powered-By'])
|
|
extras << "Powered by " + res.headers['X-Powered-By']
|
|
end
|
|
|
|
if (res.headers['Via'])
|
|
extras << "Via-" + res.headers['Via']
|
|
end
|
|
|
|
if (res.headers['X-AspNet-Version'])
|
|
extras << "AspNet-Version-" + res.headers['X-AspNet-Version']
|
|
end
|
|
|
|
case res.body
|
|
when nil
|
|
# Nothing
|
|
when /openAboutWindow.*\>DD\-WRT ([^\<]+)\<|Authorization.*please note that the default username is \"root\" in/
|
|
extras << "DD-WRT #{$1.to_s.strip}".strip
|
|
|
|
when /ID_ESX_Welcome/, /ID_ESX_VIClientDesc/
|
|
extras << "VMware ESX Server"
|
|
|
|
when /Test Page for.*Fedora/
|
|
extras << "Fedora Default Page"
|
|
|
|
when /Placeholder page/
|
|
extras << "Debian Default Page"
|
|
|
|
when /Welcome to Windows Small Business Server (\d+)/
|
|
extras << "Windows SBS #{$1}"
|
|
|
|
when /Asterisk@Home/
|
|
extras << "Asterisk"
|
|
|
|
when /swfs\/Shell\.html/
|
|
extras << "BPS-1000"
|
|
end
|
|
|
|
if datastore['RPORT'].to_i == 3790
|
|
if res.code == 302 and res.headers and res.headers['Location'] =~ /[\x5c\x2f](login|setup)$/
|
|
if res['Server'] =~ /^(thin.*No Hup)|(nginx[\x5c\x2f][\d\.]+)$/
|
|
extras << "Metasploit"
|
|
end
|
|
end
|
|
end
|
|
|
|
case res.code
|
|
when 301,302
|
|
extras << "#{res.code}-#{res.headers['Location']}"
|
|
when 401
|
|
extras << "#{res.code}-#{res.headers['WWW-Authenticate']}"
|
|
when 403
|
|
extras << "#{res.code}-#{res.headers['WWW-Authenticate']||res.message}"
|
|
when 500 .. 599
|
|
extras << "#{res.code}-#{res.message}"
|
|
end
|
|
|
|
info = "#{res.headers['Server']}"
|
|
info << " ( #{extras.join(", ")} )" if extras.length > 0
|
|
# 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
|
|
end
|
|
|
|
def make_cnonce
|
|
Digest::MD5.hexdigest "%x" % (Time.now.to_i + rand(65535))
|
|
end
|
|
|
|
protected
|
|
|
|
attr_accessor :client
|
|
|
|
end
|
|
|
|
end
|