class Rex::Proto::Http::Client

Acts as a client to an HTTP server, sending requests and receiving responses.

See the RFC: www.w3.org/Protocols/rfc2616/rfc2616.html

Constants

DefaultUserAgent

Attributes

config[RW]

The client request configuration

config_types[RW]

The client request configuration classes

conn[RW]

The underlying connection.

context[RW]

The calling context to pass to the socket

junk_pipeline[RW]

When parsing the request, thunk off the first response from the server, since junk

local_host[RW]

The local host of the client.

local_port[RW]

The local port of the client.

password[RW]

Auth

pipeline[RW]

Whether or not pipelining is in use.

proxies[RW]

The proxy list

username[RW]

Auth

Public Class Methods

new(host, port = 80, context = {}, ssl = nil, ssl_version = nil, proxies = nil, username = '', password = '') click to toggle source

Creates a new client instance

# File lib/rex/proto/http/client.rb, line 30
def initialize(host, port = 80, context = {}, ssl = nil, ssl_version = nil, proxies = nil, username = '', password = '')
  self.hostname = host
  self.port     = port.to_i
  self.context  = context
  self.ssl      = ssl
  self.ssl_version = ssl_version
  self.proxies  = proxies
  self.username = username
  self.password = password

  # Take ClientRequest's defaults, but override with our own
  self.config = Http::ClientRequest::DefaultConfig.merge({
    'read_max_data'   => (1024*1024*1),
    'vhost'           => self.hostname,
  })

  # XXX: This info should all be controlled by ClientRequest
  self.config_types = {
    'uri_encode_mode'        => ['hex-normal', 'hex-all', 'hex-random', 'u-normal', 'u-random', 'u-all'],
    'uri_encode_count'       => 'integer',
    'uri_full_url'           => 'bool',
    'pad_method_uri_count'   => 'integer',
    'pad_uri_version_count'  => 'integer',
    'pad_method_uri_type'    => ['space', 'tab', 'apache'],
    'pad_uri_version_type'   => ['space', 'tab', 'apache'],
    'method_random_valid'    => 'bool',
    'method_random_invalid'  => 'bool',
    'method_random_case'     => 'bool',
    'version_random_valid'   => 'bool',
    'version_random_invalid' => 'bool',
    'version_random_case'    => 'bool',
    'uri_dir_self_reference' => 'bool',
    'uri_dir_fake_relative'  => 'bool',
    'uri_use_backslashes'    => 'bool',
    'pad_fake_headers'       => 'bool',
    'pad_fake_headers_count' => 'integer',
    'pad_get_params'         => 'bool',
    'pad_get_params_count'   => 'integer',
    'pad_post_params'        => 'bool',
    'pad_post_params_count'  => 'integer',
    'uri_fake_end'           => 'bool',
    'uri_fake_params_start'  => 'bool',
    'header_folding'         => 'bool',
    'chunked_size'           => 'integer'
  }


end

Public Instance Methods

_send_recv(req, t = -1, persist=false) click to toggle source

Transmit an HTTP request and receive the response

If persist is set, then the request will attempt to reuse an existing connection.

