Back to Repositories

Testing Password Attack Implementation in WPScan

This test suite validates the password attack functionality in WPScan, focusing on different authentication methods including wp-login and XML-RPC interfaces. It ensures robust security testing capabilities across various WordPress configurations.

Test Coverage Overview

The test suite provides comprehensive coverage of password attack implementations in WPScan, including both wp-login and XML-RPC methods. Key functionality tested includes:

  • CLI options validation for password attacks
  • User enumeration and validation
  • XML-RPC authentication methods detection
  • WordPress version-specific attack strategies
  • Error handling for disabled or unavailable interfaces

Implementation Analysis

The testing approach uses RSpec’s behavior-driven development patterns to validate the password attack controller. It employs extensive mocking and stubbing to test different WordPress configurations and authentication scenarios.

The implementation leverages RSpec’s shared examples and context blocks to organize related test cases and reduce code duplication.

Technical Details

Key technical components include:

  • RSpec testing framework
  • Web request stubbing
  • XML response mocking
  • Version-specific behavior testing
  • Controller state validation
  • Error scenario coverage

Best Practices Demonstrated

The test suite demonstrates several testing best practices:

  • Comprehensive edge case coverage
  • Isolated test contexts
  • Clear test organization
  • Effective use of RSpec matchers
  • Proper setup and teardown patterns
  • Meaningful test descriptions

wpscanteam/wpscan

spec/app/controllers/password_attack_spec.rb

            
# frozen_string_literal: true

XMLRPC_FAILED_BODY = '
<?xml version="1.0" encoding="UTF-8"?>
<methodResponse>
  <fault>
    <value>
      <struct>
        <member>
          <name>faultCode</name>
          <value><int>405</int></value>
        </member>
        <member>
          <name>faultString</name>
          <value><string>%s</string></value>
        </member>
      </struct>
    </value>
  </fault>
</methodResponse>'

describe WPScan::Controller::PasswordAttack do
  subject(:controller) { described_class.new }
  let(:target_url)     { 'http://ex.lo/' }
  let(:cli_args)       { "--url #{target_url}" }

  before do
    WPScan::ParsedCli.options = rspec_parsed_options(cli_args)
  end

  describe '#cli_options' do
    its(:cli_options) { should_not be_empty }
    its(:cli_options) { should be_a Array }

    it 'contains to correct options' do
      expect(controller.cli_options.map(&:to_sym))
        .to eq(%i[passwords usernames multicall_max_passwords password_attack login_uri])
    end
  end

  describe '#users' do
    context 'when no --usernames' do
      it 'calls target.users' do
        expect(controller.target).to receive(:users)
        controller.users
      end
    end

    context 'when --usernames' do
      let(:cli_args) { "#{super()} --usernames admin,editor" }

      it 'returns an array with the users' do
        expected = %w[admin editor].reduce([]) do |a, e|
          a << WPScan::Model::User.new(e)
        end

        expect(controller.users).to eql expected
      end
    end
  end

  describe '#run' do
    context 'when no --passwords is supplied' do
      it 'does not run the attacker' do
        expect(controller.run).to eql nil
      end
    end
  end

  describe '#xmlrpc_get_users_blogs_enabled?' do
    before { expect(controller.target).to receive(:xmlrpc).and_return(xmlrpc) }

    context 'when xmlrpc not found' do
      let(:xmlrpc) { nil }

      its(:xmlrpc_get_users_blogs_enabled?) { should be false }
    end

    context 'when xmlrpc not enabled' do
      let(:xmlrpc) { WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php") }

      it 'returns false' do
        expect(xmlrpc).to receive(:enabled?).and_return(false)

        expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
      end
    end

    context 'when xmlrpc enabled' do
      let(:xmlrpc) { WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php") }

      before { expect(xmlrpc).to receive(:enabled?).and_return(true) }

      context 'when wp.getUsersBlogs methods not listed' do
        it 'returns false' do
          expect(xmlrpc).to receive(:available_methods).and_return(%w[m1 m2])

          expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
        end
      end

      context 'when wp.getUsersBlogs method listed' do
        before do
          expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2])

          stub_request(:post, xmlrpc.url).to_return(body: body)
        end

        context 'when wp.getUsersBlogs method disabled' do
          context 'when blog is in EN' do
            let(:body) { format(XMLRPC_FAILED_BODY, 'XML-RPC services are disabled on this site.') }

            it 'returns false' do
              expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
            end
          end

          context 'when blog is in FR' do
            let(:body) { format(XMLRPC_FAILED_BODY, 'Les services XML-RPC sont désactivés sur ce site.') }

            it 'returns false' do
              expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
            end
          end
        end

        context 'when wp.getUsersBlogs method enabled' do
          let(:body) { 'Incorrect username or password.' }

          it 'returns true' do
            expect(controller.xmlrpc_get_users_blogs_enabled?).to be true
          end
        end
      end
    end
  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' }

        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

      context 'when xmlrpc' do
        context 'when xmlrpc not detected on target' do
          before do
            expect(controller.target).to receive(:xmlrpc).and_return(nil)
          end

          context 'when single xmlrpc' do
            let(:attack) { 'xmlrpc' }

            it 'raises an error' do
              expect { controller.attacker }.to raise_error(WPScan::Error::XMLRPCNotDetected)
            end
          end

          context 'when xmlrpc-multicall' do
            let(:attack) { 'xmlrpc-multicall' }

            it 'raises an error' do
              expect { controller.attacker }.to raise_error(WPScan::Error::XMLRPCNotDetected)
            end
          end
        end

        context 'when xmlrpc detected on target' do
          before do
            expect(controller.target)
              .to receive(:xmlrpc)
              .and_return(WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php"))
          end

          context 'when single xmlrpc' do
            let(:attack) { 'xmlrpc' }

            it 'returns the correct object' do
              expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC
              expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
            end
          end

          context 'when xmlrpc-multicall' do
            let(:attack) { 'xmlrpc-multicall' }

            it 'returns the correct object' do
              expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPCMulticall
              expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
            end
          end
        end
      end
    end

    context 'when automatic detection' do
      context 'when xmlrpc_get_users_blogs_enabled? is false' 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

        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

      context 'when xmlrpc_get_users_blogs_enabled? is true' do
        before do
          expect(controller).to receive(:xmlrpc_get_users_blogs_enabled?).and_return(true)

          expect(controller.target)
            .to receive(:xmlrpc).and_return(WPScan::Model::XMLRPC.new("#{target_url}xmlrpc.php"))
        end

        context 'when WP version not found' do
          it 'returns the XMLRPC' do
            expect(controller.target).to receive(:wp_version).and_return(false)

            expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC
            expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
          end
        end

        context 'when WP version found' do
          before { expect(controller.target).to receive(:wp_version).and_return(wp_version) }

          context 'when WP < 4.4' do
            let(:wp_version) { WPScan::Model::WpVersion.new('3.8.1') }

            it 'returns the XMLRPCMulticall' do
              expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPCMulticall
              expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
            end
          end

          context 'when WP >= 4.4' do
            let(:wp_version) { WPScan::Model::WpVersion.new('4.4') }

            it 'returns the XMLRPC' do
              expect(controller.attacker).to be_a WPScan::Finders::Passwords::XMLRPC
              expect(controller.attacker.target).to be_a WPScan::Model::XMLRPC
            end
          end
        end
      end
    end
  end
end