Mirror of metasploit
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

request.rb 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. require 'uri'
  2. module Msf
  3. class Plugin::Requests < Msf::Plugin
  4. class ConsoleCommandDispatcher
  5. include Msf::Ui::Console::CommandDispatcher
  6. HELP_REGEX = /^-?-h(?:elp)?$/
  7. def name
  8. 'Request'
  9. end
  10. def commands
  11. {
  12. 'request' => "Make a request of the specified type (#{types.join(', ')})",
  13. }
  14. end
  15. # Dynamically determine the types of requests that are supported based on
  16. # methods prefixed with "parse_args".
  17. #
  18. # @return [Array<String>] The supported request types.
  19. def types
  20. parse_methods = self.public_methods.select {|m| m.to_s =~ /^parse_args_/}
  21. parse_methods.collect {|m| m.to_s.split('_').slice(2..-1).join('_')}
  22. end
  23. # The main handler for the request command.
  24. #
  25. # @param args [Array<String>] The array of arguments provided by the user.
  26. # @return [nil]
  27. def cmd_request(*args)
  28. # short circuit the whole deal if they need help
  29. return help if args.length == 0
  30. return help if args.length == 1 && args.first =~ HELP_REGEX
  31. # detect the request type from the uri which must be the last arg given
  32. uri = args.last
  33. if uri && uri =~ /^[A-Za-z]{3,5}:\/\//
  34. type = uri.split('://', 2).first
  35. else
  36. print_error("The last argument must be a valid and supported URI")
  37. return help
  38. end
  39. # parse options
  40. opts, opt_parser = parse_args(args, type)
  41. if opts && opt_parser
  42. # handle any "global" options
  43. if opts[:output_file]
  44. begin
  45. opts[:output_file] = File.new(opts[:output_file], 'w')
  46. rescue ::Errno::EACCES, Errno::EISDIR, Errno::ENOTDIR
  47. return help(opt_parser, 'Failed to open the specified file for output')
  48. end
  49. end
  50. # hand off the actual request to the appropriate request handler
  51. handler_method = "handle_request_#{type}".to_sym
  52. if self.respond_to?(handler_method)
  53. # call the appropriate request handler
  54. self.send(handler_method, opts, opt_parser)
  55. else
  56. # this should be dead code if parse_args is doing it's job correctly
  57. help(opt_parser, "No request handler found for type (#{type.to_s}).")
  58. end
  59. else
  60. if types.include? type
  61. help(opt_parser)
  62. else
  63. help
  64. end
  65. end
  66. end
  67. # Parse the provided arguments by dispatching to the correct method based
  68. # on the specified type.
  69. #
  70. # @param args [Array<String>] The command line arguments to parse.
  71. # @param type [String] The protocol type that the request is for such as
  72. # HTTP.
  73. # @return [Array<Hash, Rex::Parser::Arguments>] An array with the options
  74. # hash and the argument parser.
  75. def parse_args(args, type = 'http')
  76. type.downcase!
  77. parse_method = "parse_args_#{type}".to_sym
  78. if self.respond_to?(parse_method)
  79. self.send(parse_method, args, type)
  80. else
  81. print_error("Unsupported URI type: #{type}")
  82. end
  83. end
  84. # Parse the provided arguments for making HTTPS requests. The argument flags
  85. # are intended to be similar to the curl utility.
  86. #
  87. # @param args [Array<String>] The command line arguments to parse.
  88. # @param type [String] The protocol type that the request is for.
  89. # @return [Array<Hash, Rex::Parser::Arguments>] An array with the options
  90. # hash and the argument parser.
  91. def parse_args_https(args = [], type = 'https')
  92. # just let http do it
  93. parse_args_http(args, type)
  94. end
  95. # Parse the provided arguments for making HTTP requests. The argument flags
  96. # are intended to be similar to the curl utility.
  97. #
  98. # @param args [Array<String>] The command line arguments to parse.
  99. # @param type [String] The protocol type that the request is for.
  100. # @return [Array<Hash>, Rex::Parser::Arguments>] An array with the options
  101. # hash and the argument parser.
  102. def parse_args_http(args = [], type = 'http')
  103. opt_parser = Rex::Parser::Arguments.new(
  104. '-0' => [ false, 'Use HTTP 1.0' ],
  105. '-1' => [ false, 'Use TLSv1 (SSL)' ],
  106. '-2' => [ false, 'Use SSLv2 (SSL)' ],
  107. '-3' => [ false, 'Use SSLv3 (SSL)' ],
  108. '-A' => [ true, 'User-Agent to send to server' ],
  109. '-d' => [ true, 'HTTP POST data' ],
  110. '-G' => [ false, 'Send the -d data with an HTTP GET' ],
  111. '-h' => [ false, 'This help text' ],
  112. '-H' => [ true, 'Custom header to pass to server' ],
  113. '-i' => [ false, 'Include headers in the output' ],
  114. '-I' => [ false, 'Show document info only' ],
  115. '-o' => [ true, 'Write output to <file> instead of stdout' ],
  116. '-u' => [ true, 'Server user and password' ],
  117. '-X' => [ true, 'Request method to use' ]
  118. #'-x' => [ true, 'Proxy to use, format: [proto://][user:pass@]host[:port]' +
  119. # ' Proto defaults to http:// and port to 1080'],
  120. )
  121. options = {
  122. :headers => {},
  123. :print_body => true,
  124. :print_headers => false,
  125. :ssl_version => 'Auto',
  126. :user_agent => Rex::Proto::Http::Client::DefaultUserAgent,
  127. :version => '1.1'
  128. }
  129. opt_parser.parse(args) do |opt, idx, val|
  130. case opt
  131. when '-0'
  132. options[:version] = '1.0'
  133. when '-1'
  134. options[:ssl_version] = 'TLS1'
  135. when '-2'
  136. options[:ssl_version] = 'SSL2'
  137. when '-3'
  138. options[:ssl_version] = 'SSL3'
  139. when '-A'
  140. options[:user_agent] = val
  141. when '-d'
  142. options[:data] = val
  143. options[:method] ||= 'POST'
  144. when '-G'
  145. options[:method] = 'GET'
  146. when HELP_REGEX
  147. #help(opt_parser)
  148. # guard to prevent further option processing & stymie request handling
  149. return [nil, opt_parser]
  150. when '-H'
  151. name, value = val.split(':', 2)
  152. options[:headers][name] = value.to_s.strip
  153. when '-i'
  154. options[:print_headers] = true
  155. when '-I'
  156. options[:print_headers] = true
  157. options[:print_body] = false
  158. options[:method] ||= 'HEAD'
  159. when '-o'
  160. options[:output_file] = File.expand_path(val)
  161. when '-u'
  162. val = val.split(':', 2) # only split on first ':' as per curl:
  163. # from curl man page: "The user name and passwords are split up on the
  164. # first colon, which makes it impossible to use a colon in the user
  165. # name with this option. The password can, still.
  166. options[:auth_username] = val.first
  167. options[:auth_password] = val.last
  168. when '-p'
  169. options[:auth_password] = val
  170. when '-X'
  171. options[:method] = val
  172. #when '-x'
  173. # @TODO proxy
  174. else
  175. options[:uri] = val
  176. end
  177. end
  178. unless options[:uri]
  179. help(opt_parser)
  180. end
  181. options[:method] ||= 'GET'
  182. options[:uri] = URI(options[:uri])
  183. [options, opt_parser]
  184. end
  185. # Perform an HTTPS request based on the user specifed options.
  186. #
  187. # @param opts [Hash] The options to use for making the HTTPS request.
  188. # @option opts [String] :auth_username An optional username to use with
  189. # basic authentication.
  190. # @option opts [String] :auth_password An optional password to use with
  191. # basic authentication. This is only used when :auth_username is
  192. # specified.
  193. # @option opts [String] :data Any data to include within the body of the
  194. # request. Often used with the POST HTTP method.
  195. # @option opts [Hash] :headers A hash of additional headers to include in
  196. # the request.
  197. # @option opts [String] :method The HTTP method to use in the request.
  198. # @option opts [#write] :output_file A file to write the response data to.
  199. # @option opts [Boolean] :print_body Whether or not to print the body of the
  200. # response.
  201. # @option opts [Boolean] :print_headers Whether or not to print the headers
  202. # of the response.
  203. # @option opts [String] :ssl_version The version of SSL to use if the
  204. # request scheme is HTTPS.
  205. # @option opts [String] :uri The target uri to request.
  206. # @option opts [String] :user_agent The value to use in the User-Agent
  207. # header of the request.
  208. # @param opt_parser [Rex::Parser::Arguments] the argument parser for the
  209. # request type.
  210. # @return [nil]
  211. def handle_request_https(opts, opt_parser)
  212. # let http do it
  213. handle_request_http(opts, opt_parser)
  214. end
  215. # Perform an HTTP request based on the user specifed options.
  216. #
  217. # @param opts [Hash] The options to use for making the HTTP request.
  218. # @option opts [String] :auth_username An optional username to use with
  219. # basic authentication.
  220. # @option opts [String] :auth_password An optional password to use with
  221. # basic authentication. This is only used when :auth_username is
  222. # specified.
  223. # @option opts [String] :data Any data to include within the body of the
  224. # request. Often used with the POST HTTP method.
  225. # @option opts [Hash] :headers A hash of additional headers to include in
  226. # the request.
  227. # @option opts [String] :method The HTTP method to use in the request.
  228. # @option opts [#write] :output_file A file to write the response data to.
  229. # @option opts [Boolean] :print_body Whether or not to print the body of the
  230. # response.
  231. # @option opts [Boolean] :print_headers Whether or not to print the headers
  232. # of the response.
  233. # @option opts [String] :ssl_version The version of SSL to use if the
  234. # request scheme is HTTPS.
  235. # @option opts [String] :uri The target uri to request.
  236. # @option opts [String] :user_agent The value to use in the User-Agent
  237. # header of the request.
  238. # @param opt_parser [Rex::Parser::Arguments] the argument parser for the
  239. # request type.
  240. # @return [nil]
  241. def handle_request_http(opts, opt_parser)
  242. uri = opts[:uri]
  243. http_client = Rex::Proto::Http::Client.new(
  244. uri.host,
  245. uri.port,
  246. {'Msf' => framework},
  247. uri.scheme == 'https',
  248. opts[:ssl_version]
  249. )
  250. if opts[:auth_username]
  251. auth_str = opts[:auth_username] + ':' + opts[:auth_password]
  252. auth_str = 'Basic ' + Rex::Text.encode_base64(auth_str)
  253. opts[:headers]['Authorization'] = auth_str
  254. end
  255. uri.path = '/' if uri.path.length == 0
  256. begin
  257. http_client.connect
  258. req = http_client.request_cgi(
  259. 'agent' => opts[:user_agent],
  260. 'data' => opts[:data],
  261. 'headers' => opts[:headers],
  262. 'method' => opts[:method],
  263. 'password' => opts[:auth_password],
  264. 'query' => uri.query,
  265. 'uri' => uri.path,
  266. 'username' => opts[:auth_username],
  267. 'version' => opts[:version]
  268. )
  269. response = http_client.send_recv(req)
  270. rescue ::OpenSSL::SSL::SSLError
  271. print_error('Encountered an SSL error')
  272. rescue ::Errno::ECONNRESET => ex
  273. print_error('The connection was reset by the peer')
  274. rescue ::EOFError, Errno::ETIMEDOUT, Rex::ConnectionError, ::Timeout::Error
  275. print_error('Encountered an error')
  276. ensure
  277. http_client.close
  278. end
  279. unless response
  280. opts[:output_file].close if opts[:output_file]
  281. return nil
  282. end
  283. if opts[:print_headers]
  284. output_line(opts, response.cmd_string)
  285. output_line(opts, response.headers.to_s)
  286. end
  287. output_line(opts, response.body) if opts[:print_body]
  288. if opts[:output_file]
  289. print_status("Wrote #{opts[:output_file].tell} bytes to #{opts[:output_file].path}")
  290. opts[:output_file].close
  291. end
  292. end
  293. # Output lines based on the provided options. Data is either printed to the
  294. # console or written to a file. Trailing new lines are removed.
  295. #
  296. # @param opts [Hash] The options as parsed from parse_args.
  297. # @option opts [#write, nil] :output_file An optional file to write the
  298. # output to.
  299. # @param line [String] The string to output.
  300. # @return [nil]
  301. def output_line(opts, line)
  302. if opts[:output_file].nil?
  303. if line[-2..-1] == "\r\n"
  304. print_line(line[0..-3])
  305. elsif line[-1] == "\n"
  306. print_line(line[0..-2])
  307. else
  308. print_line(line)
  309. end
  310. else
  311. opts[:output_file].write(line)
  312. end
  313. end
  314. # Print the appropriate help text depending on an optional option parser.
  315. #
  316. # @param opt_parser [Rex::Parser::Arguments] the argument parser for the
  317. # request type.
  318. # @param msg [String] the first line of the help text to display to the
  319. # user.
  320. # @return [nil]
  321. def help(opt_parser = nil, msg = 'Usage: request [options] uri')
  322. print_line(msg)
  323. if opt_parser
  324. print_line(opt_parser.usage)
  325. else
  326. print_line("Supported uri types are: #{types.collect{|t| t + '://'}.join(', ')}")
  327. print_line("To see usage for a specific uri type, use request -h uri")
  328. end
  329. end
  330. end
  331. def initialize(framework, opts)
  332. super
  333. add_console_dispatcher(ConsoleCommandDispatcher)
  334. end
  335. def cleanup
  336. remove_console_dispatcher('Request')
  337. end
  338. def name
  339. 'Request'
  340. end
  341. def desc
  342. 'Make requests from within Metasploit using various protocols.'
  343. end
  344. end # end class
  345. end # end module