diff --git a/app/controllers/password_attack.rb b/app/controllers/password_attack.rb index a46e5cd7..6c544519 100644 --- a/app/controllers/password_attack.rb +++ b/app/controllers/password_attack.rb @@ -23,27 +23,32 @@ module WPScan ] end - def run - return unless ParsedCli.passwords - - if user_interaction? - output('@info', - msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s") - end - - attack_opts = { + def attack_opts + @attack_opts ||= { show_progression: user_interaction?, multicall_max_passwords: ParsedCli.multicall_max_passwords } + end + + def run + return unless ParsedCli.passwords begin found = [] + if user_interaction? + output('@info', + msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s") + end + attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user| found << user attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}") end + rescue Error::NoLoginInterfaceDetected => e + # TODO: Maybe output that in JSON as well. + output('@notice', msg: e.to_s) if user_interaction? ensure output('users', users: found) end @@ -65,6 +70,8 @@ module WPScan case ParsedCli.password_attack when :wp_login + raise Error::NoLoginInterfaceDetected unless target.login_url + Finders::Passwords::WpLogin.new(target) when :xmlrpc raise Error::XMLRPCNotDetected unless xmlrpc @@ -100,8 +107,10 @@ module WPScan else Finders::Passwords::XMLRPC.new(xmlrpc) end - else + elsif target.login_url Finders::Passwords::WpLogin.new(target) + else + raise Error::NoLoginInterfaceDetected end end diff --git a/lib/wpscan/errors/wordpress.rb b/lib/wpscan/errors/wordpress.rb index a99b84a6..37403ec1 100644 --- a/lib/wpscan/errors/wordpress.rb +++ b/lib/wpscan/errors/wordpress.rb @@ -29,5 +29,11 @@ module WPScan ' use the --scope option or make sure the --url value given is the correct one' end end + + class NoLoginInterfaceDetected < Standard + def to_s + 'Could not find a login interface to perform the password attack against' + end + end end end diff --git a/lib/wpscan/target/platform/wordpress.rb b/lib/wpscan/target/platform/wordpress.rb index 962c0ccf..ef9aeab1 100644 --- a/lib/wpscan/target/platform/wordpress.rb +++ b/lib/wpscan/target/platform/wordpress.rb @@ -139,15 +139,16 @@ module WPScan # the first time the method is called, and the effective_url is then used # if suitable, otherwise the default wp-login will be. # - # @return [ String ] The URL to the login page + # @return [ String, false ] The URL to the login page or false if not detected def login_url - return @login_url if @login_url + return @login_url unless @login_url.nil? - @login_url = url('wp-login.php') + @login_url = url('wp-login.php') # TODO: url(ParsedCli.login_uri) res = Browser.get_and_follow_location(@login_url) @login_url = res.effective_url if res.effective_url =~ /wp-login\.php\z/i && in_scope?(res.effective_url) + @login_url = false if res.code == 404 @login_url end diff --git a/spec/app/controllers/password_attack_spec.rb b/spec/app/controllers/password_attack_spec.rb index 815b487e..60a73db2 100644 --- a/spec/app/controllers/password_attack_spec.rb +++ b/spec/app/controllers/password_attack_spec.rb @@ -107,15 +107,34 @@ describe WPScan::Controller::PasswordAttack do end describe '#attacker' do + before do + allow(controller.target).to receive(:sub_dir) + controller.target.instance_variable_set(:@login_url, nil) + end + context 'when --password-attack provided' do let(:cli_args) { "#{super()} --password-attack #{attack}" } context 'when wp-login' do + before { stub_request(:get, controller.target.url('wp-login.php')).to_return(status: status) } + let(:attack) { 'wp-login' } - it 'returns the correct object' do - expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin - expect(controller.attacker.target).to be_a WPScan::Target + context 'when available' do + let(:status) { 200 } + + it 'returns the correct object' do + expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin + expect(controller.attacker.target).to be_a WPScan::Target + end + end + + context 'when not available (404)' do + let(:status) { 404 } + + it 'raises an error' do + expect { controller.attacker }.to raise_error(WPScan::Error::NoLoginInterfaceDetected) + end end end @@ -172,11 +191,26 @@ describe WPScan::Controller::PasswordAttack do context 'when automatic detection' do context 'when xmlrpc_get_users_blogs_enabled? is false' do - it 'returns the WpLogin' do + before do expect(controller).to receive(:xmlrpc_get_users_blogs_enabled?).and_return(false) + stub_request(:get, controller.target.url('wp-login.php')).to_return(status: status) + end - expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin - expect(controller.attacker.target).to be_a WPScan::Target + context 'when wp-login available' do + let(:status) { 200 } + + it 'returns the WpLogin' do + expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin + expect(controller.attacker.target).to be_a WPScan::Target + end + end + + context 'when wp-login.php not available' do + let(:status) { 404 } + + it 'raises an error' do + expect { controller.attacker }.to raise_error(WPScan::Error::NoLoginInterfaceDetected) + end end end diff --git a/spec/shared_examples/target/platform/wordpress.rb b/spec/shared_examples/target/platform/wordpress.rb index cf169f29..9a57d670 100644 --- a/spec/shared_examples/target/platform/wordpress.rb +++ b/spec/shared_examples/target/platform/wordpress.rb @@ -246,6 +246,12 @@ shared_examples WPScan::Target::Platform::WordPress do its(:login_url) { should eql target.url('wp-login.php') } end + context 'when a 404' do + before { stub_request(:get, target.url('wp-login.php')).to_return(status: 404) } + + its(:login_url) { should eql false } + end + context 'when a redirection occured' do before do expect(WPScan::Browser).to receive(:get_and_follow_location)