# -*- 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