Skip to content

Commit

Permalink
Merge pull request #256 from wpscanteam/add/saml-cookie-management
Browse files Browse the repository at this point in the history
Add authenticating onto client site
  • Loading branch information
jwidavid authored May 31, 2024
2 parents 6ad3fbf + 77ab7f0 commit 3656bf0
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 22 deletions.
29 changes: 19 additions & 10 deletions app/controllers/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,26 @@ def saml_request?(effective_uri)
#
# @return [ Void ]
def handle_saml_authentication(effective_uri)
# If we ended up here, the cookie_string is set, and no --expect-saml flag was included
raise Error::SAMLAuthenticationFailed if NS::ParsedCli.cookie_string && !NS::ParsedCli.expect_saml
# If we ended up here but no --expect-saml flag was included
raise Error::SAMLAuthenticationRequired unless NS::ParsedCli.expect_saml

cookies = BrowserAuthenticator.authenticate(effective_uri.to_s)
# Authenticate using the ferrum browser
cookie_string = BrowserAuthenticator.authenticate(effective_uri.to_s)

# Extract name=value pairs and concatenate into a single string
cookie_string = cookies.map do |cookie|
cookie.split(';').first # Takes only the part before the first semicolon (name=value)
end.join('; ')
target_url = target.url # Needed for overriding in tests

puts cookie_string
# Filter out --expect-saml, --cookie-string, and --no-banner flags from the original options
filtered_options = ARGV.reject do |arg|
arg.start_with?('--expect-saml', '--cookie-string', '--no-banner')
end.join(' ')

# Now, use these cookies for the scanning process
# NS::Browser.instance.headers['Cookie'] = cookie_string
# Continue scanning
# Restart the scan with the cookies set and pass in the original options filtered
command = "wpscan --url #{target_url} --cookie-string '#{cookie_string}' --no-banner #{filtered_options}"
raise Error::AuthenticatedRescanFailure, command unless Kernel.system(command)

raise Error::SAMLAuthenticationRequired
exit(NS::ExitCode::OK)
end

# Checks for redirects, an out of scope redirect will raise an Error::HTTPRedirect
Expand All @@ -103,6 +107,11 @@ def handle_redirection(res)
effective_url = target.homepage_res.effective_url # Basically get and follow location of target.url
effective_uri = Addressable::URI.parse(effective_url)

if NS::ParsedCli.expect_saml && !saml_request?(effective_uri)
puts 'SAML authentication was expected but not required.'
puts # New line to serve as buffer before the scan results start
end

handle_saml_authentication(effective_uri) if saml_request?(effective_uri)
handle_scheme_change(effective_url, effective_uri)
return if target.in_scope?(effective_url)
Expand Down
34 changes: 24 additions & 10 deletions lib/cms_scanner/browser_authenticator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,33 @@

require 'ferrum'

module BrowserAuthenticator
def self.authenticate(login_url)
browser = Ferrum::Browser.new(headless: false)
module CMSScanner
module BrowserAuthenticator
def self.authenticate(login_url)
browser = Ferrum::Browser.new(headless: false)

browser.goto(login_url)
begin
puts 'SAML authentication needed. Log in via the opened browser, then press enter.'
browser.goto(login_url)
gets # Waits for user input

# Here you might prompt the user to manually complete the login
puts 'Please log in through the opened browser window. Press enter once done.'
gets # Waits for user input
# Attempt an innocuous command to check if the browser is still responsive
browser.current_url

cookies = browser.cookies.all.to_a
browser.quit
cookies = browser.cookies.all
rescue Ferrum::BrowserError, Ferrum::DeadBrowserError
raise Error::BrowserFailed
ensure
browser.quit if browser&.process
end

cookies
raise Error::SAMLAuthenticationFailed if cookies.nil? || cookies.empty?

