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_request.rb
Views: 11704
# -*- coding: binary -*-1require 'uri'23require 'rex/mime'4require 'rex/socket'5require 'rex/text'67require 'pp'89module Rex10module Proto11module Http1213class ClientRequest1415DefaultConfig = {16#17# Regular HTTP stuff18#19'agent' => nil,20'cgi' => true,21'cookie' => nil,22'data' => '',23'headers' => nil,24'raw_headers' => '',25'method' => 'GET',26'partial' => false,27'path_info' => '',28'port' => 80,29'proto' => 'HTTP',30'query' => '',31'ssl' => false,32'uri' => '/',33'vars_get' => {},34'vars_post' => {},35'vars_form_data' => [],36'version' => '1.1',37'vhost' => nil,38'ssl_server_name_indication' => nil,3940#41# Evasion options42#43'encode_params' => true,44'encode' => false,45'uri_encode_mode' => 'hex-normal', # hex-normal, hex-all, hex-noslashes, hex-random, u-normal, u-all, u-noslashes, u-random46'uri_encode_count' => 1, # integer47'uri_full_url' => false, # bool48'pad_method_uri_count' => 1, # integer49'pad_uri_version_count' => 1, # integer50'pad_method_uri_type' => 'space', # space, tab, apache51'pad_uri_version_type' => 'space', # space, tab, apache52'method_random_valid' => false, # bool53'method_random_invalid' => false, # bool54'method_random_case' => false, # bool55'version_random_valid' => false, # bool56'version_random_invalid' => false, # bool57'uri_dir_self_reference' => false, # bool58'uri_dir_fake_relative' => false, # bool59'uri_use_backslashes' => false, # bool60'pad_fake_headers' => false, # bool61'pad_fake_headers_count' => 16, # integer62'pad_get_params' => false, # bool63'pad_get_params_count' => 8, # integer64'pad_post_params' => false, # bool65'pad_post_params_count' => 8, # integer66'uri_fake_end' => false, # bool67'uri_fake_params_start' => false, # bool68'shuffle_get_params' => false, # bool69'shuffle_post_params' => false, # bool70'header_folding' => false, # bool71'chunked_size' => 0, # integer7273#74# NTLM Options75#76'usentlm2_session' => true,77'use_ntlmv2' => true,78'send_lm' => true,79'send_ntlm' => true,80'SendSPN' => true,81'UseLMKey' => false,82'domain' => 'WORKSTATION',83#84# Digest Options85#86'DigestAuthIIS' => true87}8889attr_reader :opts9091def initialize(opts={})92@opts = DefaultConfig.merge(opts)93@opts['agent'] ||= Rex::UserAgent.session_agent94@opts['headers'] ||= {}95end9697def to_s(headers_only: false)98# Start GET query string99qstr = opts['query'] ? opts['query'].dup : ""100101# Start POST data string102pstr = opts['data'] ? opts['data'].dup : ""103104ctype = opts['ctype']105106if opts['cgi']107uri_str = set_uri108109if (opts['pad_get_params'])1101.upto(opts['pad_get_params_count'].to_i) do |i|111qstr << '&' if qstr.length > 0112qstr << set_encode_uri(Rex::Text.rand_text_alphanumeric(rand(32)+1))113qstr << '='114qstr << set_encode_uri(Rex::Text.rand_text_alphanumeric(rand(32)+1))115end116end117if opts.key?("vars_get") && opts['vars_get']118opts['vars_get'] = Hash[opts['vars_get'].to_a.shuffle] if (opts['shuffle_get_params'])119120opts['vars_get'].each_pair do |var,val|121var = var.to_s122123qstr << '&' if qstr.length > 0124qstr << (opts['encode_params'] ? set_encode_uri(var) : var)125# support get parameter without value126# Example: uri?parameter127if val128val = val.to_s129qstr << '='130qstr << (opts['encode_params'] ? set_encode_uri(val) : val)131end132end133end134if (opts['pad_post_params'])1351.upto(opts['pad_post_params_count'].to_i) do |i|136rand_var = Rex::Text.rand_text_alphanumeric(rand(32)+1)137rand_val = Rex::Text.rand_text_alphanumeric(rand(32)+1)138pstr << '&' if pstr.length > 0139pstr << (opts['encode_params'] ? set_encode_uri(rand_var) : rand_var)140pstr << '='141pstr << (opts['encode_params'] ? set_encode_uri(rand_val) : rand_val)142end143end144145opts['vars_post'] = Hash[opts['vars_post'].to_a.shuffle] if (opts['shuffle_post_params'])146147opts['vars_post'].each_pair do |var,val|148var = var.to_s149unless val.is_a?(Array)150val = [val]151end152val.each do |v|153v = v.to_s154pstr << '&' if pstr.length > 0155pstr << (opts['encode_params'] ? set_encode_uri(var) : var)156pstr << '='157pstr << (opts['encode_params'] ? set_encode_uri(v) : v)158end159end160161if opts['vars_form_data'] && opts['vars_form_data'].length > 0162unless opts['vars_form_data'].is_a?(::Array)163raise ::ArgumentError, "request_cgi: The provided `form_data` option is not valid. Expected: Array, Got: #{opts['form_data'].class}"164end165166form_data = Rex::MIME::Message.new167# Initialize or reuse the previous form data boundary to ensure idempotency168opts['vars_form_data_boundary'] ||= form_data.bound169form_data.bound = opts['vars_form_data_boundary']170171opts['vars_form_data'].each do |field_hash|172# The name of the HTTP form field173field_name = field_hash.fetch('name', nil)174unless field_name.is_a?(::String) || field_name == nil175raise ::ArgumentError, "to_s: The provided field `name` option is not valid. Expected: String, Got: #{field_name.class}"176end177178mime_type = field_hash.fetch('content_type', nil)179encoding = field_hash.fetch('encoding', nil)180181file_contents = get_file_data(field_hash['data'])182filename = field_hash.fetch('filename') { get_filename(field_hash['data']) }183184content_disposition = 'form-data'185content_disposition << "; name=\"#{field_name}\"" if field_name186# NOTE: The file name is intentionally unescaped, as exploits such as playsms_filename_exec embed payloads into the file name which shouldn't be escaped187content_disposition << "; filename=\"#{filename}\"" if filename188189form_data.add_part(file_contents, mime_type, encoding, content_disposition)190end191192pstr += form_data.to_s193end194195ctype ||= "multipart/form-data; boundary=#{opts['vars_form_data_boundary']}" if opts['vars_form_data_boundary']196ctype ||= 'application/x-www-form-urlencoded' if opts['method'] == 'POST'197else198if opts['encode']199qstr = set_encode_uri(qstr)200end201uri_str = set_uri202end203204req = ''205req << set_method206req << set_method_uri_spacer()207req << set_uri_prepend()208209if opts['encode']210req << set_encode_uri(uri_str)211else212req << uri_str213end214215216if (qstr.length > 0)217req << '?'218req << qstr219end220221req << set_path_info222req << set_uri_append()223req << set_uri_version_spacer()224req << set_version225226# Set a default Host header if one wasn't passed in227unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?('host')228req << set_host_header229end230231# If an explicit User-Agent header is set, then use that instead of232# the default233unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?('user-agent')234req << set_agent_header235end236237# Similar to user-agent, only add an automatic auth header if a238# manual one hasn't been provided239unless opts['headers'] && opts['headers'].keys.map(&:downcase).include?('authorization')240req << set_auth_header241end242243req << set_cookie_header244req << set_connection_header245req << set_extra_headers246247req << set_content_type_header(ctype)248req << set_content_len_header(pstr.length)249req << set_chunked_header250req << opts['raw_headers']251req << set_body(pstr) unless headers_only252253req254end255256protected257258def set_uri259uri_str = opts['uri'].dup260if (opts['uri_dir_self_reference'])261uri_str.gsub!('/', '/./')262end263264if (opts['uri_dir_fake_relative'])265buf = ""266uri_str.split('/',-1).each do |part|267cnt = rand(8)+22681.upto(cnt) { |idx|269buf << "/" + Rex::Text.rand_text_alphanumeric(rand(32)+1)270}271buf << ("/.." * cnt)272buf << "/" + part273end274uri_str = buf275end276277if (opts['uri_full_url'])278url = opts['ssl'] ? "https://" : "http://"279url << opts['vhost']280url << ((opts['port'] == 80) ? "" : ":#{opts['port']}")281url << uri_str282url283else284uri_str285end286end287288def set_encode_uri(str)289a = str.to_s.dup290opts['uri_encode_count'].times {291a = Rex::Text.uri_encode(a, opts['uri_encode_mode'])292}293return a294end295296def set_method297ret = opts['method'].dup298299if (opts['method_random_valid'])300ret = ['GET', 'POST', 'HEAD'][rand(3)]301end302303if (opts['method_random_invalid'])304ret = Rex::Text.rand_text_alpha(rand(20)+1)305end306307if (opts['method_random_case'])308ret = Rex::Text.to_rand_case(ret)309end310311ret312end313314def set_method_uri_spacer315len = opts['pad_method_uri_count'].to_i316set = " "317buf = ""318319case opts['pad_method_uri_type']320when 'tab'321set = "\t"322when 'apache'323set = "\t \x0b\x0c\x0d"324end325326while(buf.length < len)327buf << set[ rand(set.length) ]328end329330return buf331end332333#334# Return the padding to place before the uri335#336def set_uri_prepend337prefix = ""338339if (opts['uri_fake_params_start'])340prefix << '/%3fa=b/../'341end342343if (opts['uri_fake_end'])344prefix << '/%20HTTP/1.0/../../'345end346347prefix348end349350#351# Return the HTTP path info352# TODO:353# * Encode path information354def set_path_info355opts['path_info'] ? opts['path_info'] : ''356end357358#359# Return the padding to place before the uri360#361def set_uri_append362# TODO:363# * Support different padding types364""365end366367#368# Return the spacing between the uri and the version369#370def set_uri_version_spacer371len = opts['pad_uri_version_count'].to_i372set = " "373buf = ""374375case opts['pad_uri_version_type']376when 'tab'377set = "\t"378when 'apache'379set = "\t \x0b\x0c\x0d"380end381382while(buf.length < len)383buf << set[ rand(set.length) ]384end385386return buf387end388389#390# Return the HTTP version string391#392def set_version393ret = opts['proto'] + "/" + opts['version']394395if (opts['version_random_valid'])396ret = opts['proto'] + "/" + ['1.0', '1.1'][rand(2)]397end398399if (opts['version_random_invalid'])400ret = Rex::Text.rand_text_alphanumeric(rand(20)+1)401end402403ret << "\r\n"404end405406#407# Return a formatted header string408#409def set_formatted_header(var, val)410if (self.opts['header_folding'])411"#{var}:\r\n\t#{val}\r\n"412else413"#{var}: #{val}\r\n"414end415end416417#418# Return the HTTP agent header419#420def set_agent_header421opts['agent'] ? set_formatted_header("User-Agent", opts['agent']) : ""422end423424def set_auth_header425opts['authorization'] ? set_formatted_header("Authorization", opts['authorization']) : ""426end427428#429# Return the HTTP cookie header430#431def set_cookie_header432opts['cookie'] ? set_formatted_header("Cookie", opts['cookie']) : ""433end434435#436# Return the HTTP connection header437#438def set_connection_header439opts['connection'] ? set_formatted_header("Connection", opts['connection']) : ""440end441442#443# Return the content type header444#445def set_content_type_header(ctype)446ctype ? set_formatted_header("Content-Type", ctype) : ""447end448449#450# Return the content length header451#452def set_content_len_header(clen)453if opts['method'] == 'GET' && (clen == 0 || opts['chunked_size'] > 0)454# This condition only applies to GET because of the specs.455# RFC-7230:456# A Content-Length header field is normally sent in a POST457# request even when the value is 0 (indicating an empty payload body)458return ''459elsif opts['headers'] && opts['headers']['Content-Length']460# If the module has a modified content-length header, respect that by461# not setting another one.462return ''463end464set_formatted_header("Content-Length", clen)465end466467#468# Return the HTTP Host header469#470def set_host_header471return "" if opts['uri_full_url']472host = opts['vhost']473474# IPv6 addresses must be placed in brackets475if Rex::Socket.is_ipv6?(host)476host = "[#{host}]"477end478479# The port should be appended if non-standard480if not [80,443].include?(opts['port'])481host = host + ":#{opts['port']}"482end483484set_formatted_header("Host", host)485end486487#488# Return a string of formatted extra headers489#490def set_extra_headers491buf = ''492493if (opts['pad_fake_headers'])4941.upto(opts['pad_fake_headers_count'].to_i) do |i|495buf << set_formatted_header(496Rex::Text.rand_text_alphanumeric(rand(32)+1),497Rex::Text.rand_text_alphanumeric(rand(32)+1)498)499end500end501502opts['headers'].each_pair do |var,val|503buf << set_formatted_header(var, val)504end505506buf507end508509def set_chunked_header510return "" if opts['chunked_size'] == 0511set_formatted_header('Transfer-Encoding', 'chunked')512end513514#515# Return the HTTP separator and body string516#517def set_body(bdata)518return "\r\n" + bdata if opts['chunked_size'] == 0519str = bdata.dup520chunked = ''521while str.size > 0522chunk = str.slice!(0,rand(opts['chunked_size']) + 1)523chunked << sprintf("%x", chunk.size) + "\r\n" + chunk + "\r\n"524end525"\r\n" + chunked + "0\r\n\r\n"526end527528def get_file_data(file)529file.respond_to?('read') ? (file.rewind; contents = file.read; file.rewind; contents) : file.to_s530end531532def get_filename(data)533data.is_a?(::Pathname) || data.is_a?(::File) ? ::File.basename(data) : nil534end535536end537538539540end541end542end543544545