Call this directly instead of {#send_recv} if you don’t want automatic authentication handling.

@return (see read_response)

# File lib/rex/proto/http/client.rb, line 231
def _send_recv(req, t = -1, persist=false)
  @pipeline = persist
  send_request(req, t)
  res = read_response(t)
  res.request = req.to_s if res
  res
end
basic_auth_header(username,password) click to toggle source

Converts username and password into the HTTP Basic authorization string.

@return [String] A value suitable for use as an Authorization header

# File lib/rex/proto/http/client.rb, line 308
def basic_auth_header(username,password)
  auth_str = username.to_s + ":" + password.to_s
  auth_str = "Basic " + Rex::Text.encode_base64(auth_str)
end
close() click to toggle source

Closes the connection to the remote server.

# File lib/rex/proto/http/client.rb, line 197
def close
  if (self.conn)
    self.conn.shutdown
    self.conn.close unless self.conn.closed?
  end

  self.conn = nil
end
conn?() click to toggle source

Returns whether or not the conn is valid.

# File lib/rex/proto/http/client.rb, line 658
def conn?
  conn != nil
end
connect(t = -1) click to toggle source

Connects to the remote server if possible.

@param t [Fixnum] Timeout @see Rex::Socket::Tcp.create @return [Rex::Socket::Tcp]

# File lib/rex/proto/http/client.rb, line 169
def connect(t = -1)
  # If we already have a connection and we aren't pipelining, close it.
  if (self.conn)
    if !pipelining?
      close
    else
      return self.conn
    end
  end

  timeout = (t.nil? or t == -1) ? 0 : t

  self.conn = Rex::Socket::Tcp.create(
    'PeerHost'   => self.hostname,
    'PeerPort'   => self.port.to_i,
    'LocalHost'  => self.local_host,
    'LocalPort'  => self.local_port,
    'Context'    => self.context,
    'SSL'        => self.ssl,
    'SSLVersion' => self.ssl_version,
    'Proxies'    => self.proxies,
    'Timeout'    => timeout
  )
end
digest_auth(opts={}) click to toggle source

Send a series of requests to complete Digest Authentication

@param opts [Hash] the options used to build an HTTP request

@return [Response] the last valid HTTP response we received

# File lib/rex/proto/http/client.rb, line 318
def digest_auth(opts={})
  @nonce_count = 0

  to = opts['timeout'] || 20

  digest_user = opts['username'] || ""
  digest_password =  opts['password'] || ""

  method = opts['method']
  path = opts['uri']
  iis = true
  if (opts['DigestAuthIIS'] == false or self.config['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
    r = request_cgi(opts.merge({
        'uri' => path,
        'method' => method }))
    resp = _send_recv(r, to)
    unless resp.kind_of? Rex::Proto::Http::Response
      return nil
    end

    if resp.code != 401
      return resp
    end
    return resp 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 = request_cgi(opts.merge({
    'uri' => path,
    'method' => method,
    'headers' => headers }))
  resp = _send_recv(r, to, true)
  unless resp.kind_of? Rex::Proto::Http::Response
    return nil
  end

  return resp

  rescue ::Errno::EPIPE, ::Timeout::Error
  end
end
negotiate_auth(opts={}) click to toggle source

Builds a series of requests to complete Negotiate Auth. Works essentially the same way as Digest auth. Same pipelining concerns exist.

@option opts (see send_request_cgi) @option opts provider [“Negotiate”,“NTLM”] What Negotiate provider to use

@return [Response] the last valid HTTP response we received

# File lib/rex/proto/http/client.rb, line 459
def negotiate_auth(opts={})
  ntlm_options = {
    :signing          => false,
    :usentlm2_session => self.config['usentlm2_session'],
    :use_ntlmv2       => self.config['use_ntlmv2'],
    :send_lm          => self.config['send_lm'],
    :send_ntlm        => self.config['send_ntlm']
  }

  to = opts['timeout'] || 20
  opts['username'] ||= ''
  opts['password'] ||= ''

  if opts['provider'] and opts['provider'].include? 'Negotiate'
    provider = "Negotiate "
  else
    provider = 'NTLM '
  end

  opts['method']||= 'GET'
  opts['headers']||= {}

  ntlmssp_flags = ::Rex::Proto::NTLM::Utils.make_ntlm_flags(ntlm_options)
  workstation_name = Rex::Text.rand_text_alpha(rand(8)+6)
  domain_name = self.config['domain']

  b64_blob = Rex::Text::encode_base64(
    ::Rex::Proto::NTLM::Utils::make_ntlmssp_blob_init(
      domain_name,
      workstation_name,
      ntlmssp_flags
  ))

  ntlm_message_1 = provider + b64_blob

  begin
    # First request to get the challenge
    opts['headers']['Authorization'] = ntlm_message_1
    r = request_cgi(opts)
    resp = _send_recv(r, to)
    unless resp.kind_of? Rex::Proto::Http::Response
      return nil
    end

    return resp unless resp.code == 401 && resp.headers['WWW-Authenticate']

    # Get the challenge and craft the response
    ntlm_challenge = resp.headers['WWW-Authenticate'].scan(/#{provider}([A-Z0-9\x2b\x2f=]+)/ni).flatten[0]
    return resp unless ntlm_challenge

    ntlm_message_2 = Rex::Text::decode_base64(ntlm_challenge)
    blob_data = ::Rex::Proto::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
    default_name         = blob_data[:default_name]         || '' #netbios name
    default_domain       = blob_data[:default_domain]       || '' #netbios domain
    dns_host_name        = blob_data[:dns_host_name]        || '' #dns name
    dns_domain_name      = blob_data[:dns_domain_name]      || '' #dns domain
    chall_MsvAvTimestamp = blob_data[:chall_MsvAvTimestamp] || '' #Client time

    spnopt = {:use_spn => self.config['SendSPN'], :name =>  self.hostname}

    resp_lm, resp_ntlm, client_challenge, ntlm_cli_challenge = ::Rex::Proto::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 = ::Rex::Proto::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
    opts['headers']['Authorization'] = "#{provider}#{ntlm_message_3}"
    r = request_cgi(opts)
    resp = _send_recv(r, to, true)
    unless resp.kind_of? Rex::Proto::Http::Response
      return nil
    end
    return resp

  rescue ::Errno::EPIPE, ::Timeout::Error
    return nil
  end
end
pipelining?() click to toggle source

Whether or not connections should be pipelined.

# File lib/rex/proto/http/client.rb, line 665
def pipelining?
  pipeline
end
read_response(t = -1, opts = {}) click to toggle source

Read a response from the server

@return [Response]

# File lib/rex/proto/http/client.rb, line 565
def read_response(t = -1, opts = {})

  resp = Response.new
  resp.max_data = config['read_max_data']

  # Wait at most t seconds for the full response to be read in.  We only
  # do this if t was specified as a negative value indicating an infinite
  # wait cycle.  If t were specified as nil it would indicate that no
  # response parsing is required.

  return resp if not t

  Timeout.timeout((t < 0) ? nil : t) do

    rv = nil
    while (
             rv != Packet::ParseCode::Completed and
             rv != Packet::ParseCode::Error
            )

      begin

        buff = conn.get_once(-1, 1)
        rv   = resp.parse( buff || '' )

      # Handle unexpected disconnects
      rescue ::Errno::EPIPE, ::EOFError, ::IOError
        case resp.state
        when Packet::ParseState::ProcessingHeader
          resp = nil
        when Packet::ParseState::ProcessingBody
          # truncated request, good enough
          resp.error = :truncated
        end
        break
      end

      # This is a dirty hack for broken HTTP servers
      if rv == Packet::ParseCode::Completed
        rbody = resp.body
        rbufq = resp.bufq

        rblob = rbody.to_s + rbufq.to_s
        tries = 0
        begin
          # XXX: This doesn't deal with chunked encoding or "Content-type: text/html; charset=..."
          while tries < 1000 and resp.headers["Content-Type"]== "text/html" and rblob !~ /<\/html>/i
            buff = conn.get_once(-1, 0.05)
            break if not buff
            rblob += buff
            tries += 1
          end
        rescue ::Errno::EPIPE, ::EOFError, ::IOError
        end

        resp.bufq = ""
        resp.body = rblob
      end
    end
  end

  return resp if not resp

  # As a last minute hack, we check to see if we're dealing with a 100 Continue here.
  # Most of the time this is handled by the parser via check_100()
  if resp.proto == '1.1' and resp.code == 100 and not opts[:skip_100]
    # Read the real response from the body if we found one
    # If so, our real response became the body, so we re-parse it.
    if resp.body.to_s =~ /^HTTP/
      body = resp.body
      resp = Response.new
      resp.max_data = config['read_max_data']
      rv = resp.parse(body)
    # We found a 100 Continue but didn't read the real reply yet
    # Otherwise reread the reply, but don't try this hack again
    else
      resp = read_response(t, :skip_100 => true)
    end
  end

  resp
end
request_cgi(opts={}) click to toggle source

Create a CGI compatible request

@param (see request_raw) @option opts (see request_raw) @option opts ‘ctype’ [String] Content-Type header value, default: application/x-www-form-urlencoded @option opts ‘encode_params’ [Bool] URI encode the GET or POST variables (names and values), default: true @option opts ‘vars_get’ [Hash] GET variables as a hash to be translated into a query string @option opts ‘vars_post’ [Hash] POST variables as a hash to be translated into POST data

@return [ClientRequest]

# File lib/rex/proto/http/client.rb, line 151
def request_cgi(opts={})
  opts = self.config.merge(opts)

  opts['ctype']       ||= 'application/x-www-form-urlencoded'
  opts['ssl']         = self.ssl
  opts['cgi']         = true
  opts['port']        = self.port

  req = ClientRequest.new(opts)
  req
end
request_raw(opts={}) click to toggle source

Create an arbitrary HTTP request

@param opts [Hash] @option opts ‘agent’ [String] User-Agent header value @option opts ‘connection’ [String] Connection header value @option opts ‘cookie’ [String] Cookie header value @option opts ‘data’ [String] HTTP data (only useful with some methods, see rfc2616) @option opts ‘encode’ [Bool] URI encode the supplied URI, default: false @option opts ‘headers’ [Hash] HTTP headers, e.g. { "X-MyHeader" => "value" } @option opts ‘method’ [String] HTTP method to use in the request, not limited to standard methods defined by rfc2616, default: GET @option opts ‘proto’ [String] protocol, default: HTTP @option opts ‘query’ [String] raw query string @option opts ‘raw_headers’ [Hash] HTTP headers @option opts ‘uri’ [String] the URI to request @option opts ‘version’ [String] version of the protocol, default: 1.1 @option opts ‘vhost’ [String] Host header value

@return [ClientRequest]

# File lib/rex/proto/http/client.rb, line 129
def request_raw(opts={})
  opts = self.config.merge(opts)

  opts['ssl']         = self.ssl
  opts['cgi']         = false
  opts['port']        = self.port

  req = ClientRequest.new(opts)
end
send_auth(res, opts, t, persist) click to toggle source

Resends an HTTP Request with the propper authentcation headers set. If we do not support the authentication type the server requires we return the original response object

@param res [Response] the HTTP Response object @param opts [Hash] the options used to generate the original HTTP request @param t [Fixnum] the timeout for the request in seconds @param persist [Boolean] whether or not to persist the TCP connection (pipelining)

@return [Response] the last valid HTTP response object we received

# File lib/rex/proto/http/client.rb, line 261
def send_auth(res, opts, t, persist)
  if opts['username'].nil? or opts['username'] == ''
    if self.username and not (self.username == '')
      opts['username'] = self.username
      opts['password'] = self.password
    else
      opts['username'] = nil
      opts['password'] = nil
    end
  end

  return res if opts['username'].nil? or opts['username'] == ''
  supported_auths = res.headers['WWW-Authenticate']
  if supported_auths.include? 'Basic'
    opts['headers'] ||= {}
    opts['headers']['Authorization'] = basic_auth_header(opts['username'],opts['password'] )
    req = request_cgi(opts)
    res = _send_recv(req,t,persist)
    return res
  elsif  supported_auths.include? "Digest"
    temp_response = digest_auth(opts)
    if temp_response.kind_of? Rex::Proto::Http::Response
      res = temp_response
    end
    return res
  elsif supported_auths.include? "NTLM"
    opts['provider'] = 'NTLM'
    temp_response = negotiate_auth(opts)
    if temp_response.kind_of? Rex::Proto::Http::Response
      res = temp_response
    end
    return res
  elsif supported_auths.include? "Negotiate"
    opts['provider'] = 'Negotiate'
    temp_response = negotiate_auth(opts)
    if temp_response.kind_of? Rex::Proto::Http::Response
      res = temp_response
    end
    return res
  end
  return res
end
send_recv(req, t = -1, persist=false) click to toggle source

Sends a request and gets a response back

If the request is a 401, and we have creds, it will attempt to complete authentication and return the final response

@return (see #_send_recv)

# File lib/rex/proto/http/client.rb, line 213
def send_recv(req, t = -1, persist=false)
  res = _send_recv(req,t,persist)
  if res and res.code == 401 and res.headers['WWW-Authenticate']
    res = send_auth(res, req.opts, t, persist)
  end
  res
end
send_request(req, t = -1) click to toggle source

Send an HTTP request to the server

@param req [Request,ClientRequest,#to_s] The request to send @param t (see connect)

@return [void]

# File lib/rex/proto/http/client.rb, line 246
def send_request(req, t = -1)
  connect(t)
  conn.put(req.to_s)
end
set_config(opts = {}) click to toggle source

Set configuration options

# File lib/rex/proto/http/client.rb, line 82
def set_config(opts = {})
  opts.each_pair do |var,val|
    # Default type is string
    typ = self.config_types[var] || 'string'

    # These are enum types
    if(typ.class.to_s == 'Array')
      if not typ.include?(val)
        raise RuntimeError, "The specified value for #{var} is not one of the valid choices"
      end
    end

    # The caller should have converted these to proper ruby types, but
    # take care of the case where they didn't before setting the
    # config.

    if(typ == 'bool')
      val = (val =~ /^(t|y|1)$/i ? true : false || val === true)
    end

    if(typ == 'integer')
      val = val.to_i
    end

    self.config[var]=val
  end
end
stop() click to toggle source

Cleans up any outstanding connections and other resources.

# File lib/rex/proto/http/client.rb, line 651
def stop
  close
end