2012-06-29 00:18:28 -05:00
# -*- coding: binary -*-
2012-03-09 14:44:32 -06:00
require 'uri'
2011-03-14 22:37:16 +00:00
require 'digest'
2011-05-12 20:03:55 +00:00
require 'rex/proto/ntlm/crypt'
require 'rex/proto/ntlm/constants'
require 'rex/proto/ntlm/utils'
require 'rex/proto/ntlm/exceptions'
2010-08-07 06:59:16 +00:00
module Msf
###
#
# This module provides methods for acting as an HTTP client when
# exploiting an HTTP server.
#
###
module Exploit::Remote::HttpClient
2013-08-30 16:28:33 -05:00
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 ] )
2010-08-07 06:59:16 +00:00
#
# 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]),
2013-08-30 16:28:33 -05:00
] , 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 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
2013-09-12 16:58:49 -05:00
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
2013-08-30 16:28:33 -05:00
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
2010-08-07 06:59:16 +00:00
protected
2013-08-30 16:28:33 -05:00
attr_accessor :client
2010-08-07 06:59:16 +00:00
end
2013-02-11 20:49:55 -06:00
end