9a58b7b732
This will make sure all the double slashes are gone. Also, the function description is updated to clarify its purpose.
731 lines
22 KiB
Ruby
731 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 a modified version of the URI that:
|
|
# 1. Always has a starting slash
|
|
# 2. Removes all the double slashes
|
|
# 3. Removes the trailing slash
|
|
#
|
|
def normalize_uri(str)
|
|
# Makes sure there's a starting slash
|
|
unless str.to_s[0,1] == "/"
|
|
str = "/" + str.to_s
|
|
end
|
|
|
|
# Removes all double slashes
|
|
str = str.gsub!("//", "/") while str.index("//")
|
|
|
|
# Makes sure there's no trailing slash
|
|
unless str.length == 1
|
|
str = str.gsub(/\/+$/, '')
|
|
end
|
|
|
|
str
|
|
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
|