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

	#
	# 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']),
				OptString.new('BasicAuthUser', [false, 'The HTTP username to specify for basic authentication']),
				OptString.new('BasicAuthPass', [false, 'The HTTP password to specify for basic authentication']),
				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])
			], 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 })
	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."
						raise RuntimeError, 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)
			resp = c.send_recv(r, opts[:timeout] ? opts[:timeout] : timeout)
			while(resp and resp.code == 100)
				print_status("Got a 100 response, trying to read again")
				resp = c.read_response(opts[:timeout] ? opts[:timeout] : timeout)
			end
			resp
		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)
			resp = c.send_recv(r, opts[:timeout] ? opts[:timeout] : timeout)
			while(resp and resp.code == 100)
				print_status("Got a 100 response, trying to read again")
				resp = c.read_response(opts[:timeout] ? opts[:timeout] : timeout)
			end
			resp
		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="
		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
			while(resp and resp.code == 100)
				resp = c.reread_response(resp, to)
			end
			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
			ntlm_message_2 = Net::NTLM::Message.decode64(ntlm_challenge)
			ntlm_message_3 = ntlm_message_2.response(
				{
					:user => opts['username'],
					:password => opts['password']
				}, {:ntlmv2 => true}
			)

			# Send the response
			r = c.request_cgi(opts.merge({
				'uri' => opts['uri'],
				'method' => 'GET',
				'headers' => { 'Authorization' => "NTLM #{ntlm_message_3.encode64}"}}))
			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
			while(resp and resp.code == 100)
				resp = c.reread_response(resp, to)
			end
			return [resp,c]

		rescue ::Errno::EPIPE, ::Timeout::Error
		end
	end

	##
	#
	# Wrappers for getters
	#
	##

	#
	# 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 rport.to_i == 443) 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)

				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 = []

		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

		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 ([^\<]+)\</
				extras << "DD-WRT #{$1.strip}"

			when /ID_ESX_Welcome/
				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

		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_service(:host => rhost, :port => rport, :name => (ssl ? 'https' : 'http'), :info => info.dup)
		info
	end


protected

	attr_accessor :client

end

end
