333 lines
7.8 KiB
Ruby
333 lines
7.8 KiB
Ruby
# -*- coding: binary -*-
|
|
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# Framework web site for more information on licensing and terms of use.
|
|
# https://metasploit.com/framework/
|
|
##
|
|
|
|
require 'uri'
|
|
|
|
module Msf
|
|
class Auxiliary::Web::HTTP
|
|
|
|
class Request
|
|
attr_accessor :url
|
|
attr_reader :opts
|
|
attr_reader :callbacks
|
|
|
|
def initialize( url, opts = {}, &callback )
|
|
@url = url.to_s.dup
|
|
@opts = opts.dup
|
|
|
|
@opts[:method] ||= :get
|
|
|
|
@callbacks = [callback].compact
|
|
end
|
|
|
|
def method
|
|
opts[:method]
|
|
end
|
|
|
|
def handle_response( response )
|
|
callbacks.each { |c| c.call response }
|
|
end
|
|
end
|
|
|
|
class Response < Rex::Proto::Http::Response
|
|
|
|
def self.from_rex_response( response )
|
|
return empty if !response
|
|
|
|
r = new( response.code, response.message, response.proto )
|
|
response.instance_variables.each do |iv|
|
|
r.instance_variable_set( iv, response.instance_variable_get( iv ) )
|
|
end
|
|
r
|
|
end
|
|
|
|
def self.empty
|
|
new( 0, '' )
|
|
end
|
|
|
|
def self.timed_out
|
|
r = empty
|
|
r.timed_out
|
|
r
|
|
end
|
|
|
|
def timed_out?
|
|
!!@timed_out
|
|
end
|
|
|
|
def timed_out
|
|
@timed_out = true
|
|
end
|
|
end
|
|
|
|
attr_reader :opts
|
|
attr_reader :headers
|
|
attr_reader :framework
|
|
attr_reader :parent
|
|
|
|
attr_accessor :redirect_limit
|
|
attr_accessor :username , :password, :domain
|
|
|
|
def initialize( opts = {} )
|
|
@opts = opts.dup
|
|
|
|
@framework = opts[:framework]
|
|
@parent = opts[:parent]
|
|
|
|
@headers = {
|
|
'Accept' => '*/*',
|
|
'Cookie' => opts[:cookie_string]
|
|
}.merge( opts[:headers] || {} )
|
|
|
|
@headers.delete( 'Cookie' ) if !@headers['Cookie']
|
|
|
|
@request_opts = {}
|
|
if opts[:auth].is_a? Hash
|
|
@username = opts[:auth][:user].to_s
|
|
@password = opts[:auth][:password].to_s
|
|
@domain = opts[:auth][:domain].to_s
|
|
end
|
|
|
|
self.redirect_limit = opts[:redirect_limit] || 20
|
|
|
|
@queue = Queue.new
|
|
|
|
@after_run_blocks = []
|
|
end
|
|
|
|
def after_run( &block )
|
|
@after_run_blocks << block
|
|
end
|
|
|
|
def connect
|
|
c = Rex::Proto::Http::Client.new(
|
|
opts[:target].host,
|
|
opts[:target].port,
|
|
{},
|
|
opts[:target].ssl,
|
|
'Auto',
|
|
nil,
|
|
username,
|
|
password
|
|
)
|
|
|
|
c.set_config({
|
|
'vhost' => opts[:target].vhost,
|
|
'agent' => opts[:user_agent] || Rex::UserAgent.session_agent,
|
|
'domain' => domain
|
|
})
|
|
c
|
|
end
|
|
|
|
def run
|
|
return if @queue.empty?
|
|
|
|
tl = []
|
|
loop do
|
|
while tl.size <= (opts[:max_threads] || 5) && !@queue.empty? && (req = @queue.pop)
|
|
tl << framework.threads.spawn( "#{self.class.name} - #{req})", false, req ) do |request|
|
|
# Keep callback failures isolated.
|
|
begin
|
|
request.handle_response request( request.url, request.opts )
|
|
rescue => e
|
|
print_error e.to_s
|
|
e.backtrace.each { |l| print_error l }
|
|
end
|
|
end
|
|
end
|
|
|
|
break if tl.empty?
|
|
tl.reject! { |t| !t.alive? }
|
|
|
|
select( nil, nil, nil, 0.05 )
|
|
end
|
|
|
|
call_after_run_blocks
|
|
end
|
|
|
|
def request( url, opts = {} )
|
|
rlimit = self.redirect_limit
|
|
|
|
while rlimit >= 0
|
|
rlimit -= 1
|
|
res = _request( url, opts )
|
|
return res if !opts[:follow_redirect] || !url = res.headers['location']
|
|
end
|
|
nil
|
|
end
|
|
|
|
def request_async( url, opts = {}, &callback )
|
|
queue Request.new( url, opts, &callback )
|
|
end
|
|
|
|
def get_async( url, opts = {}, &callback )
|
|
request_async( url, opts.merge( :method => :get ), &callback )
|
|
end
|
|
|
|
def post_async( url, opts = {}, &callback )
|
|
request_async( url, opts.merge( :method => :post ), &callback )
|
|
end
|
|
|
|
def get( url, opts = {} )
|
|
request( url, opts.merge( :method => :get ) )
|
|
end
|
|
|
|
def post( url, opts = {} )
|
|
request( url, opts.merge( :method => :post ) )
|
|
end
|
|
|
|
def if_not_custom_404( path, body, &callback )
|
|
custom_404?( path, body ) { |b| callback.call if !b }
|
|
end
|
|
|
|
def custom_404?( path, body, &callback )
|
|
return if !path || !body
|
|
|
|
precision = 2
|
|
|
|
trv_back = File.dirname( path )
|
|
trv_back << '/' if trv_back[-1,1] != '/'
|
|
|
|
# 404 probes
|
|
generators = [
|
|
# get a random path with an extension
|
|
proc{ path + Rex::Text.rand_text_alpha( 10 ) + '.' + Rex::Text.rand_text_alpha( 10 )[0..precision] },
|
|
|
|
# get a random path without an extension
|
|
proc{ path + Rex::Text.rand_text_alpha( 10 ) },
|
|
|
|
# move up a dir and get a random file
|
|
proc{ trv_back + Rex::Text.rand_text_alpha( 10 ) },
|
|
|
|
# move up a dir and get a random file with an extension
|
|
proc{ trv_back + Rex::Text.rand_text_alpha( 10 ) + '.' + Rex::Text.rand_text_alpha( 10 )[0..precision] },
|
|
|
|
# get a random directory
|
|
proc{ path + Rex::Text.rand_text_alpha( 10 ) + '/' }
|
|
]
|
|
|
|
synchronize do
|
|
@@_404 ||= {}
|
|
@@_404[path] ||= []
|
|
|
|
@@_404_gathered ||= Set.new
|
|
|
|
gathered = 0
|
|
if !@@_404_gathered.include?( path.hash )
|
|
generators.each.with_index do |generator, i|
|
|
@@_404[path][i] ||= {}
|
|
|
|
precision.times {
|
|
get_async( generator.call, :follow_redirect => true ) do |res|
|
|
gathered += 1
|
|
|
|
if gathered == generators.size * precision
|
|
@@_404_gathered << path.hash
|
|
callback.call is_404?( path, body )
|
|
else
|
|
@@_404[path][i]['rdiff_now'] ||= false
|
|
|
|
if !@@_404[path][i]['body']
|
|
@@_404[path][i]['body'] = res.body
|
|
else
|
|
@@_404[path][i]['rdiff_now'] = true
|
|
end
|
|
|
|
if @@_404[path][i]['rdiff_now'] && !@@_404[path][i]['rdiff']
|
|
@@_404[path][i]['rdiff'] = Rex::Text.refine( @@_404[path][i]['body'], res.body )
|
|
end
|
|
end
|
|
end
|
|
}
|
|
end
|
|
else
|
|
callback.call is_404?( path, body )
|
|
end
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
private
|
|
|
|
def call_after_run_blocks
|
|
while block = @after_run_blocks.pop
|
|
block.call
|
|
end
|
|
end
|
|
|
|
def synchronize( &block )
|
|
(@mutex ||= Mutex.new).synchronize( &block )
|
|
end
|
|
|
|
def is_404?( path, body )
|
|
@@_404[path].each { |_404| return true if Rex::Text.refine( _404['body'], body ) == _404['rdiff'] }
|
|
false
|
|
end
|
|
|
|
def queue( request )
|
|
@queue << request
|
|
end
|
|
|
|
def _request( url, opts = {} )
|
|
body = opts[:body]
|
|
timeout = opts[:timeout] || 10
|
|
method = opts[:method].to_s.upcase || 'GET'
|
|
url = url.is_a?( URI ) ? url : URI( url.to_s )
|
|
|
|
rex_overrides = opts.delete( :rex ) || {}
|
|
|
|
param_opts = {}
|
|
|
|
if !(vars_get = Auxiliary::Web::Form.query_to_params( url.query )).empty?
|
|
param_opts['vars_get'] = vars_get
|
|
end
|
|
|
|
if method == 'GET'
|
|
param_opts['vars_get'] ||= {}
|
|
param_opts['vars_get'].merge!( opts[:params] ) if opts[:params].is_a?( Hash )
|
|
elsif method == 'POST'
|
|
param_opts['vars_post'] = opts[:params] || {}
|
|
end
|
|
|
|
opts = @request_opts.merge( param_opts ).merge(
|
|
'uri' => url.path || '/',
|
|
'method' => method,
|
|
'headers' => headers.merge( opts[:headers] || {} )
|
|
# Allow for direct rex overrides
|
|
).merge( rex_overrides )
|
|
|
|
opts['data'] = body if body
|
|
|
|
c = connect
|
|
if opts['username'] and opts['username'] != ''
|
|
c.username = opts['username'].to_s
|
|
c.password = opts['password'].to_s
|
|
end
|
|
Response.from_rex_response c.send_recv( c.request_cgi( opts ), timeout )
|
|
rescue ::Timeout::Error
|
|
Response.timed_out
|
|
#rescue ::Errno::EPIPE, ::Errno::ECONNRESET, Rex::ConnectionTimeout
|
|
# This is bad but we can't anticipate the gazilion different types of network
|
|
# i/o errors between Rex and Errno.
|
|
rescue => e
|
|
elog e.to_s
|
|
e.backtrace.each { |l| elog l }
|
|
Response.empty
|
|
end
|
|
|
|
def print_error( message )
|
|
return if !@parent
|
|
@parent.print_error message
|
|
end
|
|
|
|
alias_method :print_bad, :print_error
|
|
|
|
end
|
|
end
|