diff --git a/lib/msf/core/auxiliary/web/http.rb b/lib/msf/core/auxiliary/web/http.rb index c826a69727..4850f382ad 100644 --- a/lib/msf/core/auxiliary/web/http.rb +++ b/lib/msf/core/auxiliary/web/http.rb @@ -11,11 +11,16 @@ require 'uri' module Msf class Auxiliary::Web::HTTP + # Wraps a queued HTTP request and the callbacks that should process its response. class Request attr_accessor :url attr_reader :opts attr_reader :callbacks + # @param url [String, URI] The target URL to request. + # @param opts [Hash] Request options forwarded to {HTTP#request}. + # @yield [response] Handles the completed response. + # @yieldparam response [Response] The received response. def initialize( url, opts = {}, &callback ) @url = url.to_s.dup @opts = opts.dup @@ -25,17 +30,27 @@ class Auxiliary::Web::HTTP @callbacks = [callback].compact end + # @return [Symbol] The HTTP verb used when the request is executed. def method opts[:method] end - + + # Calls each registered callback with the received response. + # + # @param response [Response] The response to dispatch. + # @return [Array] The stored callbacks. def handle_response( response ) callbacks.each { |c| c.call response } end end + # Decorates {Rex::Proto::Http::Response} with convenience helpers used by web auxiliaries. class Response < Rex::Proto::Http::Response + # Builds a response object from a Rex HTTP response. + # + # @param response [Rex::Proto::Http::Response, nil] The response returned by Rex. + # @return [Response] A wrapped response, or {empty} when no response was received. def self.from_rex_response( response ) return empty if !response @@ -46,20 +61,28 @@ class Auxiliary::Web::HTTP r end + # @return [Response] An empty placeholder response. def self.empty new( 0, '' ) end + # Builds an empty response marked as having timed out. + # + # @return [Response] A timeout sentinel response. def self.timed_out r = empty r.timed_out r end + # @return [Boolean] True when the request timed out. def timed_out? !!@timed_out end + # Marks this response as timed out. + # + # @return [TrueClass] Always returns true. def timed_out @timed_out = true end @@ -73,6 +96,13 @@ class Auxiliary::Web::HTTP attr_accessor :redirect_limit attr_accessor :username , :password, :domain + # @param opts [Hash] Configuration for the HTTP helper. + # @option opts [Object] :framework The active framework instance used for threading. + # @option opts [Object] :parent The owning module used for error reporting. + # @option opts [Hash] :headers Additional HTTP headers to send with every request. + # @option opts [String] :cookie_string Cookie header value to set by default. + # @option opts [Hash] :auth Default authentication options. + # @option opts [Integer] :redirect_limit Maximum redirects to follow. def initialize( opts = {} ) @opts = opts.dup @@ -100,10 +130,17 @@ class Auxiliary::Web::HTTP @after_run_blocks = [] end + # Registers a callback to be executed after {#run} drains the request queue. + # + # @yield A block to run after all queued requests finish. + # @return [Array] The accumulated after-run callbacks. def after_run( &block ) @after_run_blocks << block end + # Creates a Rex HTTP client using the configured target and authentication defaults. + # + # @return [Rex::Proto::Http::Client] A configured HTTP client. def connect c = Rex::Proto::Http::Client.new( opts[:target].host, @@ -126,6 +163,12 @@ class Auxiliary::Web::HTTP c end + # Executes all queued asynchronous requests using the configured thread pool size. + # + # Callback exceptions are reported through the parent module and isolated from + # the rest of the queue processing. + # + # @return [void] def run return if @queue.empty? @@ -152,6 +195,12 @@ class Auxiliary::Web::HTTP call_after_run_blocks end + # Sends an HTTP request and optionally follows redirect responses. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Request options forwarded to {#_request}. + # @option opts [Boolean] :follow_redirect Whether to follow Location headers. + # @return [Response, nil] The final response, or nil when the redirect limit is exceeded. def request( url, opts = {} ) rlimit = self.redirect_limit @@ -163,30 +212,74 @@ class Auxiliary::Web::HTTP nil end + # Enqueues a request to be sent later by {#run}. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Request options. + # @yield [response] Processes the response after execution. + # @yieldparam response [Response] The completed response. + # @return [Queue] The internal request queue. def request_async( url, opts = {}, &callback ) queue Request.new( url, opts, &callback ) end + # Enqueues a GET request. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Additional request options. + # @yield [response] Processes the response after execution. + # @return [Queue] The internal request queue. def get_async( url, opts = {}, &callback ) request_async( url, opts.merge( :method => :get ), &callback ) end + # Enqueues a POST request. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Additional request options. + # @yield [response] Processes the response after execution. + # @return [Queue] The internal request queue. def post_async( url, opts = {}, &callback ) request_async( url, opts.merge( :method => :post ), &callback ) end + # Sends a synchronous GET request. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Additional request options. + # @return [Response, nil] The received response. def get( url, opts = {} ) request( url, opts.merge( :method => :get ) ) end + # Sends a synchronous POST request. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Additional request options. + # @return [Response, nil] The received response. def post( url, opts = {} ) request( url, opts.merge( :method => :post ) ) end + # Yields only when the supplied response body does not appear to be a custom 404 page. + # + # @param path [String] The original request path. + # @param body [String] The response body to compare. + # @yield Runs when the body is not identified as a custom 404. + # @return [void] def if_not_custom_404( path, body, &callback ) custom_404?( path, body ) { |b| callback.call if !b } end + # Determines whether a response body matches the application's custom 404 behavior. + # + # The helper probes several random paths, refines their dynamic content, and compares + # the result to the supplied response body. + # + # @param path [String] The original request path. + # @param body [String] The response body to classify. + # @yieldparam is_custom_404 [Boolean] True when the body matches the custom 404 fingerprint. + # @return [nil] Returns nil immediately and reports the result via the callback. def custom_404?( path, body, &callback ) return if !path || !body @@ -257,25 +350,51 @@ class Auxiliary::Web::HTTP private + # Runs and clears the callbacks registered through {#after_run}. + # + # @return [void] def call_after_run_blocks while block = @after_run_blocks.pop block.call end end + # Executes a block while holding the helper's mutex. + # + # @yield The critical section. + # @return [Object] The block result. def synchronize( &block ) (@mutex ||= Mutex.new).synchronize( &block ) end + # Compares the supplied body to the cached custom 404 signatures for a path. + # + # @param path [String] The original request path. + # @param body [String] The response body to compare. + # @return [Boolean] True when the body matches the cached custom 404 fingerprint. def is_404?( path, body ) @@_404[path].each { |_404| return true if Rex::Text.refine( _404['body'], body ) == _404['rdiff'] } false end + # Appends a request to the internal work queue. + # + # @param request [Request] The queued request wrapper. + # @return [Queue] The internal queue. def queue( request ) @queue << request end + # Issues a single HTTP request without following redirects. + # + # @param url [String, URI] The URL to request. + # @param opts [Hash] Request options. + # @option opts [String] :body Optional request body. + # @option opts [Integer] :timeout Request timeout in seconds. + # @option opts [Hash] :params Query string or POST parameters. + # @option opts [Hash] :headers Per-request headers. + # @option opts [Hash] :rex Direct Rex request overrides. + # @return [Response] The response wrapper. def _request( url, opts = {} ) body = opts[:body] timeout = opts[:timeout] || 10 @@ -323,6 +442,10 @@ class Auxiliary::Web::HTTP Response.empty end + # Prints an error through the owning module when available. + # + # @param message [String] The message to report. + # @return [void] def print_error( message ) return if !@parent @parent.print_error message