Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/lib/rex/proto/http/client.rb
Views: 11704
# -*- coding: binary -*-1require 'rex/socket'23require 'rex/text'4require 'digest'567module Rex8module Proto9module Http1011###12#13# Acts as a client to an HTTP server, sending requests and receiving responses.14#15# See the RFC: http://www.w3.org/Protocols/rfc2616/rfc2616.html16#17###18class Client1920#21# Creates a new client instance22# @param http_trace_proc_request [Proc] A proc object passed to log HTTP requests if HTTP-Trace is set23# @param http_trace_proc_response [Proc] A proc object passed to log HTTP responses if HTTP-Trace is set24#25def initialize(host, port = 80, context = {}, ssl = nil, ssl_version = nil, proxies = nil, username = '', password = '', kerberos_authenticator: nil, comm: nil, subscriber: nil)26self.hostname = host27self.port = port.to_i28self.context = context29self.ssl = ssl30self.ssl_version = ssl_version31self.proxies = proxies32self.username = username33self.password = password34self.kerberos_authenticator = kerberos_authenticator35self.comm = comm36self.subscriber = subscriber || HttpSubscriber.new3738# Take ClientRequest's defaults, but override with our own39self.config = Http::ClientRequest::DefaultConfig.merge({40'read_max_data' => (1024*1024*1),41'vhost' => self.hostname,42'ssl_server_name_indication' => self.hostname,43})44self.config['agent'] ||= Rex::UserAgent.session_agent4546# XXX: This info should all be controlled by ClientRequest47self.config_types = {48'uri_encode_mode' => ['hex-normal', 'hex-all', 'hex-random', 'hex-noslashes', 'u-normal', 'u-random', 'u-all'],49'uri_encode_count' => 'integer',50'uri_full_url' => 'bool',51'pad_method_uri_count' => 'integer',52'pad_uri_version_count' => 'integer',53'pad_method_uri_type' => ['space', 'tab', 'apache'],54'pad_uri_version_type' => ['space', 'tab', 'apache'],55'method_random_valid' => 'bool',56'method_random_invalid' => 'bool',57'method_random_case' => 'bool',58'version_random_valid' => 'bool',59'version_random_invalid' => 'bool',60'uri_dir_self_reference' => 'bool',61'uri_dir_fake_relative' => 'bool',62'uri_use_backslashes' => 'bool',63'pad_fake_headers' => 'bool',64'pad_fake_headers_count' => 'integer',65'pad_get_params' => 'bool',66'pad_get_params_count' => 'integer',67'pad_post_params' => 'bool',68'pad_post_params_count' => 'integer',69'shuffle_get_params' => 'bool',70'shuffle_post_params' => 'bool',71'uri_fake_end' => 'bool',72'uri_fake_params_start' => 'bool',73'header_folding' => 'bool',74'chunked_size' => 'integer',75'partial' => 'bool'76}77end7879#80# Set configuration options81#82def set_config(opts = {})83opts.each_pair do |var,val|84# Default type is string85typ = self.config_types[var] || 'string'8687# These are enum types88if typ.is_a?(Array)89if not typ.include?(val)90raise RuntimeError, "The specified value for #{var} is not one of the valid choices"91end92end9394# The caller should have converted these to proper ruby types, but95# take care of the case where they didn't before setting the96# config.9798if(typ == 'bool')99val = (val == true || val.to_s =~ /^(t|y|1)/i)100end101102if(typ == 'integer')103val = val.to_i104end105106self.config[var]=val107end108end109110#111# Create an arbitrary HTTP request112#113# @param opts [Hash]114# @option opts 'agent' [String] User-Agent header value115# @option opts 'connection' [String] Connection header value116# @option opts 'cookie' [String] Cookie header value117# @option opts 'data' [String] HTTP data (only useful with some methods, see rfc2616)118# @option opts 'encode' [Bool] URI encode the supplied URI, default: false119# @option opts 'headers' [Hash] HTTP headers, e.g. <code>{ "X-MyHeader" => "value" }</code>120# @option opts 'method' [String] HTTP method to use in the request, not limited to standard methods defined by rfc2616, default: GET121# @option opts 'proto' [String] protocol, default: HTTP122# @option opts 'query' [String] raw query string123# @option opts 'raw_headers' [String] Raw HTTP headers124# @option opts 'uri' [String] the URI to request125# @option opts 'version' [String] version of the protocol, default: 1.1126# @option opts 'vhost' [String] Host header value127#128# @return [ClientRequest]129def request_raw(opts = {})130opts = self.config.merge(opts)131132opts['cgi'] = false133opts['port'] = self.port134opts['ssl'] = self.ssl135136ClientRequest.new(opts)137end138139#140# Create a CGI compatible request141#142# @param (see #request_raw)143# @option opts (see #request_raw)144# @option opts 'ctype' [String] Content-Type header value, default for POST requests: +application/x-www-form-urlencoded+145# @option opts 'encode_params' [Bool] URI encode the GET or POST variables (names and values), default: true146# @option opts 'vars_get' [Hash] GET variables as a hash to be translated into a query string147# @option opts 'vars_post' [Hash] POST variables as a hash to be translated into POST data148# @option opts 'vars_form_data' [Hash] POST form_data variables as a hash to be translated into multi-part POST form data149#150# @return [ClientRequest]151def request_cgi(opts = {})152opts = self.config.merge(opts)153154opts['cgi'] = true155opts['port'] = self.port156opts['ssl'] = self.ssl157158ClientRequest.new(opts)159end160161#162# Connects to the remote server if possible.163#164# @param t [Integer] Timeout165# @see Rex::Socket::Tcp.create166# @return [Rex::Socket::Tcp]167def connect(t = -1)168# If we already have a connection and we aren't pipelining, close it.169if (self.conn)170if !pipelining?171close172else173return self.conn174end175end176177timeout = (t.nil? or t == -1) ? 0 : t178179self.conn = Rex::Socket::Tcp.create(180'PeerHost' => self.hostname,181'PeerHostname' => self.config['ssl_server_name_indication'] || self.config['vhost'],182'PeerPort' => self.port.to_i,183'LocalHost' => self.local_host,184'LocalPort' => self.local_port,185'Context' => self.context,186'SSL' => self.ssl,187'SSLVersion' => self.ssl_version,188'Proxies' => self.proxies,189'Timeout' => timeout,190'Comm' => self.comm191)192end193194#195# Closes the connection to the remote server.196#197def close198if self.conn && !self.conn.closed?199self.conn.shutdown200self.conn.close201end202203self.conn = nil204self.ntlm_client = nil205end206207#208# Sends a request and gets a response back209#210# If the request is a 401, and we have creds, it will attempt to complete211# authentication and return the final response212#213# @return (see #_send_recv)214def send_recv(req, t = -1, persist = false)215res = _send_recv(req, t, persist)216if res and res.code == 401 and res.headers['WWW-Authenticate']217res = send_auth(res, req.opts, t, persist)218end219res220end221222#223# Transmit an HTTP request and receive the response224#225# If persist is set, then the request will attempt to reuse an existing226# connection.227#228# Call this directly instead of {#send_recv} if you don't want automatic229# authentication handling.230#231# @return (see #read_response)232def _send_recv(req, t = -1, persist = false)233@pipeline = persist234subscriber.on_request(req)235if req.respond_to?(:opts) && req.opts['ntlm_transform_request'] && self.ntlm_client236req = req.opts['ntlm_transform_request'].call(self.ntlm_client, req)237elsif req.respond_to?(:opts) && req.opts['krb_transform_request'] && self.krb_encryptor238req = req.opts['krb_transform_request'].call(self.krb_encryptor, req)239end240241send_request(req, t)242243res = read_response(t, :original_request => req)244if req.respond_to?(:opts) && req.opts['ntlm_transform_response'] && self.ntlm_client245req.opts['ntlm_transform_response'].call(self.ntlm_client, res)246elsif req.respond_to?(:opts) && req.opts['krb_transform_response'] && self.krb_encryptor247req = req.opts['krb_transform_response'].call(self.krb_encryptor, res)248end249res.request = req.to_s if res250res.peerinfo = peerinfo if res251subscriber.on_response(res)252res253end254255#256# Send an HTTP request to the server257#258# @param req [Request,ClientRequest,#to_s] The request to send259# @param t (see #connect)260#261# @return [void]262def send_request(req, t = -1)263connect(t)264conn.put(req.to_s)265end266267# Resends an HTTP Request with the proper authentication headers268# set. If we do not support the authentication type the server requires269# we return the original response object270#271# @param res [Response] the HTTP Response object272# @param opts [Hash] the options used to generate the original HTTP request273# @param t [Integer] the timeout for the request in seconds274# @param persist [Boolean] whether or not to persist the TCP connection (pipelining)275#276# @return [Response] the last valid HTTP response object we received277def send_auth(res, opts, t, persist)278if opts['username'].nil? or opts['username'] == ''279if self.username and not (self.username == '')280opts['username'] = self.username281opts['password'] = self.password282else283opts['username'] = nil284opts['password'] = nil285end286end287288if opts[:kerberos_authenticator].nil?289opts[:kerberos_authenticator] = self.kerberos_authenticator290end291292return res if (opts['username'].nil? or opts['username'] == '') and opts[:kerberos_authenticator].nil?293supported_auths = res.headers['WWW-Authenticate']294295# if several providers are available, the client may want one in particular296preferred_auth = opts['preferred_auth']297298if supported_auths.include?('Basic') && (preferred_auth.nil? || preferred_auth == 'Basic')299opts['headers'] ||= {}300opts['headers']['Authorization'] = basic_auth_header(opts['username'],opts['password'] )301req = request_cgi(opts)302res = _send_recv(req,t,persist)303return res304elsif supported_auths.include?('Digest') && (preferred_auth.nil? || preferred_auth == 'Digest')305temp_response = digest_auth(opts)306if temp_response.kind_of? Rex::Proto::Http::Response307res = temp_response308end309return res310elsif supported_auths.include?('NTLM') && (preferred_auth.nil? || preferred_auth == 'NTLM')311opts['provider'] = 'NTLM'312temp_response = negotiate_auth(opts)313if temp_response.kind_of? Rex::Proto::Http::Response314res = temp_response315end316return res317elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Negotiate')318opts['provider'] = 'Negotiate'319temp_response = negotiate_auth(opts)320if temp_response.kind_of? Rex::Proto::Http::Response321res = temp_response322end323return res324elsif supported_auths.include?('Negotiate') && (preferred_auth.nil? || preferred_auth == 'Kerberos')325opts['provider'] = 'Negotiate'326temp_response = kerberos_auth(opts)327if temp_response.kind_of? Rex::Proto::Http::Response328res = temp_response329end330return res331end332return res333end334335# Converts username and password into the HTTP Basic authorization336# string.337#338# @return [String] A value suitable for use as an Authorization header339def basic_auth_header(username,password)340auth_str = username.to_s + ":" + password.to_s341auth_str = "Basic " + Rex::Text.encode_base64(auth_str)342end343344345def make_cnonce346Digest::MD5.hexdigest "%x" % (::Time.now.to_i + rand(65535))347end348349# Send a series of requests to complete Digest Authentication350#351# @param opts [Hash] the options used to build an HTTP request352# @return [Response] the last valid HTTP response we received353def digest_auth(opts={})354cnonce = make_cnonce355nonce_count = 0356357to = opts['timeout'] || 20358359digest_user = opts['username'] || ""360digest_password = opts['password'] || ""361362method = opts['method']363path = opts['uri']364iis = true365if (opts['DigestAuthIIS'] == false or self.config['DigestAuthIIS'] == false)366iis = false367end368369begin370nonce_count += 1371372resp = opts['response']373374if not resp375# Get authentication-challenge from server, and read out parameters required376r = request_cgi(opts.merge({377'uri' => path,378'method' => method }))379resp = _send_recv(r, to)380unless resp.kind_of? Rex::Proto::Http::Response381return nil382end383384if resp.code != 401385return resp386end387return resp unless resp.headers['WWW-Authenticate']388end389390# Don't anchor this regex to the beginning of string because header391# folding makes it appear later when the server presents multiple392# WWW-Authentication options (such as is the case with IIS configured393# for Digest or NTLM).394resp['www-authenticate'] =~ /Digest (.*)/395396parameters = {}397$1.split(/,[[:space:]]*/).each do |p|398k, v = p.split("=", 2)399parameters[k] = v.gsub('"', '')400end401402qop = parameters['qop']403404if parameters['algorithm'] =~ /(.*?)(-sess)?$/405algorithm = case $1406when 'MD5' then Digest::MD5407when 'SHA1' then Digest::SHA1408when 'SHA2' then Digest::SHA2409when 'SHA256' then Digest::SHA256410when 'SHA384' then Digest::SHA384411when 'SHA512' then Digest::SHA512412when 'RMD160' then Digest::RMD160413else raise Error, "unknown algorithm \"#{$1}\""414end415algstr = parameters["algorithm"]416sess = $2417else418algorithm = Digest::MD5419algstr = "MD5"420sess = false421end422423a1 = if sess then424[425algorithm.hexdigest("#{digest_user}:#{parameters['realm']}:#{digest_password}"),426parameters['nonce'],427cnonce428].join ':'429else430"#{digest_user}:#{parameters['realm']}:#{digest_password}"431end432433ha1 = algorithm.hexdigest(a1)434ha2 = algorithm.hexdigest("#{method}:#{path}")435436request_digest = [ha1, parameters['nonce']]437request_digest.push(('%08x' % nonce_count), cnonce, qop) if qop438request_digest << ha2439request_digest = request_digest.join ':'440441# Same order as IE7442auth = [443"Digest username=\"#{digest_user}\"",444"realm=\"#{parameters['realm']}\"",445"nonce=\"#{parameters['nonce']}\"",446"uri=\"#{path}\"",447"cnonce=\"#{cnonce}\"",448"nc=#{'%08x' % nonce_count}",449"algorithm=#{algstr}",450"response=\"#{algorithm.hexdigest(request_digest)[0, 32]}\"",451# The spec says the qop value shouldn't be enclosed in quotes, but452# some versions of IIS require it and Apache accepts it. Chrome453# and Firefox both send it without quotes but IE does it this way.454# Use the non-compliant-but-everybody-does-it to be as compatible455# as possible by default. The user can override if they don't like456# it.457if qop.nil? then458elsif iis then459"qop=\"#{qop}\""460else461"qop=#{qop}"462end,463if parameters.key? 'opaque' then464"opaque=\"#{parameters['opaque']}\""465end466].compact467468headers ={ 'Authorization' => auth.join(', ') }469headers.merge!(opts['headers']) if opts['headers']470471# Send main request with authentication472r = request_cgi(opts.merge({473'uri' => path,474'method' => method,475'headers' => headers }))476resp = _send_recv(r, to, true)477unless resp.kind_of? Rex::Proto::Http::Response478return nil479end480481return resp482483rescue ::Errno::EPIPE, ::Timeout::Error484end485end486487def kerberos_auth(opts={})488to = opts['timeout'] || 20489auth_result = self.kerberos_authenticator.authenticate(mechanism: Rex::Proto::Gss::Mechanism::KERBEROS)490gss_data = auth_result[:security_blob]491gss_data_b64 = Rex::Text.encode_base64(gss_data)492493# Separate options for the auth requests494auth_opts = opts.clone495auth_opts['headers'] = opts['headers'].clone496auth_opts['headers']['Authorization'] = "Kerberos #{gss_data_b64}"497498if auth_opts['no_body_for_auth']499auth_opts.delete('data')500auth_opts.delete('krb_transform_request')501auth_opts.delete('krb_transform_response')502end503504begin505# Send the auth request506r = request_cgi(auth_opts)507resp = _send_recv(r, to)508unless resp.kind_of? Rex::Proto::Http::Response509return nil510end511512# Get the challenge and craft the response513response = resp.headers['WWW-Authenticate'].scan(/Kerberos ([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]514return resp unless response515516decoded = Rex::Text.decode_base64(response)517mutual_auth_result = self.kerberos_authenticator.parse_gss_init_response(decoded, auth_result[:session_key])518self.krb_encryptor = self.kerberos_authenticator.get_message_encryptor(mutual_auth_result[:ap_rep_subkey],519auth_result[:client_sequence_number],520mutual_auth_result[:server_sequence_number])521522if opts['no_body_for_auth']523# If the body wasn't sent in the authentication, now do the actual request524r = request_cgi(opts)525resp = _send_recv(r, to, true)526end527return resp528529rescue ::Errno::EPIPE, ::Timeout::Error530return nil531end532end533534#535# Builds a series of requests to complete Negotiate Auth. Works essentially536# the same way as Digest auth. Same pipelining concerns exist.537#538# @option opts (see #send_request_cgi)539# @option opts provider ["Negotiate","NTLM"] What Negotiate provider to use540#541# @return [Response] the last valid HTTP response we received542def negotiate_auth(opts={})543544to = opts['timeout'] || 20545opts['username'] ||= ''546opts['password'] ||= ''547548if opts['provider'] and opts['provider'].include? 'Negotiate'549provider = "Negotiate "550else551provider = "NTLM "552end553554opts['method']||= 'GET'555opts['headers']||= {}556557workstation_name = Rex::Text.rand_text_alpha(rand(8)+6)558domain_name = self.config['domain']559560ntlm_client = ::Net::NTLM::Client.new(561opts['username'],562opts['password'],563workstation: workstation_name,564domain: domain_name,565)566type1 = ntlm_client.init_context567568begin569# Separate options for the auth requests570auth_opts = opts.clone571auth_opts['headers'] = opts['headers'].clone572auth_opts['headers']['Authorization'] = provider + type1.encode64573574if auth_opts['no_body_for_auth']575auth_opts.delete('data')576auth_opts.delete('ntlm_transform_request')577auth_opts.delete('ntlm_transform_response')578end579580# First request to get the challenge581r = request_cgi(auth_opts)582resp = _send_recv(r, to)583unless resp.kind_of? Rex::Proto::Http::Response584return nil585end586587return resp unless resp.code == 401 && resp.headers['WWW-Authenticate']588589# Get the challenge and craft the response590ntlm_challenge = resp.headers['WWW-Authenticate'].scan(/#{provider}([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]591return resp unless ntlm_challenge592593ntlm_message_3 = ntlm_client.init_context(ntlm_challenge, channel_binding)594595self.ntlm_client = ntlm_client596# Send the response597auth_opts['headers']['Authorization'] = "#{provider}#{ntlm_message_3.encode64}"598r = request_cgi(auth_opts)599resp = _send_recv(r, to, true)600601unless resp.kind_of? Rex::Proto::Http::Response602return nil603end604if opts['no_body_for_auth']605# If the body wasn't sent in the authentication, now do the actual request606r = request_cgi(opts)607resp = _send_recv(r, to, true)608end609return resp610611rescue ::Errno::EPIPE, ::Timeout::Error612return nil613end614end615616def channel_binding617if !self.conn.respond_to?(:peer_cert) or self.conn.peer_cert.nil?618nil619else620Net::NTLM::ChannelBinding.create(OpenSSL::X509::Certificate.new(self.conn.peer_cert))621end622end623624# Read a response from the server625#626# Wait at most t seconds for the full response to be read in.627# If t is specified as a negative value, it indicates an indefinite wait cycle.628# If t is specified as nil or 0, it indicates no response parsing is required.629#630# @return [Response]631def read_response(t = -1, opts = {})632# Return a nil response if timeout is nil or 0633return if t.nil? || t == 0634635resp = Response.new636resp.max_data = config['read_max_data']637638original_request = opts.fetch(:original_request) { nil }639parse_opts = {}640unless original_request.nil?641parse_opts = { :orig_method => original_request.opts['method'] }642end643644Timeout.timeout((t < 0) ? nil : t) do645646rv = nil647while (648not conn.closed? and649rv != Packet::ParseCode::Completed and650rv != Packet::ParseCode::Error651)652653begin654655buff = conn.get_once(resp.max_data, 1)656rv = resp.parse(buff || '', parse_opts)657658# Handle unexpected disconnects659rescue ::Errno::EPIPE, ::EOFError, ::IOError660case resp.state661when Packet::ParseState::ProcessingHeader662resp = nil663when Packet::ParseState::ProcessingBody664# truncated request, good enough665resp.error = :truncated666end667break668end669670# This is a dirty hack for broken HTTP servers671if rv == Packet::ParseCode::Completed672rbody = resp.body673rbufq = resp.bufq674675rblob = rbody.to_s + rbufq.to_s676tries = 0677begin678# XXX: This doesn't deal with chunked encoding679while tries < 1000 and resp.headers["Content-Type"] and resp.headers["Content-Type"].start_with?('text/html') and rblob !~ /<\/html>/i680buff = conn.get_once(-1, 0.05)681break if not buff682rblob += buff683tries += 1684end685rescue ::Errno::EPIPE, ::EOFError, ::IOError686end687688resp.bufq = ""689resp.body = rblob690end691end692end693694return resp if not resp695696# As a last minute hack, we check to see if we're dealing with a 100 Continue here.697# Most of the time this is handled by the parser via check_100()698if resp.proto == '1.1' and resp.code == 100 and not opts[:skip_100]699# Read the real response from the body if we found one700# If so, our real response became the body, so we re-parse it.701if resp.body.to_s =~ /^HTTP/702body = resp.body703resp = Response.new704resp.max_data = config['read_max_data']705rv = resp.parse(body, parse_opts)706# We found a 100 Continue but didn't read the real reply yet707# Otherwise reread the reply, but don't try this hack again708else709resp = read_response(t, :skip_100 => true)710end711end712713resp714rescue Timeout::Error715# Allow partial response due to timeout716resp if config['partial']717end718719#720# Cleans up any outstanding connections and other resources.721#722def stop723close724end725726#727# Returns whether or not the conn is valid.728#729def conn?730conn != nil731end732733#734# Whether or not connections should be pipelined.735#736def pipelining?737pipeline738end739740#741# Target host addr and port for this connection742#743def peerinfo744if self.conn745pi = self.conn.peerinfo || nil746if pi747return {748'addr' => pi.split(':')[0],749'port' => pi.split(':')[1].to_i750}751end752end753nil754end755756#757# An optional comm to use for creating the underlying socket.758#759attr_accessor :comm760#761# The client request configuration762#763attr_accessor :config764#765# The client request configuration classes766#767attr_accessor :config_types768#769# Whether or not pipelining is in use.770#771attr_accessor :pipeline772#773# The local host of the client.774#775attr_accessor :local_host776#777# The local port of the client.778#779attr_accessor :local_port780#781# The underlying connection.782#783attr_accessor :conn784#785# The calling context to pass to the socket786#787attr_accessor :context788#789# The proxy list790#791attr_accessor :proxies792793# Auth794attr_accessor :username, :password, :kerberos_authenticator795796# When parsing the request, thunk off the first response from the server, since junk797attr_accessor :junk_pipeline798799# @return [Rex::Proto::Http::HttpSubscriber] The HTTP subscriber800attr_accessor :subscriber801802protected803804# https805attr_accessor :ssl, :ssl_version # :nodoc:806807attr_accessor :hostname, :port # :nodoc:808809#810# The established NTLM connection info811#812attr_accessor :ntlm_client813814#815# The established kerberos connection info816#817attr_accessor :krb_encryptor818end819820end821end822end823824825