diff --git a/lib/browser.rb b/lib/browser.rb index da230e74..d36af789 100644 --- a/lib/browser.rb +++ b/lib/browser.rb @@ -21,14 +21,15 @@ class Browser USER_AGENT_MODES = %w{ static semi-static random } ACCESSOR_OPTIONS = [ - :user_agent, - :user_agent_mode, - :available_user_agents, - :proxy, - :proxy_auth, - :max_threads, - :cache_timeout, - :request_timeout + :user_agent, + :user_agent_mode, + :available_user_agents, + :proxy, + :proxy_auth, + :max_threads, + :cache_timeout, + :request_timeout, + :basic_auth ] attr_reader :hydra, :config_file @@ -36,6 +37,7 @@ class Browser def initialize(options = {}) @config_file = options[:config_file] || CONF_DIR + '/browser.conf.json' + #@basic_auth = options[:basic_auth] options.delete(:config_file) load_config() @@ -138,9 +140,9 @@ class Browser def setup_cache_handlers @hydra.cache_setter do |request| @cache.write_entry( - Browser.generate_cache_key_from_request(request), - request.response, - request.cache_timeout + Browser.generate_cache_key_from_request(request), + request.response, + request.cache_timeout ) end @@ -153,20 +155,20 @@ class Browser def get(url, params = {}) run_request( - forge_request(url, params.merge(:method => :get)) + forge_request(url, params.merge(:method => :get)) ) end def post(url, params = {}) run_request( - forge_request(url, params.merge(:method => :post)) + forge_request(url, params.merge(:method => :post)) ) end def forge_request(url, params = {}) Typhoeus::Request.new( - url.to_s, - merge_request_params(params) + url.to_s, + merge_request_params(params) ) end @@ -179,6 +181,14 @@ class Browser end end + if @basic_auth + if !params.has_key?(:headers) + params = params.merge(:headers => {'Authorization' => @basic_auth}) + elsif !params[:headers].has_key?('Authorization') + params[:headers]['Authorization'] = @basic_auth + end + end + unless params.has_key?(:disable_ssl_host_verification) params = params.merge(:disable_ssl_host_verification => true) end diff --git a/lib/wpscan/modules/web_site.rb b/lib/wpscan/modules/web_site.rb index e76baeae..8b08ceea 100644 --- a/lib/wpscan/modules/web_site.rb +++ b/lib/wpscan/modules/web_site.rb @@ -24,16 +24,16 @@ module WebSite wordpress = false response = Browser.instance.get( - login_url(), - {:follow_location => true, :max_redirects => 2} + login_url(), + {:follow_location => true, :max_redirects => 2} ) if response.body =~ %r{WordPress}i wordpress = true else response = Browser.instance.get( - xmlrpc_url(), - {:follow_location => true, :max_redirects => 2} + xmlrpc_url(), + {:follow_location => true, :max_redirects => 2} ) if response.body =~ %r{XML-RPC server accepts POST requests only}i @@ -53,6 +53,10 @@ module WebSite Browser.instance.get(@uri.to_s).code != 0 end + def has_basic_auth? + Browser.instance.get(@uri.to_s).code == 401 + end + # see if the remote url returns 30x redirect # return a string with the redirection or nil def redirection(url = nil) diff --git a/lib/wpscan/wp_target.rb b/lib/wpscan/wp_target.rb index 406f7213..214e6c74 100644 --- a/lib/wpscan/wp_target.rb +++ b/lib/wpscan/wp_target.rb @@ -75,7 +75,7 @@ class WpTarget # Valid HTTP return codes def self.valid_response_codes - [200, 403, 301, 302, 500] + [200, 301, 302, 401, 403, 500] end # return WpTheme diff --git a/lib/wpscan/wpscan_options.rb b/lib/wpscan/wpscan_options.rb index d5557303..8c8e5829 100644 --- a/lib/wpscan/wpscan_options.rb +++ b/lib/wpscan/wpscan_options.rb @@ -43,7 +43,8 @@ class WpscanOptions :wp_plugins_dir, :help, :config_file, - :exclude_content_based + :exclude_content_based, + :basic_auth ] attr_accessor *ACCESSOR_OPTIONS @@ -136,6 +137,11 @@ class WpscanOptions end end + def basic_auth=(basic_auth) + raise "Invalid basic authentication format, login:password expected" if basic_auth.index(':').nil? + @basic_auth = "Basic #{Base64.encode64(basic_auth).chomp}" + end + def has_options? !to_h.empty? end @@ -225,22 +231,23 @@ class WpscanOptions # Even if a short option is given (IE : -u), the long one will be returned (IE : --url) def self.get_opt_long GetoptLong.new( - ["--url", "-u", GetoptLong::REQUIRED_ARGUMENT], - ["--enumerate", "-e", GetoptLong::OPTIONAL_ARGUMENT], - ["--username", "-U", GetoptLong::REQUIRED_ARGUMENT], - ["--wordlist", "-w", GetoptLong::REQUIRED_ARGUMENT], - ["--threads", "-t", GetoptLong::REQUIRED_ARGUMENT], - ["--force", "-f", GetoptLong::NO_ARGUMENT], - ["--help", "-h", GetoptLong::NO_ARGUMENT], - ["--verbose", "-v", GetoptLong::NO_ARGUMENT], - ["--proxy", GetoptLong::REQUIRED_ARGUMENT], - ["--proxy-auth", GetoptLong::REQUIRED_ARGUMENT], - ["--update", GetoptLong::NO_ARGUMENT], - ["--follow-redirection", GetoptLong::NO_ARGUMENT], - ["--wp-content-dir", GetoptLong::REQUIRED_ARGUMENT], - ["--wp-plugins-dir", GetoptLong::REQUIRED_ARGUMENT], - ["--config-file", "-c", GetoptLong::REQUIRED_ARGUMENT], - ["--exclude-content-based", GetoptLong::REQUIRED_ARGUMENT] + ["--url", "-u", GetoptLong::REQUIRED_ARGUMENT], + ["--enumerate", "-e", GetoptLong::OPTIONAL_ARGUMENT], + ["--username", "-U", GetoptLong::REQUIRED_ARGUMENT], + ["--wordlist", "-w", GetoptLong::REQUIRED_ARGUMENT], + ["--threads", "-t", GetoptLong::REQUIRED_ARGUMENT], + ["--force", "-f", GetoptLong::NO_ARGUMENT], + ["--help", "-h", GetoptLong::NO_ARGUMENT], + ["--verbose", "-v", GetoptLong::NO_ARGUMENT], + ["--proxy", GetoptLong::REQUIRED_ARGUMENT], + ["--proxy-auth", GetoptLong::REQUIRED_ARGUMENT], + ["--update", GetoptLong::NO_ARGUMENT], + ["--follow-redirection", GetoptLong::NO_ARGUMENT], + ["--wp-content-dir", GetoptLong::REQUIRED_ARGUMENT], + ["--wp-plugins-dir", GetoptLong::REQUIRED_ARGUMENT], + ["--config-file", "-c", GetoptLong::REQUIRED_ARGUMENT], + ["--exclude-content-based", GetoptLong::REQUIRED_ARGUMENT], + ["--basic-auth", GetoptLong::REQUIRED_ARGUMENT] ) end diff --git a/spec/lib/browser_spec.rb b/spec/lib/browser_spec.rb index f3af0996..15c66971 100644 --- a/spec/lib/browser_spec.rb +++ b/spec/lib/browser_spec.rb @@ -235,6 +235,24 @@ describe Browser do @browser.merge_request_params(:headers => {'accept' => 'text/html'}).should == expected_params end + + it "should merge the basic-auth" do + @browser.basic_auth = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" + expected_params = { + :disable_ssl_host_verification => true, + :disable_ssl_peer_verification => true, + :headers => { + "Authorization" => "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "user-agent" => @browser.user_agent + }, + :cache_timeout => @json_config_without_proxy['cache_timeout'] + } + + @browser.merge_request_params().should == expected_params + + expected_params[:headers].merge!("user-agent" => "Fake FF") + @browser.merge_request_params(:headers => {"user-agent" => "Fake FF"}).should == expected_params + end end describe "#merge_request_params with proxy" do diff --git a/spec/lib/wpscan/modules/web_site_spec.rb b/spec/lib/wpscan/modules/web_site_spec.rb index 18c70ad8..cf70f984 100644 --- a/spec/lib/wpscan/modules/web_site_spec.rb +++ b/spec/lib/wpscan/modules/web_site_spec.rb @@ -57,7 +57,7 @@ shared_examples_for "WebSite" do it "should return true if the xmlrpc is found" do stub_request(:get, @module.xmlrpc_url). - to_return(:status => 200, :body => File.new(fixtures_dir + '/xmlrpc.php')) + to_return(:status => 200, :body => File.new(fixtures_dir + '/xmlrpc.php')) @module.is_wordpress?.should be_true end @@ -75,6 +75,18 @@ shared_examples_for "WebSite" do end end + describe "#has_basic_auth?" do + it "should detect that the wpsite is basic auth protected" do + stub_request(:get, "http://example.localhost/").to_return(:status => 401) + @module.should have_basic_auth + end + + it "should not have a basic auth for a 200" do + stub_request(:get, "http://example.localhost/").to_return(:status => 200) + @module.should_not have_basic_auth + end + end + describe "#redirection" do it "should return nil if no redirection detected" do stub_request(:get, @module.url).to_return(:status => 200, :body => '') diff --git a/spec/lib/wpscan/wpscan_options_spec.rb b/spec/lib/wpscan/wpscan_options_spec.rb index b3e4fb98..a890a32b 100644 --- a/spec/lib/wpscan/wpscan_options_spec.rb +++ b/spec/lib/wpscan/wpscan_options_spec.rb @@ -197,17 +197,20 @@ describe "WpscanOptions" do end end - describe "#to_h" do - it "should return an empty hash" do - @wpscan_options.to_h.should be_a Hash - @wpscan_options.to_h.should be_empty + describe "#basic_auth=" do + context "invalid format" do + it "should raise an error if the : is missing" do + expect { @wpscan_options.basic_auth = "helloworld" }.to raise_error( + RuntimeError, "Invalid basic authentication format, login:password expected" + ) + end end - it "should return a hash with :verbose = true" do - expected = {:verbose => true} - @wpscan_options.verbose = true - - @wpscan_options.to_h.should === expected + context "valid format" do + it "should add the 'Basic' word and do the encode64. See RFC 2617" do + @wpscan_options.basic_auth = "Aladdin:open sesame" + @wpscan_options.basic_auth.should == "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" + end end end @@ -222,6 +225,20 @@ describe "WpscanOptions" do end end + describe "#to_h" do + it "should return an empty hash" do + @wpscan_options.to_h.should be_a Hash + @wpscan_options.to_h.should be_empty + end + + it "should return a hash with :verbose = true" do + expected = {:verbose => true} + @wpscan_options.verbose = true + + @wpscan_options.to_h.should === expected + end + end + describe "#clean_option" do after :each do WpscanOptions.clean_option(@option).should === @expected