lib/msf/core/auxiliary/web/http.rb
# -*- 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