Skip to content

Commit 4c1ce88

Browse files
committed
Land #14139, Add cookie management to HttpClient and improve standards compliance
2 parents 1255c4a + 3508ba2 commit 4c1ce88

File tree

3 files changed

+90
-75
lines changed

3 files changed

+90
-75
lines changed

lib/msf/core/exploit/http/client.rb

Lines changed: 75 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'uri'
44
require 'digest'
5+
56
module Msf
67

78
###
@@ -11,6 +12,7 @@ module Msf
1112
#
1213
###
1314
module Exploit::Remote::HttpClient
15+
1416
include Msf::Auxiliary::Report
1517

1618
#
@@ -47,7 +49,6 @@ def initialize(info = {})
4749
OptBool.new('FingerprintCheck', [ false, 'Conduct a pre-exploit fingerprint verification', true]),
4850
OptString.new('DOMAIN', [ true, 'The domain to use for Windows authentication', 'WORKSTATION']),
4951
OptFloat.new('HttpClientTimeout', [false, 'HTTP connection and receive timeout']),
50-
OptBool.new('HttpPartialResponses', [false, 'Return partial HTTP responses despite timeouts', false]),
5152
OptBool.new('HttpTrace', [false, 'Show the raw HTTP requests and responses', false]),
5253
OptBool.new('HttpTraceHeadersOnly', [false, 'Show HTTP headers only in HttpTrace', false]),
5354
OptString.new('HttpTraceColors', [false, 'HTTP request and response colors for HttpTrace (unset to disable)', 'red/blu'])
@@ -88,6 +89,9 @@ def initialize(info = {})
8889
)
8990
register_autofilter_ports([ 80, 8080, 443, 8000, 8888, 8880, 8008, 3000, 8443 ])
9091
register_autofilter_services(%W{ http https })
92+
93+
# Initialize an empty cookie jar to keep cookies
94+
self.cookie_jar = Set.new
9195
end
9296

