class Arachni::Session
Session
management class.
Handles logins, provided log-out detection, stores and executes login sequences and provided general webapp session related helpers.
@author Tasos “Zapotek” Laskos <tasos.laskos@arachni-scanner.com>
Constants
- LOGIN_RETRY_WAIT
- LOGIN_TRIES
Attributes
@return [Browser]
@return [Hash]
{HTTP::Client#request} options for {#logged_in?}.
@return [Block]
Public Class Methods
# File lib/arachni/session.rb, line 65 def initialize @check_options = {} end
Public Instance Methods
@return [Bool]
`true` if there is log-in capability, `false` otherwise.
# File lib/arachni/session.rb, line 189 def can_login? configured? && has_login_check? end
# File lib/arachni/session.rb, line 69 def clean_up configuration.clear shutdown_browser end
# File lib/arachni/session.rb, line 109 def configuration Data.session.configuration end
@param [Hash] options @option options [String] :url
URL containing the login form.
@option options [Hash{String=>String}] :inputs
Hash containing inputs with which to locate and fill-in the form.
# File lib/arachni/session.rb, line 104 def configure( options ) configuration.clear configuration.merge! options end
@return [Bool]
`true` if {#configure configured}, `false` otherwise.
# File lib/arachni/session.rb, line 115 def configured? !!@login_sequence || configuration.any? end
@return [Bool, nil]
`true` if logged-in, `false` otherwise, `nil` if there's no log-in capability.
# File lib/arachni/session.rb, line 196 def ensure_logged_in return if !can_login? return true if logged_in? print_bad 'The scanner has been logged out.' print_info 'Trying to re-login...' LOGIN_TRIES.times do |i| self.login if self.logged_in? print_ok 'Logged-in successfully.' return true end print_bad "Login attempt #{i+1} failed, retrying after " << "#{LOGIN_RETRY_WAIT} seconds..." sleep LOGIN_RETRY_WAIT end print_bad 'Could not re-login.' false end
Finds a login forms based on supplied location, collection and criteria.
@param [Hash] opts @option opts [Bool] :requires_password
Does the login form include a password field? (Defaults to `true`)
@option opts [Array, Regexp] :action
Regexp to match or String to compare against the form action.
@option opts [String, Array
, Hash
, Symbol] :inputs
Inputs that the form must contain.
@option opts [Array<Element::Form>] :forms
Collection of forms to look through.
@option opts [Page, Array
<Page>] :pages
Pages to look through.
@option opts [String] :url
URL to fetch and look for forms.
@param [Block] block
If a block and a :url are given, the request will run async and the block will be called with the result of this method.
# File lib/arachni/session.rb, line 138 def find_login_form( opts = {}, &block ) async = block_given? requires_password = (opts[:requires_password].nil? ? true : opts[:requires_password]) find = proc do |cforms| cforms.select do |f| next if requires_password && !f.requires_password? oks = [] if action = opts[:action] oks << !!(action.is_a?( Regexp ) ? f.action =~ action : f.action == action) end if inputs = opts[:inputs] oks << f.has_inputs?( inputs ) end oks.count( true ) == oks.size end.first end forms = if opts[:pages] [opts[:pages]].flatten.map { |p| p.forms }.flatten elsif opts[:forms] opts[:forms] elsif (url = opts[:url]) http_opts = { update_cookies: true, follow_location: true, performer: self } if async http.get( url, http_opts ) do |r| block.call find.call( forms_from_response( r, true ) ) end else forms_from_response( http.get( url, http_opts.merge( mode: :sync ) ), true ) end end find.call( forms || [] ) if !async end
# File lib/arachni/session.rb, line 314 def has_browser? Browser.has_executable? && Options.scope.dom_depth_limit > 0 end
@return [Bool]
`true` if a login check exists, `false` otherwise.
# File lib/arachni/session.rb, line 305 def has_login_check? !!@login_check || !!(Options.session.check_url && Options.session.check_pattern) end
@return [HTTP::Client]
# File lib/arachni/session.rb, line 310 def http HTTP::Client end
@param [Hash] http_options
HTTP options to use for the check.
@param [Block] block
If a block has been provided the check will be async and the result will be passed to it, otherwise the method will return the result.
@return [Bool, nil]
`true` if we're logged-in, `false` otherwise.
@raise [Error::NoLoginCheck]
If no login-check has been configured.
# File lib/arachni/session.rb, line 274 def logged_in?( http_options = {}, &block ) fail Error::NoLoginCheck if !has_login_check? http_options = http_options.merge( method: :get, mode: block_given? ? :async : :sync, follow_location: true, performer: self ) http_options.merge!( @check_options ) print_debug 'Performing login check.' bool = nil http.request( Options.session.check_url, http_options ) do |response| bool = !!response.body.match( Options.session.check_pattern ) print_debug "Login check done: #{bool}" if !bool print_debug "\n#{response.request}#{response}" end block.call( bool ) if block end bool end
Uses the information provided by {#configure} or {#login_sequence} to login.
@return [Page, nil]
{Page} if the login was successful, `nil` otherwise.
@raise [Error::NotConfigured]
If not {#configured?}.
@raise [Error::FormNotFound]
If the form could not be found.
# File lib/arachni/session.rb, line 238 def login( raise = false ) fail Error::NotConfigured, 'Please configure the session first.' if !configured? refresh_browser page = nil exception_jail raise do page = @login_sequence ? login_from_sequence : login_from_configuration end if has_browser? http.update_cookies browser.cookies end page ensure shutdown_browser end
@param [Block] block
Login sequence. Must return the resulting {Page}. If a {#browser} is {#has_browser? available} it will be passed to the block.
# File lib/arachni/session.rb, line 225 def record_login_sequence( &block ) @login_sequence = block end
@param [Block] block
Block to be passed the {#browser}.
# File lib/arachni/session.rb, line 259 def with_browser( *args, &block ) block.call browser, *args end
Private Instance Methods
# File lib/arachni/session.rb, line 325 def login_from_configuration print_debug 'Logging in via configuration.' if has_browser? print_debug 'Logging in using browser.' else print_debug 'Logging in without browser.' end print_debug "Grabbing page at: #{configuration[:url]}" # Revert to the Framework DOM Level 1 page handling if no browser # is available. page = has_browser? ? browser.load( configuration[:url], take_snapshot: false ).to_page : Page.from_url( configuration[:url], precision: 1, http: { update_cookies: true }) print_debug "Got page with URL #{page.url}" form = find_login_form( # We need to reparse the body in order to override the scope # and thus extract even out-of-scope forms in case we're dealing # with a Single-Sign-On situation. forms: forms_from_parser( page.parser, true ), inputs: configuration[:inputs].keys ) if !form print_debug_level_2 page.body fail Error::FormNotFound, "Login form could not be found with: #{configuration}" end print_debug "Found login form: #{form.id}" form.page = page if has_browser? # Use the form DOM to submit if a browser is available. form = form.dom form.browser = browser if !form.locate.displayed? fail Error::FormNotVisible, 'Login form is not visible in the DOM.' end end form.update configuration[:inputs] form.auditor = self print_debug "Updated form inputs: #{form.inputs}" page = nil if has_browser? print_debug 'Submitting form.' click_button = configuration[:inputs]. find { |k, _| form.parent.details_for( k )[:type] == :submit } if click_button click_button = click_button.first transitions = [] transitions << browser.fire_event( form.locate, :fill, inputs: form.inputs ) transitions << browser.fire_event( Browser::ElementLocator.new( tag_name: :input, attributes: form.parent.details_for( click_button ) ), :click ) page = browser.to_page page.dom.transitions += transitions else form.submit { |p| page = p } end print_debug 'Form submitted.' else page = form.submit( mode: :sync, follow_location: false, update_cookies: true, performer: self ).to_page if page.response.redirection? url = to_absolute( page.response.headers.location, page.url ) print_debug "Redirected to: #{url}" page = Page.from_url( url, precision: 1, http: { performer: self, update_cookies: true } ) end end page end
# File lib/arachni/session.rb, line 320 def login_from_sequence print_debug "Logging in via sequence: #{@login_sequence}" @login_sequence.call browser end
# File lib/arachni/session.rb, line 436 def refresh_browser return if !has_browser? shutdown_browser # The session handling browser needs to be able to roam free in order # to support SSO. @browser = Browser.new( store_pages: false, ignore_scope: true ) end
# File lib/arachni/session.rb, line 429 def shutdown_browser return if !@browser @browser.shutdown @browser = nil end