diff --git a/lib/common/collections/wp_items/detectable.rb b/lib/common/collections/wp_items/detectable.rb index e0749f37..bca5cd6c 100644 --- a/lib/common/collections/wp_items/detectable.rb +++ b/lib/common/collections/wp_items/detectable.rb @@ -95,7 +95,7 @@ class WpItems < Array code = tag.text.to_s next if code.empty? - if ! code.valid_encoding? + if !code.valid_encoding? code = code.encode('UTF-16be', :invalid => :replace, :replace => '?').encode('UTF-8') end diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 2580a70e..53a1e51d 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -294,18 +294,25 @@ end def get_random_user_agent user_agents = [] - unless File.exist?(USER_AGENTS_FILE) - raise('[ERROR] Missing user-agent data. Please re-run with --update.') - end + # If we can't access the file, die + raise('[ERROR] Missing user-agent data. Please re-run with just --update.') unless File.exist?(USER_AGENTS_FILE) + # Read in file f = File.open(USER_AGENTS_FILE, 'r') + + # Read every line in the file f.each_line do |line| - # ignore comments + # Remove any End of Line issues, and leading/trailing spaces + line = line.strip.chomp + # Ignore empty files and comments next if line.empty? or line =~ /^\s*(#|\/\/)/ + # Add to array user_agents << line.strip end + # Close file handler f.close - # return ransom user-agent + + # Return random user-agent user_agents.sample end @@ -331,4 +338,9 @@ end # Get the HTTP response code def get_http_status(url) Browser.get(url.to_s).code +end + +# Check to see if we need a "s" +def grammar_s(size) + size.to_i >= 1 ? "s" : "" end \ No newline at end of file diff --git a/lib/wpscan/web_site/humans_txt.rb b/lib/wpscan/web_site/humans_txt.rb index 0c25de50..18d9386b 100644 --- a/lib/wpscan/web_site/humans_txt.rb +++ b/lib/wpscan/web_site/humans_txt.rb @@ -3,12 +3,6 @@ class WebSite module HumansTxt - # Checks if a humans.txt file exists - # @return [ Boolean ] - def has_humans? - Browser.get(humans_url).code == 200 - end - # Gets a humans.txt URL # @return [ String ] def humans_url @@ -22,18 +16,15 @@ class WebSite response = Browser.get(humans_url.to_s) body = response.body + # Get all non-comments entries = body.split(/\n/) + # Did we get something? if entries - entries.flatten! - entries.uniq! - - entries.each do |d| - temp = d.strip - return_object << temp.to_s - end + # Remove any rubbish + entries = clean_uri(entries) end - return_object + return return_object end end diff --git a/lib/wpscan/web_site/robots_txt.rb b/lib/wpscan/web_site/robots_txt.rb index 1a4cae85..b9f6589f 100644 --- a/lib/wpscan/web_site/robots_txt.rb +++ b/lib/wpscan/web_site/robots_txt.rb @@ -29,16 +29,12 @@ class WebSite # Did we get something? if entries - # Extract elements - entries.flatten! - # Remove any leading/trailing spaces - entries.collect{|x| x.strip || x } - # End Of Line issues - entries.collect{|x| x.chomp! || x } - # Remove nil's and sort - entries.compact.sort! - # Unique values only - entries.uniq! + # Remove any rubbish + entries = clean_uri(entries) + + # Sort + entries.sort! + # Wordpress URL wordpress_path = @uri.path @@ -50,19 +46,10 @@ class WebSite entries.delete(dir_with_subdir) end - # Each value now, try and make it a full URL - entries.each do |d| - begin - temp = @uri.clone - temp.path = d.strip - rescue URI::Error - temp = d.strip - end - return_object << temp.to_s - end - + # Convert to full URIs + return_object = full_uri(entries) end - return_object + return return_object end protected diff --git a/lib/wpscan/web_site/security_txt.rb b/lib/wpscan/web_site/security_txt.rb index e19be594..77a686ef 100644 --- a/lib/wpscan/web_site/security_txt.rb +++ b/lib/wpscan/web_site/security_txt.rb @@ -3,12 +3,6 @@ class WebSite module SecurityTxt - # Checks if a security.txt file exists - # @return [ Boolean ] - def has_security? - Browser.get(security_url).code == 200 - end - # Gets a security.txt URL # @return [ String ] def security_url @@ -25,16 +19,12 @@ class WebSite # Get all non-comments entries = body.split(/\n/) + # Did we get something? if entries - entries.flatten! - entries.uniq! - - entries.each do |d| - temp = d.strip - return_object << temp.to_s - end + # Remove any rubbish + entries = clean_uri(entries) end - return_object + return return_object end end diff --git a/lib/wpscan/web_site/sitemap.rb b/lib/wpscan/web_site/sitemap.rb index f6aae16e..bc3a3736 100644 --- a/lib/wpscan/web_site/sitemap.rb +++ b/lib/wpscan/web_site/sitemap.rb @@ -31,37 +31,22 @@ class WebSite # Make request response = Browser.get(sitemap_url.to_s) - body = response.body # Get all allow and disallow urls - entries = body.scan(/^sitemap\s*:\s*(.*)$/i) + entries = response.body.scan(/^sitemap\s*:\s*(.*)$/i) # Did we get something? if entries - # Extract elements - entries.flatten! - # Remove any leading/trailing spaces - entries.collect{|x| x.strip || x } - # End Of Line issues - entries.collect{|x| x.chomp! || x } - # Remove nil's and sort - entries.compact.sort! - # Unique values only - entries.uniq! + # Remove any rubbish + entries = clean_uri(entries) - # Each value now, try and make it a full URL - entries.each do |d| - begin - temp = @uri.clone - temp.path = d.strip - rescue URI::Error - temp = d.strip - end - return_object << temp.to_s - end + # Sort + entries.sort! + # Convert to full URIs + return_object = full_uri(entries) end - return_object + return return_object end end diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index fe1c7942..09942573 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -17,13 +17,15 @@ class WpTarget < WebSite data = JSON.parse(response.body) # If there is nothing there, return false - return false if data.empty? - + if data.empty? + return false # WAF/API disabled response - return false if data.include?('message') and data['message'] =~ /Only authenticated users can access the REST API/ - + elsif data.include?('message') and data['message'] =~ /Only authenticated users can access the REST API/ + return false # Success! - return true if response.code == 200 + elsif response.code == 200 + return true + end end # Something went wrong @@ -70,6 +72,10 @@ class WpTarget < WebSite # Sort and uniq users = users.sort.uniq + # Feedback + grammar = grammar_s(users.size) + puts warning("#{users.size} user#{grammar} exposed via API: #{json_users_url}") + # Print results table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], rows: users) diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb index 334eaf19..5aef1127 100644 --- a/lib/wpscan/wp_target/wp_rss.rb +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -43,12 +43,13 @@ class WpTarget < WebSite end if users - # Feedback - puts warning("Detected users from RSS feed:") - # Sort and uniq users = users.sort_by { |user| user.to_s.downcase }.uniq + # Feedback + grammar = grammar_s(users.size) + puts warning("Detected #{users.size} user#{grammar} from RSS feed:") + # Print results table = Terminal::Table.new(headings: ['Name'], rows: users) diff --git a/lib/wpscan/wpscan_helper.rb b/lib/wpscan/wpscan_helper.rb index aff83e65..02626441 100644 --- a/lib/wpscan/wpscan_helper.rb +++ b/lib/wpscan/wpscan_helper.rb @@ -120,6 +120,39 @@ def help puts end + +def clean_uri(entries) + # Extract elements + entries.flatten! + # Remove any leading/trailing spaces + entries.collect{|x| x.strip || x } + # End Of Line issues + entries.collect{|x| x.chomp! || x } + # Remove nil's + entries.compact + # Unique values only + entries.uniq! + + return entries +end + +# Return the full URL +def full_uri(entries) + return_object = [] + # Each value now, try and make it a full URL + entries.each do |d| + begin + temp = @uri.clone + temp.path = d.strip + rescue URI::Error + temp = d.strip + end + return_object << temp.to_s + end + + return return_object +end + # Hook to check if the target if down during the scan # And have the number of requests performed to display at the end of the scan # The target is considered down after 30 requests with status = 0 @@ -138,3 +171,4 @@ Typhoeus.on_complete do |response| sleep(Browser.instance.throttle) end + diff --git a/wpscan.rb b/wpscan.rb index d9fcbfdc..7e64c7bb 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -248,7 +248,8 @@ def main end if wp_target.has_sitemap? - puts info("Sitemap found: #{wp_target.sitemap_url}") + code = get_http_status(wp_target.robots_url) + puts info("Sitemap found: #{wp_target.sitemap_url} [HTTP #{code}]") wp_target.parse_sitemap.each do |dir| code = get_http_status(dir) @@ -257,8 +258,8 @@ def main spacer() end - if wp_target.has_humans? - code = get_http_status(wp_target.humans_url) + code = get_http_status(wp_target.humans_url) + if code == 200 puts info("humans.txt available under: #{wp_target.humans_url} [HTTP #{code}]") wp_target.parse_humans_txt.each do |dir| @@ -267,8 +268,8 @@ def main spacer() end - if wp_target.has_security? - code = get_http_status(wp_target.humans_url) + code = get_http_status(wp_target.security_url) + if code == 200 puts info("security.txt available under: #{wp_target.security_url} [HTTP #{code}]") wp_target.parse_security_txt.each do |dir| @@ -308,18 +309,18 @@ def main end if wp_target.has_xml_rpc? - puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}") + code = get_http_status(wp_target.xml_rpc_url) + puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url} [HTTP #{code}]") spacer() end # Test to see if MAIN API URL gives anything back if wp_target.has_api?(wp_target.json_url) - puts info("API exposed: #{wp_target.json_url}") + code = get_http_status(wp_target.json_url) + puts info("API exposed: #{wp_target.json_url} [HTTP #{code}]") # Test to see if USER API URL gives anything back if wp_target.has_api?(wp_target.json_users_url) - puts warning("Users exposed via API: #{wp_target.json_users_url}") - # Print users from JSON wp_target.json_get_users(wp_target.json_users_url) end @@ -360,6 +361,7 @@ def main exclude_content: wpscan_options.exclude_content_based } + puts info('Enumerating WordPress version ...') if (wp_version = wp_target.version(WP_VERSIONS_FILE)) if wp_target.has_readme? && VersionCompare::lesser?(wp_version.identifier, '4.7') puts warning("The WordPress '#{wp_target.readme_url}' file exists exposing a version number") @@ -398,14 +400,11 @@ def main wp_plugins = WpPlugins.passive_detection(wp_target) if !wp_plugins.empty? - if wp_plugins.size == 1 - puts " | #{wp_plugins.size} plugin found:" - else - puts " | #{wp_plugins.size} plugins found:" - end + grammar = grammar_s(wp_plugins.size) + puts " | #{wp_plugins.size} plugin#{grammar} found:" wp_plugins.output(wpscan_options.verbose) else - puts info('No plugins found') + puts info('No plugins found passively') end spacer() end @@ -438,7 +437,7 @@ def main puts if !wp_plugins.empty? - grammar = wp_themes.size == 1 ? "" : "s" + grammar = grammar_s(wp_plugins.size) puts info("We found #{wp_plugins.size} plugin#{grammar}:") wp_plugins.output(wpscan_options.verbose) @@ -475,7 +474,7 @@ def main ) puts if !wp_themes.empty? - grammar = wp_themes.size == 1 ? "" : "s" + grammar = grammar_s(wp_themes.size) puts info("We found #{wp_themes.size} theme#{grammar}:") wp_themes.output(wpscan_options.verbose) @@ -498,7 +497,7 @@ def main ) puts if !wp_timthumbs.empty? - grammar = wp_timthumbs.size == 1 ? "" : "s" + grammar = grammar_s(wp_timthumbs.size) puts info("We found #{wp_timthumbs.size} timthumb file#{grammar}:") wp_timthumbs.output(wpscan_options.verbose) @@ -533,7 +532,7 @@ def main exit(1) end else - grammar = wp_users.size == 1 ? "" : "s" + grammar = grammar_s(wp_users.size) puts info("We identified the following #{wp_users.size} user#{grammar}:") wp_users.output(margin_left: ' ' * 4) if wp_users[0].login == "admin" @@ -544,21 +543,21 @@ def main else wp_users = WpUsers.new + # Username file? if wpscan_options.usernames File.open(wpscan_options.usernames).each do |username| wp_users << WpUser.new(wp_target.uri, login: username.chomp) end + # Single username? else wp_users << WpUser.new(wp_target.uri, login: wpscan_options.username) end - spacer() end # Start the brute forcer bruteforce = true if wpscan_options.wordlist if wp_target.has_login_protection? - protection_plugin = wp_target.login_protection_plugin() puts