9397
def deregister_http_client_options
@@ -168,7 +172,7 @@ def connect(opts={})
168172
nclient.set_config(
169173
'vhost' => opts['vhost'] || opts['rhost'] || self.vhost(),
170174
'agent' => datastore['UserAgent'],
171-
'partial' => opts['partial'] || datastore['HttpPartialResponses'],
175+
'partial' => opts['partial'],
172176
'uri_encode_mode' => datastore['HTTP::uri_encode_mode'],
173177
'uri_full_url' => datastore['HTTP::uri_full_url'],
174178
'pad_method_uri_count' => datastore['HTTP::pad_method_uri_count'],
@@ -314,69 +318,80 @@ def cleanup
314318
#
315319
# Passes +opts+ through directly to Rex::Proto::Http::Client#request_raw.
316320
#
317-
def send_request_raw(opts={}, timeout = 20, disconnect = false)
321+
def send_request_raw(opts = {}, timeout = 20, disconnect = false)
318322
if datastore['HttpClientTimeout'] && datastore['HttpClientTimeout'] > 0
319323
actual_timeout = datastore['HttpClientTimeout']
320324
else
321-
actual_timeout = opts[:timeout] || timeout
325+
actual_timeout = opts[:timeout] || timeout
322326
end
323327

324-
begin
325-
c = connect(opts)
326-
r = opts[:cgi] ? c.request_cgi(opts) : c.request_raw(opts)
328+
c = connect(opts)
329+
r = opts[:cgi] ? c.request_cgi(opts) : c.request_raw(opts)
327330

328-
if datastore['HttpTrace']
329-
request_color, response_color =
330-
(datastore['HttpTraceColors'] || '').split('/').map { |color| "%bld%#{color}" }
331+
if datastore['HttpTrace']
332+
request_color, response_color =
333+
(datastore['HttpTraceColors'] || '').split('/').map { |color| "%bld%#{color}" }
331334

332-
request = r.to_s(headers_only: datastore['HttpTraceHeaders'])
335+
request = r.to_s(headers_only: datastore['HttpTraceHeaders'])
333336

334-
print_line('#' * 20)
335-
print_line('# Request:')
336-
print_line('#' * 20)
337-
print_line("%clr#{request_color}#{request}%clr")
338-
end
337+
print_line('#' * 20)
338+
print_line('# Request:')
339+
print_line('#' * 20)
340+
print_line("%clr#{request_color}#{request}%clr")
341+
end
339342

340-
res = c.send_recv(r, actual_timeout)
343+
res = c.send_recv(r, actual_timeout)
341344

342-
if datastore['HttpTrace']
343-
print_line('#' * 20)
344-
print_line('# Response:')
345-
print_line('#' * 20)
345+
if datastore['HttpTrace']
346+
print_line('#' * 20)
347+
print_line('# Response:')
348+
print_line('#' * 20)
346349

347-
if res
348-
response = res.to_terminal_output(headers_only: datastore['HttpTraceHeadersOnly'])
350+
if res
351+
response = res.to_terminal_output(headers_only: datastore['HttpTraceHeadersOnly'])
349352

350-
print_line("%clr#{response_color}#{response}%clr")
351-
else
352-
print_line('No response received')
353-
end
353+
print_line("%clr#{response_color}#{response}%clr")
354+
else
355+
print_line('No response received')
354356
end
357+
end
355358

356-
disconnect(c) if disconnect
359+
disconnect(c) if disconnect
357360

358-
res
359-
rescue ::Errno::EPIPE, ::Timeout::Error => e
360-
print_line(e.message) if datastore['HttpTrace']
361-
nil
362-
rescue Rex::ConnectionError => e
363-
vprint_error(e.to_s)
364-
nil
365-
rescue ::Exception => e
366-
print_line(e.message) if datastore['HttpTrace']
367-
raise e
368-
end
361+
res
362+
rescue ::Errno::EPIPE, ::Timeout::Error => e
363+
print_line(e.message) if datastore['HttpTrace']
364+
nil
365+
rescue Rex::ConnectionError => e
366+
vprint_error(e.to_s)
367+
nil
368+
rescue ::Exception => e
369+
print_line(e.message) if datastore['HttpTrace']
370+
raise e
369371
end
370372

371-
372373
# Connects to the server, creates a request, sends the request,
373374
# reads the response
374375
#
375376
# Passes `opts` through directly to {Rex::Proto::Http::Client#request_cgi}.
377+
# Set `opts['keep_cookies']` to keep cookies from responses for reuse in requests.
376378
#
377379
# @return (see Rex::Proto::Http::Client#send_recv))
378-
def send_request_cgi(opts={}, timeout = 20, disconnect = true)
379-
send_request_raw(opts.merge(cgi: true), timeout, disconnect)
380+
def send_request_cgi(opts = {}, timeout = 20, disconnect = true)
381+
if cookie_jar.any?
382+
opts = { 'cookie' => cookie_jar.to_a.join(' ') }.merge(opts)
383+
end
384+
385+
res = send_request_raw(opts.merge(cgi: true), timeout, disconnect)
386+
387+
return unless res
388+
389+
if opts['keep_cookies'] && res.headers['Set-Cookie'].present?
390+
# XXX: CGI::Cookie (get_cookies_parsed) is hella broken
391+
cookie_jar.merge(res.get_cookies.split(' '))
392+
end
393+
394+
res
380395
end
381396

382397
# Connects to the server, creates a request, sends the request, reads the
@@ -387,30 +402,31 @@ def send_request_cgi(opts={}, timeout = 20, disconnect = true)
387402
# `opts['redirect_uri']` will contain the full URI.
388403
#
389404
# @return (see #send_request_cgi)
390-
def send_request_cgi!(opts={}, timeout = 20, redirect_depth = 1)
391-
if datastore['HttpClientTimeout'] && datastore['HttpClientTimeout'] > 0
392-
actual_timeout = datastore['HttpClientTimeout']
393-
else
394-
actual_timeout = opts[:timeout] || timeout
395-
end
405+
def send_request_cgi!(opts = {}, timeout = 20, redirect_depth = 1)
406+
res = send_request_cgi(opts, timeout)
396407

397-
res = send_request_cgi(opts, actual_timeout)
398-
return res unless res && res.redirect? && redirect_depth > 0
408+
return unless res
409+
return res unless res.redirect? && res.redirection && redirect_depth > 0
399410

400411
redirect_depth -= 1
401-
return res if res.redirection.nil?
402412

403413
reconfig_redirect_opts!(res, opts)
404-
send_request_cgi!(opts, actual_timeout, redirect_depth)
414+
send_request_cgi!(opts, timeout, redirect_depth)
405415
end
406416

407-
408417
# Modifies the HTTP request options for a redirection.
409418
#
410419
# @param res [Rex::Proto::HTTP::Response] HTTP Response.
411420
# @param opts [Hash] The HTTP request options to modify.
412421
# @return [void]
413422
def reconfig_redirect_opts!(res, opts)
423+
# XXX: https://github.com/rapid7/metasploit-framework/issues/12281
424+
if opts['method'] == 'POST'
425+
opts['method'] = 'GET'
426+
opts['data'] = nil
427+
opts['vars_post'] = {}
428+
end
429+
414430
location = res.redirection
415431

416432
if location.relative?
@@ -425,7 +441,7 @@ def reconfig_redirect_opts!(res, opts)
425441
opts['redirect_uri'] = new_redirect_uri
426442
opts['uri'] = new_redirect_uri
427443
end
428-
444+
429445
opts['rhost'] = datastore['RHOST']
430446
opts['vhost'] = opts['vhost'] || opts['rhost'] || self.vhost()
431447
opts['rport'] = datastore['RPORT']
@@ -446,6 +462,9 @@ def reconfig_redirect_opts!(res, opts)
446462
opts['SSL'] = false
447463
end
448464
end
465+
466+
# Don't forget any GET parameters
467+
opts['query'] ||= location.query if location.query
449468
end
450469

451470
#
@@ -867,10 +886,10 @@ def service_details
867886
}
868887
end
869888

