Files
metasploit-gs/lib/msf/core/exploit/http/client.rb
T
2013-09-25 13:06:14 -05:00

523 lines
16 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('USERNAME', [false, 'The HTTP username to specify for authentication', '']),
OptString.new('PASSWORD', [false, 'The HTTP password to specify for 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(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
client_username = opts['username'] || datastore['USERNAME'] || ''
client_password = opts['password'] || datastore['PASSWORD'] || ''
nclient = Rex::Proto::Http::Client.new(
opts['rhost'] || rhost,
(opts['rport'] || rport).to_i,
{
'Msf' => framework,
'MsfExploit' => self,
},
dossl,
ssl_version,
proxies,
client_username,
client_password
)
# Configure the HTTP client with the supplied parameter
nclient.set_config(
'vhost' => self.vhost(),
'agent' => datastore['UserAgent'],
'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'],
'usentlm2_session' => datastore['NTLM::UseNTLM2_session'],
'use_ntlmv2' => datastore['NTLM::UseNTLMv2'],
'send_lm' => datastore['NTLM::SendLM'],
'send_ntlm' => datastore['NTLM::SendNTLM'],
'SendSPN' => datastore['NTLM::SendSPN'],
'UseLMKey' => datastore['NTLM::UseLMKey'],
'domain' => datastore['DOMAIN'],
'DigestAuthIIS' => datastore['DigestAuthIIS']
)
# 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(username, password)
auth_str = Rex::Text.encode_base64("#{username}:#{password}")
"Basic #{auth_str}"
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
#
def normalize_uri(*strs)
new_str = strs * "/"
new_str = new_str.gsub!("//", "/") while new_str.index("//")
# Makes sure there's a starting slash
unless new_str[0,1] == '/'
new_str = '/' + new_str
end
new_str
end
# Returns the Path+Query from a full URI String, nil on error
def path_from_uri(uri)
begin
temp = URI(uri)
ret_uri = temp.path
ret_uri << "?#{temp.query}" unless temp.query.nil? or temp.query.empty?
return ret_uri
rescue URI::Error
print_error "Invalid URI: #{uri}"
return nil
end
end
#
# Returns the target host
#
def rhost
datastore['RHOST']
end
#
# Returns the remote port
#
def rport
datastore['RPORT']
end
#
# Returns the Host and Port as a string
#
def peer
"#{rhost}:#{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)$/n
if res['Server'] =~ /^(thin.*No Hup)|(nginx[\x5c\x2f][\d\.]+)$/n
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