# Format the cookies into a string
cookies.map do |_cookie_name, cookie_object|
cookie_attributes = cookie_object.instance_variable_get(:@attributes)
"#{cookie_attributes['name']}=#{cookie_attributes['value']}"
end.join('; ')
end
end
end
36 changes: 35 additions & 1 deletion lib/cms_scanner/errors/saml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,44 @@ module CMSScanner
module Error
# SAML Authentication Required Error
class SAMLAuthenticationRequired < Standard
# :nocov:
def to_s
'SAML authentication is required to access this resource, consider using --expect-saml.'
end
# :nocov:
end

# SAML Authentication Failed Error
class SAMLAuthenticationFailed < Standard
# :nocov:
def to_s
'SAML authentication is required to access this resource. ' \
'Please ensure SAML authentication credentials are provided.'
'Please ensure correct authentication credentials.'
end
# :nocov:
end

# SAML Authentication Failed Error
class AuthenticatedRescanFailure < Standard
attr_reader :command

# @param [ String ] url
def initialize(wpscan_command)
@command = wpscan_command
end

# :nocov:
def to_s
"Following authentication, the system failed to execute follow-up command: #{command}"
end
# :nocov:
end

# Ferrum Browser Error
class BrowserFailed < Standard
# :nocov:
def to_s
'The browser was closed or failed before authentication could be completed.'
end
# :nocov:
end
Expand Down
55 changes: 54 additions & 1 deletion spec/app/controllers/core_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -364,11 +364,64 @@
end

context 'when redirection URL does not contain SAMLRequest' do
let(:redirection) { 'http://example.com/' } # No SAMLRequest in the URL
let(:redirection) { 'http://example.com/' } # No SAMLRequest in the URL

it 'does not raise any error' do
expect { core.handle_redirection(response) }.not_to raise_error
end
end
end

describe '#handle_saml_authentication' do
let(:target_url) { 'http://example.com' }
let(:effective_uri) { Addressable::URI.parse('http://example.com/?SAMLRequest=value') }
let(:cookies) { [{ name: 'sampleName', value: 'sampleValue' }] }
let(:cookie_string) { 'sampleName=sampleValue' }

before do
allow(CMSScanner::BrowserAuthenticator).to receive(:authenticate).and_return(cookies)
allow(CMSScanner::NS::ParsedCli).to receive(:expect_saml).and_return(true)
allow(CMSScanner::NS::ParsedCli).to receive(:cookie_string).and_return(nil)
end

context 'when SAMLRequest is present and --expect-saml is not set' do
it 'raises SAMLAuthenticationRequired error' do
allow(CMSScanner::NS::ParsedCli).to receive(:expect_saml).and_return(false)
expect { core.handle_saml_authentication(effective_uri) }
.to raise_error(CMSScanner::Error::SAMLAuthenticationRequired)
end
end

context 'when SAMLRequest is present and --expect-saml is set' do
let(:target_url) { 'http://example.com' }
let(:effective_uri) { Addressable::URI.parse("#{target_url}/?SAMLRequest=value") }
let(:mock_cookie_string) { 'session_id=abc123; auth_token=xyz789' }

before do
allow(core).to receive_message_chain(:target, :url).and_return(target_url)
allow(CMSScanner::BrowserAuthenticator)
.to receive(:authenticate)
.with(effective_uri.to_s)
.and_return(mock_cookie_string)
# Mock the Kernel.system call before the test and ensure it returns true
allow(Kernel).to receive(:system).and_return(true)
end

it 'authenticates and restarts scan with cookies and filters original options' do
original_options = '--some-flag value --another-flag --expect-saml --cookie-string old_value --no-banner'
stub_const('ARGV', original_options.split)
filtered_options = original_options.split.reject do |arg|
arg.start_with?('--expect-saml', '--cookie-string', '--no-banner')
end.join(' ')
command = "wpscan --url #{target_url} --cookie-string '#{mock_cookie_string}' --no-banner #{filtered_options}"

expect(Kernel).to receive(:system).with(command).and_return(true)
expect { core.handle_saml_authentication(effective_uri) }.to raise_error(SystemExit)
end
end

after do
RSpec::Mocks.space.proxy_for(CMSScanner::Browser.instance).reset # Ensure all mocks are cleared
end
end
end

0 comments on commit 3656bf0

Please sign in to comment.