Skip to content

Commit 503f6a1

Browse files
committed
Land rapid7#4926, add request plugin for http(s)
2 parents 7de78c1 + 0313f0b commit 503f6a1

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

plugins/request.rb

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

0 commit comments

Comments
 (0)