870-
protected
889+
protected
871890

872891
attr_accessor :client
892+
attr_accessor :cookie_jar
873893

874894
end
875-
876895
end

lib/rex/proto/http/client.rb

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,38 +123,36 @@ def set_config(opts = {})
123123
# @option opts 'vhost' [String] Host header value
124124
#
125125
# @return [ClientRequest]
126-
def request_raw(opts={})
126+
def request_raw(opts = {})
127127
opts = self.config.merge(opts)
128128

129-
opts['ssl'] = self.ssl
130-
opts['cgi'] = false
131-
opts['port'] = self.port
129+
opts['cgi'] = false
130+
opts['port'] = self.port
131+
opts['ssl'] = self.ssl
132132

133-
req = ClientRequest.new(opts)
133+
ClientRequest.new(opts)
134134
end
135135

136-
137136
#
138137
# Create a CGI compatible request
139138
#
140139
# @param (see #request_raw)
141140
# @option opts (see #request_raw)
142-
# @option opts 'ctype' [String] Content-Type header value, default: +application/x-www-form-urlencoded+
141+
# @option opts 'ctype' [String] Content-Type header value, default for POST requests: +application/x-www-form-urlencoded+
143142
# @option opts 'encode_params' [Bool] URI encode the GET or POST variables (names and values), default: true
144143
# @option opts 'vars_get' [Hash] GET variables as a hash to be translated into a query string
145144
# @option opts 'vars_post' [Hash] POST variables as a hash to be translated into POST data
146145
#
147146
# @return [ClientRequest]
148-
def request_cgi(opts={})
147+
def request_cgi(opts = {})
149148
opts = self.config.merge(opts)
150149

151-
opts['ctype'] ||= 'application/x-www-form-urlencoded'
152-
opts['ssl'] = self.ssl
153-
opts['cgi'] = true
154-
opts['port'] = self.port
150+
opts['cgi'] = true
151+
opts['port'] = self.port
152+
opts['ssl'] = self.ssl
153+
opts['ctype'] ||= 'application/x-www-form-urlencoded' if opts['method'] == 'POST'
155154

156-
req = ClientRequest.new(opts)
157-
req
155+
ClientRequest.new(opts)
158156
end
159157

160158
#

lib/rex/proto/http/response.rb

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,9 @@ def redirect?
219219
# @return [URI] the uri of the redirection location.
220220
# @return [nil] if the response hasn't a Location header or it isn't a valid uri.
221221
def redirection
222-
begin
223-
URI(headers['Location'])
224-
rescue ::URI::InvalidURIError
225-
nil
226-
end
222+
URI(headers['Location'])
223+
rescue ArgumentError, ::URI::InvalidURIError
224+
nil
227225
end
228226

229227
#

0 commit comments

Comments
 (0)