require 'uri' module Msf class Plugin::Requests < Msf::Plugin class ConsoleCommandDispatcher include Msf::Ui::Console::CommandDispatcher HELP_REGEX = /^-?-h(?:elp)?$/ def name 'Request' end def commands { 'request' => "Make a request of the specified type (#{types.join(', ')})", } end # Dynamically determine the types of requests that are supported based on # methods prefixed with "parse_args". # # @return [Array] The supported request types. def types parse_methods = self.public_methods.select {|m| m.to_s =~ /^parse_args_/} parse_methods.collect {|m| m.to_s.split('_').slice(2..-1).join('_')} end # The main handler for the request command. # # @param args [Array] The array of arguments provided by the user. # @return [nil] def cmd_request(*args) # short circuit the whole deal if they need help return help if args.length == 0 return help if args.length == 1 && args.first =~ HELP_REGEX # detect the request type from the uri which must be the last arg given uri = args.last if uri && uri =~ /^[A-Za-z]{3,5}:\/\// type = uri.split('://', 2).first else print_error("The last argument must be a valid and supported URI") return help end # parse options opts, opt_parser = parse_args(args, type) if opts && opt_parser # handle any "global" options if opts[:output_file] begin opts[:output_file] = File.new(opts[:output_file], 'w') rescue ::Errno::EACCES, Errno::EISDIR, Errno::ENOTDIR return help(opt_parser, 'Failed to open the specified file for output') end end # hand off the actual request to the appropriate request handler handler_method = "handle_request_#{type}".to_sym if self.respond_to?(handler_method) # call the appropriate request handler self.send(handler_method, opts, opt_parser) else # this should be dead code if parse_args is doing it's job correctly help(opt_parser, "No request handler found for type (#{type.to_s}).") end else if types.include? type help(opt_parser) else help end end end # Parse the provided arguments by dispatching to the correct method based # on the specified type. # # @param args [Array] The command line arguments to parse. # @param type [String] The protocol type that the request is for such as # HTTP. # @return [Array] An array with the options # hash and the argument parser. def parse_args(args, type = 'http') type.downcase! parse_method = "parse_args_#{type}".to_sym if self.respond_to?(parse_method) self.send(parse_method, args, type) else print_error("Unsupported URI type: #{type}") end end # Parse the provided arguments for making HTTPS requests. The argument flags # are intended to be similar to the curl utility. # # @param args [Array] The command line arguments to parse. # @param type [String] The protocol type that the request is for. # @return [Array] An array with the options # hash and the argument parser. def parse_args_https(args = [], type = 'https') # just let http do it parse_args_http(args, type) end # Parse the provided arguments for making HTTP requests. The argument flags # are intended to be similar to the curl utility. # # @param args [Array] The command line arguments to parse. # @param type [String] The protocol type that the request is for. # @return [Array, Rex::Parser::Arguments>] An array with the options # hash and the argument parser. def parse_args_http(args = [], type = 'http') opt_parser = Rex::Parser::Arguments.new( '-0' => [ false, 'Use HTTP 1.0' ], '-1' => [ false, 'Use TLSv1 (SSL)' ], '-2' => [ false, 'Use SSLv2 (SSL)' ], '-3' => [ false, 'Use SSLv3 (SSL)' ], '-A' => [ true, 'User-Agent to send to server' ], '-d' => [ true, 'HTTP POST data' ], '-G' => [ false, 'Send the -d data with an HTTP GET' ], '-h' => [ false, 'This help text' ], '-H' => [ true, 'Custom header to pass to server' ], '-i' => [ false, 'Include headers in the output' ], '-I' => [ false, 'Show document info only' ], '-o' => [ true, 'Write output to instead of stdout' ], '-u' => [ true, 'Server user and password' ], '-X' => [ true, 'Request method to use' ] #'-x' => [ true, 'Proxy to use, format: [proto://][user:pass@]host[:port]' + # ' Proto defaults to http:// and port to 1080'], ) options = { :headers => {}, :print_body => true, :print_headers => false, :ssl_version => 'Auto', :user_agent => Rex::Proto::Http::Client::DefaultUserAgent, :version => '1.1' } opt_parser.parse(args) do |opt, idx, val| case opt when '-0' options[:version] = '1.0' when '-1' options[:ssl_version] = 'TLS1' when '-2' options[:ssl_version] = 'SSL2' when '-3' options[:ssl_version] = 'SSL3' when '-A' options[:user_agent] = val when '-d' options[:data] = val options[:method] ||= 'POST' when '-G' options[:method] = 'GET' when HELP_REGEX #help(opt_parser) # guard to prevent further option processing & stymie request handling return [nil, opt_parser] when '-H' name, value = val.split(':', 2) options[:headers][name] = value.to_s.strip when '-i' options[:print_headers] = true when '-I' options[:print_headers] = true options[:print_body] = false options[:method] ||= 'HEAD' when '-o' options[:output_file] = File.expand_path(val) when '-u' val = val.split(':', 2) # only split on first ':' as per curl: # from curl man page: "The user name and passwords are split up on the # first colon, which makes it impossible to use a colon in the user # name with this option. The password can, still. options[:auth_username] = val.first options[:auth_password] = val.last when '-p' options[:auth_password] = val when '-X' options[:method] = val #when '-x' # @TODO proxy else options[:uri] = val end end unless options[:uri] help(opt_parser) end options[:method] ||= 'GET' options[:uri] = URI(options[:uri]) [options, opt_parser] end # Perform an HTTPS request based on the user specifed options. # # @param opts [Hash] The options to use for making the HTTPS request. # @option opts [String] :auth_username An optional username to use with # basic authentication. # @option opts [String] :auth_password An optional password to use with # basic authentication. This is only used when :auth_username is # specified. # @option opts [String] :data Any data to include within the body of the # request. Often used with the POST HTTP method. # @option opts [Hash] :headers A hash of additional headers to include in # the request. # @option opts [String] :method The HTTP method to use in the request. # @option opts [#write] :output_file A file to write the response data to. # @option opts [Boolean] :print_body Whether or not to print the body of the # response. # @option opts [Boolean] :print_headers Whether or not to print the headers # of the response. # @options opts [String] :ssl_version The version of SSL to use if the # request scheme is HTTPS. # @option opts [String] :uri The target uri to request. # @option opts [String] :user_agent The value to use in the User-Agent # header of the request. # @param opt_parser [Rex::Parser::Arguments] the argument parser for the # request type. # @return [nil] def handle_request_https(opts, opt_parser) # let http do it handle_request_http(opts, opt_parser) end # Perform an HTTP request based on the user specifed options. # # @param opts [Hash] The options to use for making the HTTP request. # @option opts [String] :auth_username An optional username to use with # basic authentication. # @option opts [String] :auth_password An optional password to use with # basic authentication. This is only used when :auth_username is # specified. # @option opts [String] :data Any data to include within the body of the # request. Often used with the POST HTTP method. # @option opts [Hash] :headers A hash of additional headers to include in # the request. # @option opts [String] :method The HTTP method to use in the request. # @option opts [#write] :output_file A file to write the response data to. # @option opts [Boolean] :print_body Whether or not to print the body of the # response. # @option opts [Boolean] :print_headers Whether or not to print the headers # of the response. # @options opts [String] :ssl_version The version of SSL to use if the # request scheme is HTTPS. # @option opts [String] :uri The target uri to request. # @option opts [String] :user_agent The value to use in the User-Agent # header of the request. # @param opt_parser [Rex::Parser::Arguments] the argument parser for the # request type. # @return [nil] def handle_request_http(opts, opt_parser) uri = opts[:uri] http_client = Rex::Proto::Http::Client.new( uri.host, uri.port, {'Msf' => framework}, uri.scheme == 'https', opts[:ssl_version] ) if opts[:auth_username] auth_str = opts[:auth_username] + ':' + opts[:auth_password] auth_str = 'Basic ' + Rex::Text.encode_base64(auth_str) opts[:headers]['Authorization'] = auth_str end uri.path = '/' if uri.path.length == 0 begin http_client.connect req = http_client.request_cgi( 'agent' => opts[:user_agent], 'data' => opts[:data], 'headers' => opts[:headers], 'method' => opts[:method], 'password' => opts[:auth_password], 'query' => uri.query, 'uri' => uri.path, 'username' => opts[:auth_username], 'version' => opts[:version] ) response = http_client.send_recv(req) rescue ::OpenSSL::SSL::SSLError print_error('Encountered an SSL error') rescue ::Errno::ECONNRESET => ex print_error('The connection was reset by the peer') rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error print_error('Encountered an error') ensure http_client.close end unless response opts[:output_file].close if opts[:output_file] return nil end if opts[:print_headers] output_line(opts, response.cmd_string) output_line(opts, response.headers.to_s) end output_line(opts, response.body) if opts[:print_body] if opts[:output_file] print_status("Wrote #{opts[:output_file].tell} bytes to #{opts[:output_file].path}") opts[:output_file].close end end # Output lines based on the provided options. Data is either printed to the # console or written to a file. Trailing new lines are removed. # # @param opts [Hash] The options as parsed from parse_args. # @option opts [#write, nil] :output_file An optional file to write the # output to. # @param line [String] The string to output. # @return [nil] def output_line(opts, line) if opts[:output_file].nil? if line[-2..-1] == "\r\n" print_line(line[0..-3]) elsif line[-1] == "\n" print_line(line[0..-2]) else print_line(line) end else opts[:output_file].write(line) end end # Print the appropriate help text depending on an optional option parser. # # @param opt_parser [Rex::Parser::Arguments] the argument parser for the # request type. # @param msg [String] the first line of the help text to display to the # user. # @return [nil] def help(opt_parser = nil, msg = 'Usage: request [options] uri') print_line(msg) if opt_parser print_line(opt_parser.usage) else print_line("Supported uri types are: #{types.collect{|t| t + '://'}.join(', ')}") print_line("To see usage for a specific uri type, use request -h uri") end end end def initialize(framework, opts) super add_console_dispatcher(ConsoleCommandDispatcher) end def cleanup remove_console_dispatcher('Request') end def name 'Request' end def desc 'Make requests from within Metasploit using various protocols.' end end # end class end # end module