From c8036692eef04051ac554a4d5d966ebfa1692f51 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:09:33 +0100 Subject: [PATCH 01/52] Display user-agent with verbose mode (Handy with --random-agent) --- wpscan.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/wpscan.rb b/wpscan.rb index 5f638a3b..f4e8d59d 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -196,6 +196,7 @@ def main start_memory = get_memory_usage unless windows? puts info("URL: #{wp_target.url}") puts info("Started: #{start_time.asctime}") + puts info("User-Agent: #{wpscan_options.user_agent}") if wpscan_options.verbose puts if wp_target.has_robots? From a53e9a5e126f364f04dad90da5335052d954dbc2 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:09:58 +0100 Subject: [PATCH 02/52] Show the file being downloaded with verbose --- lib/common/db_updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/common/db_updater.rb b/lib/common/db_updater.rb index f8d2439a..eb833ac6 100644 --- a/lib/common/db_updater.rb +++ b/lib/common/db_updater.rb @@ -95,7 +95,7 @@ class DbUpdater puts ' [i] Needs to be updated' if verbose create_backup(filename) puts ' [i] Backup Created' if verbose - puts ' [i] Downloading new file' if verbose + puts " [i] Downloading new file: #{remote_file_url(filename)}" if verbose dl_checksum = download(filename) puts " [i] Downloaded File Checksum: #{dl_checksum}" if verbose puts " [i] Database File Checksum : #{db_checksum}" if verbose From 5c27c78ed0faf7f1ac285d4530613affa540e208 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:10:34 +0100 Subject: [PATCH 03/52] Add friendly reminder about using -u / --url --- wpscan.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wpscan.rb b/wpscan.rb index f4e8d59d..2bfb35d6 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -54,6 +54,8 @@ def main unless wpscan_options.has_options? # first parameter only url? if ARGV.length == 1 + puts + puts notice("Please use '-u #{ARGV[0]}' next time") wpscan_options.url = ARGV[0] else usage() From ad21d97d1111c5e398c23a8f9660f6f19db776ca Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:11:46 +0100 Subject: [PATCH 04/52] Grammar police! --- wpscan.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index 2bfb35d6..dc011c21 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -346,7 +346,8 @@ def main puts if !wp_plugins.empty? - puts info("We found #{wp_plugins.size} plugins:") + grammar = wp_themes.size.equals == 1 ? "" : "s" + puts info("We found #{wp_plugins.size} plugin#{grammar}:") wp_plugins.output(wpscan_options.verbose) else @@ -381,7 +382,8 @@ def main ) puts if !wp_themes.empty? - puts info("We found #{wp_themes.size} themes:") + grammar = wp_themes.size.equals == 1 ? "" : "s" + puts info("We found #{wp_themes.size} theme#{grammar}:") wp_themes.output(wpscan_options.verbose) else @@ -402,7 +404,8 @@ def main ) puts if !wp_timthumbs.empty? - puts info("We found #{wp_timthumbs.size} timthumb file/s:") + grammar = wp_timthumbs.size.equals == 1 ? "" : "s" + puts info("We found #{wp_timthumbs.size} timthumb file#{grammar}:") wp_timthumbs.output(wpscan_options.verbose) else @@ -435,7 +438,8 @@ def main exit(1) end else - puts info("Identified the following #{wp_users.size} user/s:") + grammar = wp_users.size.equals == 1 ? "" : "s" + puts info("We identified the following #{wp_users.size} user#{grammar}:") wp_users.output(margin_left: ' ' * 4) if wp_users[0].login == "admin" puts warning("Default first WordPress username 'admin' is still used") From c2c8d63e75c47e23cc621d4954bd4387f5f0525e Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:12:27 +0100 Subject: [PATCH 05/52] Show database date when updating --- wpscan.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index dc011c21..272b14af 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -10,6 +10,7 @@ require File.join(__dir__, 'lib', 'wpscan', 'wpscan_helper') def main begin wpscan_options = WpscanOptions.load_from_arguments + date = last_update $log = wpscan_options.log @@ -74,8 +75,7 @@ def main if wpscan_options.version puts "Current version: #{WPSCAN_VERSION}" - date = last_update - puts "Last DB update: #{date.strftime('%Y-%m-%d')}" unless date.nil? + puts "Last database update: #{date.strftime('%Y-%m-%d')}" unless date.nil? exit(0) end @@ -90,6 +90,7 @@ def main if update_required? && !wpscan_options.batch && !wpscan_options.update puts notice('It seems like you have not updated the database for some time.') print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N]' + puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil? if (input = Readline.readline) =~ /^y/i wpscan_options.update = true elsif input =~ /^a/i From 282c595b38290ebb4826d15394cceffb239df1cf Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:13:07 +0100 Subject: [PATCH 06/52] Improve user prompt --- wpscan.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index 272b14af..055a4ebb 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -28,7 +28,7 @@ def main # check if file exists and has a size greater zero if File.exist?($log) && File.size?($log) puts notice("The supplied log file #{$log} already exists. If you continue the new output will be appended.") - print '[?] Do you want to continue? [Y]es [N]o, default: [N]' + print '[?] Do you want to continue? [Y]es [N]o, default: [N] >' if Readline.readline !~ /^y/i # unset logging so puts will try to log to the file $log = nil @@ -89,8 +89,8 @@ def main # also no need to check if the user supplied the --update switch if update_required? && !wpscan_options.batch && !wpscan_options.update puts notice('It seems like you have not updated the database for some time.') - print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N]' puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil? + print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N] > ' if (input = Readline.readline) =~ /^y/i wpscan_options.update = true elsif input =~ /^a/i @@ -148,7 +148,7 @@ def main puts "Following redirection #{redirection}" else puts notice("The remote host tried to redirect to: #{redirection}") - print '[?] Do you want follow the redirection ? [Y]es [N]o [A]bort, default: [N]' + print '[?] Do you want follow the redirection ? [Y]es [N]o [A]bort, default: [N] >' end if wpscan_options.follow_redirection || !wpscan_options.batch if wpscan_options.follow_redirection || (input = Readline.readline) =~ /^y/i From e437b952da20fb98d24f4f37dec920c7279aa0a2 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:14:05 +0100 Subject: [PATCH 07/52] Move timthumbs.txt to all the other data.zip files --- lib/common/common_helper.rb | 1 + wpscan.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 47f071c3..658423b8 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -21,6 +21,7 @@ WPSCAN_PLUGINS_DIR = File.join(WPSCAN_LIB_DIR, 'plugins') # Not used ATM WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') THEMES_FILE = File.join(DATA_DIR, 'themes.json') +TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd') diff --git a/wpscan.rb b/wpscan.rb index 055a4ebb..87905311 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -399,7 +399,7 @@ def main wp_timthumbs = WpTimthumbs.aggressive_detection(wp_target, enum_options.merge( - file: DATA_DIR + '/timthumbs.txt', + file: TIMTHUMBS_FILE, theme_name: wp_theme ? wp_theme.name : nil ) ) From 2c40913a648885f3bc10431078aff79831896541 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:14:41 +0100 Subject: [PATCH 08/52] Misc wording fixes --- lib/common/common_helper.rb | 1 + lib/wpscan/wpscan_helper.rb | 2 +- wpscan.rb | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 658423b8..e57d0884 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -95,6 +95,7 @@ def last_update date end +# Was it 5 days ago? def update_required? date = last_update day_seconds = 24 * 60 * 60 diff --git a/lib/wpscan/wpscan_helper.rb b/lib/wpscan/wpscan_helper.rb index d007f948..aff83e65 100644 --- a/lib/wpscan/wpscan_helper.rb +++ b/lib/wpscan/wpscan_helper.rb @@ -49,7 +49,7 @@ def usage puts '-Use custom plugins directory ...' puts "ruby #{script_name} -u www.example.com --wp-plugins-dir wp-content/custom-plugins" puts - puts '-Update the DB ...' + puts '-Update the Database ...' puts "ruby #{script_name} --update" puts puts '-Debug output ...' diff --git a/wpscan.rb b/wpscan.rb index 87905311..463a6e6a 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -85,9 +85,10 @@ def main wpscan_options.to_h.merge(max_threads: wpscan_options.threads) ) - # check if db file needs upgrade and we are not running in batch mode - # also no need to check if the user supplied the --update switch + # Check if db file needs upgrade (older than 5 days) and we are not running in --batch mode + # Also no need to check if the user supplied the --update switch if update_required? && !wpscan_options.batch && !wpscan_options.update + puts puts notice('It seems like you have not updated the database for some time.') puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil? print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N] > ' @@ -98,7 +99,7 @@ def main exit(1) else if missing_db_file? - puts critical('You can not run a scan without any databases. Extract the data.zip file.') + puts critical('You can not run a scan without any databases. Manually extract the data.zip file.') exit(1) end end From 435fb34233031efa6ad347491506f23b2adbc428 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:15:12 +0100 Subject: [PATCH 09/52] Check for user-agents.txt before using it --- lib/common/common_helper.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index e57d0884..05c6a96d 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -255,6 +255,11 @@ end # @return [ String ] A random user-agent from data/user-agents.txt 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 + f = File.open(USER_AGENTS_FILE, 'r') f.each_line do |line| # ignore comments From 25c393d557d925e6c1cc1d18580145fbad1450bd Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:58:04 +0100 Subject: [PATCH 10/52] gitignore cleanup --- .gitignore | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 0f7866fb..65c2aaf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,19 @@ +# OS Rubbish +cache/ +coverage/ +*.sublime-* +.*.swp .ash_history -cache -coverage .bundle .DS_Store .DS_Store? -*.sublime-* .idea -.*.swp -log.txt .yardoc -debug.log -wordlist.txt -rspec_results.html + +# WPScan data/ vendor/ +debug.log +log.txt +rspec_results.html +wordlist.txt From b6c6a46d2562e37c3fedc0128c54eb74bde5ccf0 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 13:58:23 +0100 Subject: [PATCH 11/52] Remove un-needed single quotes in output --- wpscan.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index 463a6e6a..c3eb8ffa 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -204,7 +204,7 @@ def main puts if wp_target.has_robots? - puts info("robots.txt available under: '#{wp_target.robots_url}'") + puts info("robots.txt available under: #{wp_target.robots_url}") wp_target.parse_robots_txt.each do |dir| puts info("Interesting entry from robots.txt: #{dir}") @@ -220,15 +220,15 @@ def main end wp_target.config_backup.each do |file_url| - puts critical("A wp-config.php backup file has been found in: '#{file_url}'") + puts critical("A wp-config.php backup file has been found in: #{file_url}") end if wp_target.search_replace_db_2_exists? - puts critical("searchreplacedb2.php has been found in: '#{wp_target.search_replace_db_2_url}'") + puts critical("searchreplacedb2.php has been found in: #{wp_target.search_replace_db_2_url}") end if wp_target.emergency_exists? - puts critical("emergency.php has been found in: '#{wp_target.emergency_url}'") + puts critical("emergency.php has been found in: #{wp_target.emergency_url}") end wp_target.interesting_headers.each do |header| From 358f3d59d811718227709c36d4e32a6e65222b07 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:04:01 +0100 Subject: [PATCH 12/52] Say when to use --force --- wpscan.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpscan.rb b/wpscan.rb index c3eb8ffa..5dc85fc8 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -178,7 +178,7 @@ def main # Remote website is wordpress? unless wpscan_options.force unless wp_target.wordpress? - raise 'The remote website is up, but does not seem to be running WordPress.' + raise 'The remote website is up, but does not seem to be running WordPress. If you are sure, use --force' end end From 5720d29492caf084deb88705142b3f5a160ccd58 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:11:09 +0100 Subject: [PATCH 13/52] Fix inconsistencies with line endings --- wpscan.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index 5dc85fc8..b7599526 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -89,7 +89,7 @@ def main # Also no need to check if the user supplied the --update switch if update_required? && !wpscan_options.batch && !wpscan_options.update puts - puts notice('It seems like you have not updated the database for some time.') + puts notice('It seems like you have not updated the database for some time') puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil? print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N] > ' if (input = Readline.readline) =~ /^y/i @@ -108,7 +108,7 @@ def main if wpscan_options.update puts notice('Updating the Database ...') DbUpdater.new(DATA_DIR).update(wpscan_options.verbose) - puts notice('Update completed.') + puts notice('Update completed') # Exit program if only option --update is used exit(0) unless wpscan_options.url end @@ -124,7 +124,7 @@ def main end if wp_target.ssl_error? - raise "The target site returned an SSL/TLS error. You can try again using the --disable-tls-checks option.\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code" + raise "The target site returned an SSL/TLS error. You can try again using the --disable-tls-checks option\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code" end # Remote website up? From dc48008d438613d0e9134fbf9d3b8e52e99dd8c3 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:16:18 +0100 Subject: [PATCH 14/52] Bug with user-agent being shown --- wpscan.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wpscan.rb b/wpscan.rb index b7599526..21c9d56f 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -200,7 +200,7 @@ def main start_memory = get_memory_usage unless windows? puts info("URL: #{wp_target.url}") puts info("Started: #{start_time.asctime}") - puts info("User-Agent: #{wpscan_options.user_agent}") if wpscan_options.verbose + puts info("User-Agent: #{wpscan_options.user_agent}") if wpscan_options.verbose and wpscan_options.user_agent puts if wp_target.has_robots? From 6c0a21c80d051965ca487ef880af1114f6406974 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:33:44 +0100 Subject: [PATCH 15/52] Add /humans.txt check See http://humanstxt.org/ --- lib/wpscan/web_site.rb | 2 + lib/wpscan/web_site/humans_txt.rb | 39 +++++++ spec/shared_examples/web_site/humans_txt.rb | 108 ++++++++++++++++++++ wpscan.rb | 8 ++ 4 files changed, 157 insertions(+) create mode 100644 lib/wpscan/web_site/humans_txt.rb create mode 100644 spec/shared_examples/web_site/humans_txt.rb diff --git a/lib/wpscan/web_site.rb b/lib/wpscan/web_site.rb index 002f324b..77125c98 100644 --- a/lib/wpscan/web_site.rb +++ b/lib/wpscan/web_site.rb @@ -1,10 +1,12 @@ # encoding: UTF-8 require 'web_site/robots_txt' +require 'web_site/humans_txt' require 'web_site/interesting_headers' class WebSite include WebSite::RobotsTxt + include WebSite::HumansTxt include WebSite::InterestingHeaders attr_reader :uri diff --git a/lib/wpscan/web_site/humans_txt.rb b/lib/wpscan/web_site/humans_txt.rb new file mode 100644 index 00000000..4a4a23d9 --- /dev/null +++ b/lib/wpscan/web_site/humans_txt.rb @@ -0,0 +1,39 @@ +# encoding: UTF-8 + +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 + @uri.clone.merge('humans.txt').to_s + end + + # Parse humans.txt + # @return [ Array ] URLs generated from humans.txt + def parse_humans_txt + return unless has_humans? + + return_object = [] + response = Browser.get(humans_url.to_s) + entries = response.body.split(/\n/) + if entries + entries.flatten! + entries.uniq! + + entries.each do |d| + temp = d.strip + return_object << temp.to_s + end + end + return_object + end + + end +end diff --git a/spec/shared_examples/web_site/humans_txt.rb b/spec/shared_examples/web_site/humans_txt.rb new file mode 100644 index 00000000..b54d1ed5 --- /dev/null +++ b/spec/shared_examples/web_site/humans_txt.rb @@ -0,0 +1,108 @@ +# encoding: UTF-8 + +shared_examples 'WebSite::HumansTxt' do + let(:known_dirs) { WebSite::HumansTxt.known_dirs } + + describe '#humans_url' do + it 'returns the correct url' do + expect(web_site.humans_url).to eql 'http://example.localhost/humans.txt' + end + end + + describe '#has_humans?' do + it 'returns true' do + stub_request(:get, web_site.humans_url).to_return(status: 200) + expect(web_site.has_humans?).to be_truthy + end + + it 'returns false' do + stub_request(:get, web_site.humans_url).to_return(status: 404) + expect(web_site.has_humans?).to be_falsey + end + end + + describe '#parse_humans_txt' do + + context 'installed in root' do + after :each do + stub_request_to_fixture(url: web_site.humans_url, fixture: @fixture) + humans = web_site.parse_humans_txt + expect(humans).to match_array @expected + end + + it 'returns an empty Array (empty humans.txt)' do + @fixture = fixtures_dir + '/humans_txt/empty_humans.txt' + @expected = [] + end + + it 'returns an empty Array (invalid humans.txt)' do + @fixture = fixtures_dir + '/humans_txt/invalid_humans.txt' + @expected = [] + end + + it 'returns some urls and some strings' do + @fixture = fixtures_dir + '/humans_txt/invalid_humans_2.txt' + @expected = %w( + /ÖÜ()=? + http://10.0.0.0/wp-includes/ + http://example.localhost/asdf/ + wooooza + ) + end + + it 'returns an Array of urls (valid humans.txt)' do + @fixture = fixtures_dir + '/humans_txt/humans.txt' + @expected = %w( + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/wp-admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + end + + it 'removes duplicate entries from humans.txt test 1' do + @fixture = fixtures_dir + '/humans_txt/humans_duplicate_1.txt' + @expected = %w( + http://example.localhost/wordpress/ + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/wp-admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + end + + it 'removes duplicate entries from humans.txt test 2' do + @fixture = fixtures_dir + '/humans_txt/humans_duplicate_2.txt' + @expected = nil + end + end + + context 'installed in sub directory' do + it 'returns an Array of urls (valid humans.txt, WP installed in subdir)' do + web_site_sub = WebSite.new('http://example.localhost/wordpress/') + fixture = fixtures_dir + '/humans_txt/humans.txt' + expected = %w( + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + stub_request_to_fixture(url: web_site_sub.humans_url, fixture: fixture) + humans = web_site_sub.parse_humans_txt + expect(humans).to match_array expected + end + end + end + + describe '#known_dirs' do + it 'does not contain duplicates' do + expect(known_dirs.flatten.uniq.length).to eq known_dirs.length + end + end + +end diff --git a/wpscan.rb b/wpscan.rb index 21c9d56f..be4a0b0d 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -211,6 +211,14 @@ def main end end + if wp_target.has_humans? + puts info("humans.txt available under: #{wp_target.humans_url}") + + wp_target.parse_humans_txt.each do |dir| + puts info("Interesting entry from humans.txt: #{dir}") + end + end + if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") end From 37a72f0c7229f49108df48147759f709065fe87b Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:34:30 +0100 Subject: [PATCH 16/52] Add /.well-known/security.txt check See https://securitytxt.org/ --- lib/wpscan/web_site.rb | 2 + lib/wpscan/web_site/security_txt.rb | 39 +++++++ spec/shared_examples/web_site/security_txt.rb | 108 ++++++++++++++++++ wpscan.rb | 8 ++ 4 files changed, 157 insertions(+) create mode 100644 lib/wpscan/web_site/security_txt.rb create mode 100644 spec/shared_examples/web_site/security_txt.rb diff --git a/lib/wpscan/web_site.rb b/lib/wpscan/web_site.rb index 77125c98..3779210b 100644 --- a/lib/wpscan/web_site.rb +++ b/lib/wpscan/web_site.rb @@ -2,11 +2,13 @@ require 'web_site/robots_txt' require 'web_site/humans_txt' +require 'web_site/security_txt' require 'web_site/interesting_headers' class WebSite include WebSite::RobotsTxt include WebSite::HumansTxt + include WebSite::SecurityTxt include WebSite::InterestingHeaders attr_reader :uri diff --git a/lib/wpscan/web_site/security_txt.rb b/lib/wpscan/web_site/security_txt.rb new file mode 100644 index 00000000..3464fd72 --- /dev/null +++ b/lib/wpscan/web_site/security_txt.rb @@ -0,0 +1,39 @@ +# encoding: UTF-8 + +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 + @uri.clone.merge('.well-known/security.txt').to_s + end + + # Parse security.txt + # @return [ Array ] URLs generated from security.txt + def parse_security_txt + return unless has_security? + + return_object = [] + response = Browser.get(security_url.to_s) + entries = response.body.split(/\n/) + if entries + entries.flatten! + entries.uniq! + + entries.each do |d| + temp = d.strip + return_object << temp.to_s + end + end + return_object + end + + end +end diff --git a/spec/shared_examples/web_site/security_txt.rb b/spec/shared_examples/web_site/security_txt.rb new file mode 100644 index 00000000..3bca86dd --- /dev/null +++ b/spec/shared_examples/web_site/security_txt.rb @@ -0,0 +1,108 @@ +# encoding: UTF-8 + +shared_examples 'WebSite::SecurityTxt' do + let(:known_dirs) { WebSite::SecurityTxt.known_dirs } + + describe '#security_url' do + it 'returns the correct url' do + expect(web_site.security_url).to eql 'http://example.localhost/security.txt' + end + end + + describe '#has_security?' do + it 'returns true' do + stub_request(:get, web_site.security_url).to_return(status: 200) + expect(web_site.has_security?).to be_truthy + end + + it 'returns false' do + stub_request(:get, web_site.security_url).to_return(status: 404) + expect(web_site.has_security?).to be_falsey + end + end + + describe '#parse_security_txt' do + + context 'installed in root' do + after :each do + stub_request_to_fixture(url: web_site.security_url, fixture: @fixture) + security = web_site.parse_security_txt + expect(security).to match_array @expected + end + + it 'returns an empty Array (empty security.txt)' do + @fixture = fixtures_dir + '/security_txt/empty_security.txt' + @expected = [] + end + + it 'returns an empty Array (invalid security.txt)' do + @fixture = fixtures_dir + '/security_txt/invalid_security.txt' + @expected = [] + end + + it 'returns some urls and some strings' do + @fixture = fixtures_dir + '/security_txt/invalid_security_2.txt' + @expected = %w( + /ÖÜ()=? + http://10.0.0.0/wp-includes/ + http://example.localhost/asdf/ + wooooza + ) + end + + it 'returns an Array of urls (valid security.txt)' do + @fixture = fixtures_dir + '/security_txt/security.txt' + @expected = %w( + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/wp-admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + end + + it 'removes duplicate entries from security.txt test 1' do + @fixture = fixtures_dir + '/security_txt/security_duplicate_1.txt' + @expected = %w( + http://example.localhost/wordpress/ + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/wp-admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + end + + it 'removes duplicate entries from security.txt test 2' do + @fixture = fixtures_dir + '/security_txt/security_duplicate_2.txt' + @expected = nil + end + end + + context 'installed in sub directory' do + it 'returns an Array of urls (valid security.txt, WP installed in subdir)' do + web_site_sub = WebSite.new('http://example.localhost/wordpress/') + fixture = fixtures_dir + '/security_txt/security.txt' + expected = %w( + http://example.localhost/wordpress/admin/ + http://example.localhost/wordpress/secret/ + http://example.localhost/Wordpress/wp-admin/ + http://example.localhost/wp-admin/tralling-space/ + http://example.localhost/asdf/ + ) + stub_request_to_fixture(url: web_site_sub.security_url, fixture: fixture) + security = web_site_sub.parse_security_txt + expect(security).to match_array expected + end + end + end + + describe '#known_dirs' do + it 'does not contain duplicates' do + expect(known_dirs.flatten.uniq.length).to eq known_dirs.length + end + end + +end diff --git a/wpscan.rb b/wpscan.rb index be4a0b0d..257ec151 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -219,6 +219,14 @@ def main end end + if wp_target.has_security? + puts info("security.txt available under: #{wp_target.security_url}") + + wp_target.parse_security_txt.each do |dir| + puts info("Interesting entry from security.txt: #{dir}") + end + end + if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") end From 991c87a89e18ca3515789f2a2ab60f3aca1840e8 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Wed, 9 May 2018 16:35:54 +0100 Subject: [PATCH 17/52] Fix inconsistencies with line endings --- wpscan.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index 257ec151..09dc7b1e 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -124,12 +124,12 @@ def main end if wp_target.ssl_error? - raise "The target site returned an SSL/TLS error. You can try again using the --disable-tls-checks option\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code" + raise "The target site returned an SSL/TLS error. You can try again using --disable-tls-checks\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code" end # Remote website up? unless wp_target.online? - raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking wpscan so you can try the --random-agent parameter." + raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking wpscan so you can try --random-agent" end if wpscan_options.proxy From 2b85b44bd15c80ded3ac54051a40efb81f90ac80 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 11:19:51 +0100 Subject: [PATCH 18/52] Add offline database update support --- Gemfile | 1 + lib/common/common_helper.rb | 20 +++++++++++++- wpscan.rb | 52 +++++++++++++++++++++++++++---------- 3 files changed, 59 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index fed1f734..b9001a26 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gem 'addressable', '>=2.5.0' gem 'yajl-ruby', '>=1.3.0' # Better JSON parser regarding memory usage gem 'terminal-table', '>=1.6.0' gem 'ruby-progressbar', '>=1.8.1' +gem 'rubyzip', '>=1.2.1' group :test do gem 'webmock', '>=2.3.2' diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 05c6a96d..983ccec0 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -12,6 +12,7 @@ MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') COLLECTIONS_LIB_DIR = File.join(COMMON_LIB_DIR, 'collections') DEFAULT_LOG_FILE = File.join(ROOT_DIR, 'log.txt') +DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip # Plugins directories COMMON_PLUGINS_DIR = File.join(COMMON_LIB_DIR, 'plugins') @@ -79,13 +80,30 @@ def add_trailing_slash(url) url =~ /\/$/ ? url : "#{url}/" end -def missing_db_file? +def missing_db_files? DbUpdater::FILES.each do |db_file| return true unless File.exist?(File.join(DATA_DIR, db_file)) end false end +# Find data.zip? +def has_db_zip? + return File.exist?(DATA_FILE)? true : false +end + +# Extract data.zip +def extract_db_zip + puts DATA_FILE + Zip::File.open(DATA_FILE) do |zip_file| + zip_file.each do |f| + f_path = File.join(DATA_DIR, f.name) + FileUtils.mkdir_p(File.dirname(f_path)) + zip_file.extract(f, f_path) + end + end +end + def last_update date = nil if File.exists?(LAST_UPDATE_FILE) diff --git a/wpscan.rb b/wpscan.rb index 09dc7b1e..988f954a 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -85,30 +85,56 @@ def main wpscan_options.to_h.merge(max_threads: wpscan_options.threads) ) - # Check if db file needs upgrade (older than 5 days) and we are not running in --batch mode + # Check if database needs upgrade (if its older than 5 days) and we are not running in --batch mode # Also no need to check if the user supplied the --update switch - if update_required? && !wpscan_options.batch && !wpscan_options.update + if update_required? and not wpscan_options.batch and not wpscan_options.update + # Banner puts puts notice('It seems like you have not updated the database for some time') puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil? + + # User prompt print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N] > ' - if (input = Readline.readline) =~ /^y/i - wpscan_options.update = true - elsif input =~ /^a/i + if (input = Readline.readline) =~ /^a/i puts 'Scan aborted' exit(1) - else - if missing_db_file? - puts critical('You can not run a scan without any databases. Manually extract the data.zip file.') - exit(1) - end + elsif input =~ /^y/i + wpscan_options.update = true + end + + # Is there a database to go on with? + if missing_db_files? and not wpscan_options.update + puts critical('You can not run a scan without any databases') + exit(1) end end + # Should we update? if wpscan_options.update - puts notice('Updating the Database ...') - DbUpdater.new(DATA_DIR).update(wpscan_options.verbose) - puts notice('Update completed') + online_update = true + + # Check for data.zip + if has_db_zip? + # User prompt + print '[?] Use the latest on-line database? Or use the off-line version? [O]n-line O[f]f-line [A]bort, default: [O] > ' + if (input = Readline.readline) =~ /^a/i + puts 'Scan aborted' + exit(1) + elsif input =~ /^f/i + online_update = false + end + end + + if online_update + puts notice('Updating the Database ...') + DbUpdater.new(DATA_DIR).update(wpscan_options.verbose) + puts notice('Update completed') + else + puts notice('Extracting the Database ...') + extract_db_zip + puts notice('Extraction completed') + end + # Exit program if only option --update is used exit(0) unless wpscan_options.url end From b65a4d0a60b9189e32de6980037e4a096415a623 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 11:20:03 +0100 Subject: [PATCH 19/52] Fix up gemfile --- Gemfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index b9001a26..03bc4df5 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,12 @@ source 'https://rubygems.org' -gem 'typhoeus', '>=1.1.2' -gem 'nokogiri', '>=1.7.0.1' gem 'addressable', '>=2.5.0' -gem 'yajl-ruby', '>=1.3.0' # Better JSON parser regarding memory usage -gem 'terminal-table', '>=1.6.0' +gem 'nokogiri', '>=1.7.0.1' gem 'ruby-progressbar', '>=1.8.1' gem 'rubyzip', '>=1.2.1' +gem 'terminal-table', '>=1.6.0' +gem 'typhoeus', '>=1.1.2' +gem 'yajl-ruby', '>=1.3.0' # Better JSON parser regarding memory usage group :test do gem 'webmock', '>=2.3.2' From ced94a73385d25e72a7eca662296b43cb808a582 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 11:20:20 +0100 Subject: [PATCH 20/52] Fix up .gitignore --- .gitignore | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 65c2aaf3..15d856b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ # OS Rubbish -cache/ coverage/ *.sublime-* .*.swp @@ -11,9 +10,12 @@ coverage/ .yardoc # WPScan +cache/ data/ +log.txt +wordlist.txt + +# WPScan (Dev) vendor/ debug.log -log.txt rspec_results.html -wordlist.txt From 0cd680bb29f3cb9f4873f4c559ecbdbf58fa4274 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 11:20:58 +0100 Subject: [PATCH 21/52] Add dev information to file locations --- lib/common/common_helper.rb | 50 +++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 983ccec0..baad2ff9 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -1,34 +1,35 @@ # encoding: UTF-8 -LIB_DIR = File.expand_path(File.join(__dir__, '..')) -ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # expand_path is used to get "wpscan/" instead of "wpscan/lib/../" -DATA_DIR = File.join(ROOT_DIR, 'data') -CONF_DIR = File.join(ROOT_DIR, 'conf') -CACHE_DIR = File.join(ROOT_DIR, 'cache') -WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') -UPDATER_LIB_DIR = File.join(LIB_DIR, 'updater') -COMMON_LIB_DIR = File.join(LIB_DIR, 'common') -MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') -COLLECTIONS_LIB_DIR = File.join(COMMON_LIB_DIR, 'collections') +LIB_DIR = File.expand_path(File.join(__dir__, '..')) # wpscan/lib/ +ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # wpscan/ - expand_path is used to get "wpscan/" instead of "wpscan/lib/../" +USER_DIR = File.expand_path(Dir.home) # ~/ +CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/ +DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/ +CONF_DIR = File.join(ROOT_DIR, 'conf') # wpscan/conf/ +COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/ +UPDATER_LIB_DIR = File.join(LIB_DIR, 'updater') # wpscan/lib/updater/ - Not used ATM +WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/ +COLLECTIONS_LIB_DIR = File.join(COMMON_LIB_DIR, 'collections') # wpscan/lib/common/collections/ - Not used ATM +MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/ -DEFAULT_LOG_FILE = File.join(ROOT_DIR, 'log.txt') +DEFAULT_LOG_FILE = File.join(USER_DIR, '.wpscan/log.txt') # ~/.wpscan/log.txt DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip # Plugins directories -COMMON_PLUGINS_DIR = File.join(COMMON_LIB_DIR, 'plugins') -WPSCAN_PLUGINS_DIR = File.join(WPSCAN_LIB_DIR, 'plugins') # Not used ATM +COMMON_PLUGINS_DIR = File.join(COMMON_LIB_DIR, 'plugins') # wpscan/lib/common/plugins/ - Not used ATM +WPSCAN_PLUGINS_DIR = File.join(WPSCAN_LIB_DIR, 'plugins') # wpscan/lib/common/plugins/ - Not used ATM -# Data files -WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') -PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') -THEMES_FILE = File.join(DATA_DIR, 'themes.json') -TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') -WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') -LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') -WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd') -LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd') -USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') -LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') +# Data files (data.zip) +LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update +LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') # ~/.wpscan/data/local_vulnerable_files.xml - Not used ATM +LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd') # ~/.wpscan/data/local_vulnerable_files.xsd - Not used ATM +PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') # ~/.wpscan/data/plugins.json +THEMES_FILE = File.join(DATA_DIR, 'themes.json') # ~/.wpscan/data/themes.json +TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') # ~/.wpscan/data/timthumbs.txt +USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') # ~/.wpscan/data/user-agents.txt +WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') # ~/.wpscan/data/wordpresses.json +WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') # ~/.wpscan/data/wp_versions.xml +WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd') # ~/.wpscan/data/wp_versions.xsd - Not used ATM MIN_RUBY_VERSION = '2.1.9' @@ -52,6 +53,7 @@ def windows? end require 'environment' +require 'zip' def escape_glob(s) s.gsub(/[\\\{\}\[\]\*\?]/) { |x| '\\' + x } From 05d27c64bebe0f4d8e4c0d437a6a27884633f372 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 11:21:14 +0100 Subject: [PATCH 22/52] Check location before using them --- lib/common/cache_file_store.rb | 4 ++++ lib/common/db_updater.rb | 9 +++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/common/cache_file_store.rb b/lib/common/cache_file_store.rb index 09af0714..98c0f331 100644 --- a/lib/common/cache_file_store.rb +++ b/lib/common/cache_file_store.rb @@ -26,6 +26,10 @@ class CacheFileStore unless Dir.exist?(@storage_path) FileUtils.mkdir_p(@storage_path) end + + unless Pathname.new(@storage_path).writable? + fail "#{@storage_path} is not writable" + end end def clean diff --git a/lib/common/db_updater.rb b/lib/common/db_updater.rb index eb833ac6..c2e1a5eb 100644 --- a/lib/common/db_updater.rb +++ b/lib/common/db_updater.rb @@ -13,8 +13,13 @@ class DbUpdater def initialize(repo_directory) @repo_directory = repo_directory - fail "#{repo_directory} is not writable" unless \ - Pathname.new(repo_directory).writable? + unless Dir.exist?(@repo_directory) + FileUtils.mkdir_p(@repo_directory) + end + + unless Pathname.new(@repo_directory).writable? + fail "#{@repo_directory} is not writable" + end end # @return [ Hash ] The params for Typhoeus::Request From fa430606ce984d6494fef6e6b59c42d8447d09e6 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 11:25:18 +0100 Subject: [PATCH 23/52] Move the last item to ~/.wpscan/ --- lib/common/common_helper.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index baad2ff9..aae7b3d5 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -1,25 +1,29 @@ # encoding: UTF-8 +# Location directories LIB_DIR = File.expand_path(File.join(__dir__, '..')) # wpscan/lib/ ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # wpscan/ - expand_path is used to get "wpscan/" instead of "wpscan/lib/../" USER_DIR = File.expand_path(Dir.home) # ~/ + +# Core WPScan directories CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/ DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/ -CONF_DIR = File.join(ROOT_DIR, 'conf') # wpscan/conf/ +CONF_DIR = File.join(ROOT_DIR, '.wpscan/conf') # ~/.wpscan/conf/ COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/ UPDATER_LIB_DIR = File.join(LIB_DIR, 'updater') # wpscan/lib/updater/ - Not used ATM WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/ COLLECTIONS_LIB_DIR = File.join(COMMON_LIB_DIR, 'collections') # wpscan/lib/common/collections/ - Not used ATM MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/ +# Core WPScan files DEFAULT_LOG_FILE = File.join(USER_DIR, '.wpscan/log.txt') # ~/.wpscan/log.txt DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip -# Plugins directories +# WPScan Plugins directories COMMON_PLUGINS_DIR = File.join(COMMON_LIB_DIR, 'plugins') # wpscan/lib/common/plugins/ - Not used ATM WPSCAN_PLUGINS_DIR = File.join(WPSCAN_LIB_DIR, 'plugins') # wpscan/lib/common/plugins/ - Not used ATM -# Data files (data.zip) +# WPScan Data files (data.zip) LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') # ~/.wpscan/data/local_vulnerable_files.xml - Not used ATM LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd') # ~/.wpscan/data/local_vulnerable_files.xsd - Not used ATM From f542a502135973634806239088c805730932e62a Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 12:24:11 +0100 Subject: [PATCH 24/52] Remove debug statement --- lib/common/common_helper.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index aae7b3d5..98185ce4 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -100,7 +100,6 @@ end # Extract data.zip def extract_db_zip - puts DATA_FILE Zip::File.open(DATA_FILE) do |zip_file| zip_file.each do |f| f_path = File.join(DATA_DIR, f.name) From 6cbc8c9924039bbf7ad777944eef15a520e62c03 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 16:58:47 +0100 Subject: [PATCH 25/52] Clean up some output confusion --- wpscan.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/wpscan.rb b/wpscan.rb index 988f954a..c00c059a 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -155,7 +155,13 @@ def main # Remote website up? unless wp_target.online? - raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking wpscan so you can try --random-agent" + if wpscan_options.user_agent + puts info("User-Agent: #{wpscan_options.user_agent}") + raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking the user-agent?" + else + raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking the wpscan user-agent, so you can try --random-agent" + end + end if wpscan_options.proxy From fea666587659fc8a8e8a3f5edf39cf5d7daf357e Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 16:59:25 +0100 Subject: [PATCH 26/52] Re-order output around slightly --- wpscan.rb | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index c00c059a..a389561f 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -235,6 +235,18 @@ def main puts info("User-Agent: #{wpscan_options.user_agent}") if wpscan_options.verbose and wpscan_options.user_agent puts + wp_target.interesting_headers.each do |header| + output = info('Interesting header: ') + + if header[1].class == Array + header[1].each do |value| + puts output + "#{header[0]}: #{value}" + end + else + puts output + "#{header[0]}: #{header[1]}" + end + end + if wp_target.has_robots? puts info("robots.txt available under: #{wp_target.robots_url}") @@ -259,10 +271,6 @@ def main end end - if wp_target.has_full_path_disclosure? - puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") - end - if wp_target.has_debug_log? puts critical("Debug log file found: #{wp_target.debug_log_url}") end @@ -279,18 +287,6 @@ def main puts critical("emergency.php has been found in: #{wp_target.emergency_url}") end - wp_target.interesting_headers.each do |header| - output = info('Interesting header: ') - - if header[1].class == Array - header[1].each do |value| - puts output + "#{header[0]}: #{value}" - end - else - puts output + "#{header[0]}: #{header[1]}" - end - end - if wp_target.multisite? puts info('This site seems to be a multisite (http://codex.wordpress.org/Glossary#Multisite)') end @@ -299,10 +295,6 @@ def main puts info("This site has 'Must Use Plugins' (http://codex.wordpress.org/Must_Use_Plugins)") end - if wp_target.registration_enabled? - puts warning("Registration is enabled: #{wp_target.registration_url}") - end - if wp_target.has_xml_rpc? puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}") end From ab67816dd9e1296207229e4cb8cb0edd39f35225 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 17:01:06 +0100 Subject: [PATCH 27/52] Check for API access and /wp-json/'s users output --- lib/common/common_helper.rb | 8 +++++ lib/wpscan/wp_target.rb | 18 +++++----- lib/wpscan/wp_target/wp_api.rb | 66 ++++++++++++++++++++++++++++++++++ wpscan.rb | 15 ++++++++ 4 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 lib/wpscan/wp_target/wp_api.rb diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 98185ce4..e2ff9112 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -304,3 +304,11 @@ end def url_encode(str) CGI.escape(str).gsub("+", "%20") end + +# Check valid JSON? +def valid_json?(json) + JSON.parse(json) + return true + rescue JSON::ParserError => e + return false +end \ No newline at end of file diff --git a/lib/wpscan/wp_target.rb b/lib/wpscan/wp_target.rb index 9fa0325e..32399056 100644 --- a/lib/wpscan/wp_target.rb +++ b/lib/wpscan/wp_target.rb @@ -1,22 +1,24 @@ # encoding: UTF-8 require 'web_site' -require 'wp_target/wp_readme' -require 'wp_target/wp_registrable' +require 'wp_target/wp_api' require 'wp_target/wp_config_backup' -require 'wp_target/wp_must_use_plugins' -require 'wp_target/wp_login_protection' require 'wp_target/wp_custom_directories' require 'wp_target/wp_full_path_disclosure' +require 'wp_target/wp_login_protection' +require 'wp_target/wp_must_use_plugins' +require 'wp_target/wp_readme' +require 'wp_target/wp_registrable' class WpTarget < WebSite - include WpTarget::WpReadme - include WpTarget::WpRegistrable + include WpTarget::WpAPI include WpTarget::WpConfigBackup - include WpTarget::WpMustUsePlugins - include WpTarget::WpLoginProtection include WpTarget::WpCustomDirectories include WpTarget::WpFullPathDisclosure + include WpTarget::WpLoginProtection + include WpTarget::WpMustUsePlugins + include WpTarget::WpReadme + include WpTarget::WpRegistrable attr_reader :verbose diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb new file mode 100644 index 00000000..f4c9dc15 --- /dev/null +++ b/lib/wpscan/wp_target/wp_api.rb @@ -0,0 +1,66 @@ +# encoding: UTF-8 + +class WpTarget < WebSite + module WpAPI + + # Checks to see if the REST API is enabled + # + # This by default in a WordPress installation since 4.5+ + # @return [ Boolean ] + def has_api?(url) + # Make the request + response = Browser.get(url) + + # Able to view the output? + if valid_json?(response.body) + # Read in JSON + data = JSON.parse(response.body) + + # If there is nothing there, return false + return false if data.empty? + + # WAF/API disabled response + return false if data.include?('message') and data['message'] =~ /Only authenticated users can access the REST API/ + + # Success! + return true if response.code == 200 + end + + # Something went wrong + return false + end + + # @return [ String ] The API/JSON URL + def json_url + @uri.merge('/wp-json/').to_s + end + + # @return [ String ] The API/JSON URL to show users + def json_users_url + @uri.merge('/wp-json/wp/v2/users').to_s + end + + # @return [ String ] The API/JSON URL to show users + def json_get_users(url) + # Make the request + response = Browser.get(url) + + # Able to view the output? + return false if not valid_json?(response.body) + + # Read in JSON + data = JSON.parse(response.body) + + # If there is nothing there, return false + return false if data.empty? + + # If not HTTP 200, return false + return false if response.code != 200 + + data.each do |child| + puts notice("ID: #{child['id']} | Name: #{child['name']}") + end + end + + end +end diff --git a/wpscan.rb b/wpscan.rb index a389561f..1a512e85 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -299,6 +299,21 @@ def main puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}") end + if wp_target.has_api?(wp_target.json_url) + puts info("API exposed: #{wp_target.json_url}") + + 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 + end + + if wp_target.has_full_path_disclosure? + puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") + end + if wp_target.upload_directory_listing_enabled? puts warning("Upload directory has directory listing enabled: #{wp_target.upload_dir_url}") end From 285b1a173305217ab711c36a30cf495814bfdaea Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 17:10:02 +0100 Subject: [PATCH 28/52] Cleaner output and fix a typo --- wpscan.rb | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index 1a512e85..f8704a28 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -245,6 +245,7 @@ def main else puts output + "#{header[0]}: #{header[1]}" end + puts " - - - - -" end if wp_target.has_robots? @@ -253,6 +254,7 @@ def main wp_target.parse_robots_txt.each do |dir| puts info("Interesting entry from robots.txt: #{dir}") end + puts " - - - - -" end if wp_target.has_humans? @@ -261,6 +263,7 @@ def main wp_target.parse_humans_txt.each do |dir| puts info("Interesting entry from humans.txt: #{dir}") end + puts " - - - - -" end if wp_target.has_security? @@ -269,34 +272,42 @@ def main wp_target.parse_security_txt.each do |dir| puts info("Interesting entry from security.txt: #{dir}") end + puts " - - - - -" end if wp_target.has_debug_log? puts critical("Debug log file found: #{wp_target.debug_log_url}") + puts " - - - - -" end wp_target.config_backup.each do |file_url| puts critical("A wp-config.php backup file has been found in: #{file_url}") + puts " - - - - -" end if wp_target.search_replace_db_2_exists? puts critical("searchreplacedb2.php has been found in: #{wp_target.search_replace_db_2_url}") + puts " - - - - -" end if wp_target.emergency_exists? puts critical("emergency.php has been found in: #{wp_target.emergency_url}") + puts " - - - - -" end if wp_target.multisite? puts info('This site seems to be a multisite (http://codex.wordpress.org/Glossary#Multisite)') + puts " - - - - -" end if wp_target.has_must_use_plugins? puts info("This site has 'Must Use Plugins' (http://codex.wordpress.org/Must_Use_Plugins)") + puts " - - - - -" end if wp_target.has_xml_rpc? puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}") + puts " - - - - -" end if wp_target.has_api?(wp_target.json_url) @@ -308,18 +319,22 @@ def main # Print users from JSON wp_target.json_get_users(wp_target.json_users_url) end + puts " - - - - -" end if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") + puts " - - - - -" end if wp_target.upload_directory_listing_enabled? puts warning("Upload directory has directory listing enabled: #{wp_target.upload_dir_url}") + puts " - - - - -" end if wp_target.include_directory_listing_enabled? puts warning("Includes directory has directory listing enabled: #{wp_target.includes_dir_url}") + puts " - - - - -" end enum_options = { @@ -337,6 +352,7 @@ def main puts puts notice('WordPress version can not be detected') end + puts " - - - - -" if wp_theme = wp_target.theme puts @@ -355,7 +371,7 @@ def main parent.output(wpscan_options.verbose) wp_theme = parent end - + puts " - - - - -" end if wpscan_options.enumerate_plugins == nil and wpscan_options.enumerate_only_vulnerable_plugins == nil @@ -373,6 +389,7 @@ def main else puts info('No plugins found') end + puts " - - - - -" end # Enumerate the installed plugins @@ -403,13 +420,14 @@ def main puts if !wp_plugins.empty? - grammar = wp_themes.size.equals == 1 ? "" : "s" + grammar = wp_themes.size == 1 ? "" : "s" puts info("We found #{wp_plugins.size} plugin#{grammar}:") wp_plugins.output(wpscan_options.verbose) else puts info('No plugins found') end + puts " - - - - -" end # Enumerate installed themes @@ -439,13 +457,14 @@ def main ) puts if !wp_themes.empty? - grammar = wp_themes.size.equals == 1 ? "" : "s" + grammar = wp_themes.size == 1 ? "" : "s" puts info("We found #{wp_themes.size} theme#{grammar}:") wp_themes.output(wpscan_options.verbose) else puts info('No themes found') end + puts " - - - - -" end if wpscan_options.enumerate_timthumbs @@ -461,13 +480,14 @@ def main ) puts if !wp_timthumbs.empty? - grammar = wp_timthumbs.size.equals == 1 ? "" : "s" + grammar = wp_timthumbs.size == 1 ? "" : "s" puts info("We found #{wp_timthumbs.size} timthumb file#{grammar}:") wp_timthumbs.output(wpscan_options.verbose) else puts info('No timthumb files found') end + puts " - - - - -" end # If we haven't been supplied a username/usernames list, enumerate them... @@ -495,7 +515,7 @@ def main exit(1) end else - grammar = wp_users.size.equals == 1 ? "" : "s" + grammar = wp_users.size == 1 ? "" : "s" puts info("We identified the following #{wp_users.size} user#{grammar}:") wp_users.output(margin_left: ' ' * 4) if wp_users[0].login == "admin" @@ -513,6 +533,7 @@ def main else wp_users << WpUser.new(wp_target.uri, login: wpscan_options.username) end + puts " - - - - -" end # Start the brute forcer @@ -545,6 +566,7 @@ def main else puts critical('Brute forcing aborted') end + puts " - - - - -" end stop_time = Time.now @@ -553,9 +575,9 @@ def main puts puts info("Finished: #{stop_time.asctime}") - puts info("Requests Done: #{@total_requests_done}") - puts info("Memory used: #{used_memory.bytes_to_human}") unless windows? puts info("Elapsed time: #{Time.at(elapsed).utc.strftime('%H:%M:%S')}") + puts info("Requests made: #{@total_requests_done}") + puts info("Memory used: #{used_memory.bytes_to_human}") unless windows? # do nothing on interrupt rescue Interrupt From 1d0128af72be8dcf66efbba44fb92546defc9134 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 18:07:57 +0100 Subject: [PATCH 29/52] Move spacer to a function --- lib/common/common_helper.rb | 5 ++++ wpscan.rb | 48 ++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index e2ff9112..ee4468f3 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -191,6 +191,11 @@ def banner puts end +# Space out sections +def spacer + puts " - - - - -" +end + def xml(file) Nokogiri::XML(File.open(file)) do |config| config.noblanks diff --git a/wpscan.rb b/wpscan.rb index f8704a28..13718a1d 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -233,7 +233,7 @@ def main puts info("URL: #{wp_target.url}") puts info("Started: #{start_time.asctime}") puts info("User-Agent: #{wpscan_options.user_agent}") if wpscan_options.verbose and wpscan_options.user_agent - puts + spacer() wp_target.interesting_headers.each do |header| output = info('Interesting header: ') @@ -245,8 +245,8 @@ def main else puts output + "#{header[0]}: #{header[1]}" end - puts " - - - - -" end + spacer() if wp_target.has_robots? puts info("robots.txt available under: #{wp_target.robots_url}") @@ -254,7 +254,7 @@ def main wp_target.parse_robots_txt.each do |dir| puts info("Interesting entry from robots.txt: #{dir}") end - puts " - - - - -" + spacer() end if wp_target.has_humans? @@ -263,7 +263,7 @@ def main wp_target.parse_humans_txt.each do |dir| puts info("Interesting entry from humans.txt: #{dir}") end - puts " - - - - -" + spacer() end if wp_target.has_security? @@ -272,42 +272,42 @@ def main wp_target.parse_security_txt.each do |dir| puts info("Interesting entry from security.txt: #{dir}") end - puts " - - - - -" + spacer() end if wp_target.has_debug_log? puts critical("Debug log file found: #{wp_target.debug_log_url}") - puts " - - - - -" + spacer() end wp_target.config_backup.each do |file_url| puts critical("A wp-config.php backup file has been found in: #{file_url}") - puts " - - - - -" + spacer() end if wp_target.search_replace_db_2_exists? puts critical("searchreplacedb2.php has been found in: #{wp_target.search_replace_db_2_url}") - puts " - - - - -" + spacer() end if wp_target.emergency_exists? puts critical("emergency.php has been found in: #{wp_target.emergency_url}") - puts " - - - - -" + spacer() end if wp_target.multisite? puts info('This site seems to be a multisite (http://codex.wordpress.org/Glossary#Multisite)') - puts " - - - - -" + spacer() end if wp_target.has_must_use_plugins? puts info("This site has 'Must Use Plugins' (http://codex.wordpress.org/Must_Use_Plugins)") - puts " - - - - -" + spacer() end if wp_target.has_xml_rpc? puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}") - puts " - - - - -" + spacer() end if wp_target.has_api?(wp_target.json_url) @@ -319,22 +319,22 @@ def main # Print users from JSON wp_target.json_get_users(wp_target.json_users_url) end - puts " - - - - -" + spacer() end if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") - puts " - - - - -" + spacer() end if wp_target.upload_directory_listing_enabled? puts warning("Upload directory has directory listing enabled: #{wp_target.upload_dir_url}") - puts " - - - - -" + spacer() end if wp_target.include_directory_listing_enabled? puts warning("Includes directory has directory listing enabled: #{wp_target.includes_dir_url}") - puts " - - - - -" + spacer() end enum_options = { @@ -352,7 +352,7 @@ def main puts puts notice('WordPress version can not be detected') end - puts " - - - - -" + spacer() if wp_theme = wp_target.theme puts @@ -371,7 +371,7 @@ def main parent.output(wpscan_options.verbose) wp_theme = parent end - puts " - - - - -" + spacer() end if wpscan_options.enumerate_plugins == nil and wpscan_options.enumerate_only_vulnerable_plugins == nil @@ -389,7 +389,7 @@ def main else puts info('No plugins found') end - puts " - - - - -" + spacer() end # Enumerate the installed plugins @@ -427,7 +427,7 @@ def main else puts info('No plugins found') end - puts " - - - - -" + spacer() end # Enumerate installed themes @@ -464,7 +464,7 @@ def main else puts info('No themes found') end - puts " - - - - -" + spacer() end if wpscan_options.enumerate_timthumbs @@ -487,7 +487,7 @@ def main else puts info('No timthumb files found') end - puts " - - - - -" + spacer() end # If we haven't been supplied a username/usernames list, enumerate them... @@ -533,7 +533,7 @@ def main else wp_users << WpUser.new(wp_target.uri, login: wpscan_options.username) end - puts " - - - - -" + spacer() end # Start the brute forcer @@ -566,7 +566,7 @@ def main else puts critical('Brute forcing aborted') end - puts " - - - - -" + spacer() end stop_time = Time.now From de960ff9db9e356ea93b523caabaa9c891ce2945 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Fri, 11 May 2018 18:18:19 +0100 Subject: [PATCH 30/52] Fix offline extraction zip bug --- lib/common/common_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index ee4468f3..a9a9aa88 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -102,7 +102,7 @@ end def extract_db_zip Zip::File.open(DATA_FILE) do |zip_file| zip_file.each do |f| - f_path = File.join(DATA_DIR, f.name) + f_path = File.join(DATA_DIR, File.basename(f.name)) FileUtils.mkdir_p(File.dirname(f_path)) zip_file.extract(f, f_path) end From 0e05f77fb7b7e2aa535ec8879ae1f90d531680f1 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 13:37:34 +0100 Subject: [PATCH 31/52] Made offline extraction more verbose --- lib/common/common_helper.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index a9a9aa88..37a134f2 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -102,8 +102,18 @@ end def extract_db_zip Zip::File.open(DATA_FILE) do |zip_file| zip_file.each do |f| + # Feedback to the user + puts "[+] Extracting: #{File.basename(f.name)}" if verbose f_path = File.join(DATA_DIR, File.basename(f.name)) + + # Create folder FileUtils.mkdir_p(File.dirname(f_path)) + + # Delete if already there + puts "[+] Deleting: #{File.basename(f.name)}" if verbose and File.exist?(f_path) + FileUtils.rm(f_path) if File.exist?(f_path) + + # Extract zip_file.extract(f, f_path) end end From 24e6820a909c2a0f75ad047e62d1bc5bf17391e9 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 13:43:33 +0100 Subject: [PATCH 32/52] Clean up wording --- lib/common/collections/wp_users/output.rb | 2 +- lib/common/db_updater.rb | 2 +- wpscan.rb | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/common/collections/wp_users/output.rb b/lib/common/collections/wp_users/output.rb index c7000dbb..b6516c1a 100644 --- a/lib/common/collections/wp_users/output.rb +++ b/lib/common/collections/wp_users/output.rb @@ -9,7 +9,7 @@ class WpUsers < WpItems # @return [ void ] def output(options = {}) rows = [] - headings = ['Id', 'Login', 'Name'] + headings = ['ID', 'Login', 'Name'] headings << 'Password' if options[:show_password] remove_junk_from_display_names diff --git a/lib/common/db_updater.rb b/lib/common/db_updater.rb index c2e1a5eb..0ad64860 100644 --- a/lib/common/db_updater.rb +++ b/lib/common/db_updater.rb @@ -88,7 +88,7 @@ class DbUpdater def update(verbose = false) FILES.each do |filename| begin - puts "[+] Checking #{filename}" if verbose + puts "[+] Checking: #{filename}" if verbose db_checksum = remote_file_checksum(filename) # Checking if the file needs to be updated diff --git a/wpscan.rb b/wpscan.rb index 13718a1d..3c480d54 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -116,10 +116,9 @@ def main # Check for data.zip if has_db_zip? # User prompt - print '[?] Use the latest on-line database? Or use the off-line version? [O]n-line O[f]f-line [A]bort, default: [O] > ' + print '[?] Use the latest on-line database? Or use the off-line copy? [O]n-line O[f]f-line [A]bort update, default: [O] > ' if (input = Readline.readline) =~ /^a/i - puts 'Scan aborted' - exit(1) + puts 'Update aborted' elsif input =~ /^f/i online_update = false end @@ -310,9 +309,11 @@ def main 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}") + # 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}") From ae3c164350e6f2af688437331b2531d914dded0c Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 13:43:49 +0100 Subject: [PATCH 33/52] Improved API output results --- lib/wpscan/wp_target/wp_api.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index f4c9dc15..ca1cfe36 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -42,6 +42,9 @@ class WpTarget < WebSite # @return [ String ] The API/JSON URL to show users def json_get_users(url) + # Variables + users = [] + # Make the request response = Browser.get(url) @@ -57,9 +60,19 @@ class WpTarget < WebSite # If not HTTP 200, return false return false if response.code != 200 + # Add to array data.each do |child| - puts notice("ID: #{child['id']} | Name: #{child['name']}") + row = [ child['id'], child['name'], child['link'] ] + users << row end + + # Sort and uniq + users = users.sort.uniq + + # Print results + table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], + rows: users) + puts table end end From 9450ba6cc5fb5be6a9036e54b7f32486897e8263 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 13:44:02 +0100 Subject: [PATCH 34/52] Add RSS author information --- lib/wpscan/web_site.rb | 7 ---- lib/wpscan/wp_target.rb | 2 ++ lib/wpscan/wp_target/wp_rss.rb | 59 ++++++++++++++++++++++++++++++++++ wpscan.rb | 12 +++++++ 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 lib/wpscan/wp_target/wp_rss.rb diff --git a/lib/wpscan/web_site.rb b/lib/wpscan/web_site.rb index 3779210b..5b2b445c 100644 --- a/lib/wpscan/web_site.rb +++ b/lib/wpscan/web_site.rb @@ -125,13 +125,6 @@ class WebSite @error_404_hash end - # Will try to find the rss url in the homepage - # Only the first one found is returned - def rss_url - homepage_body = Browser.get(@uri.to_s).body - homepage_body[%r{}, 1] - end - # Only the first 700 bytes are checked to avoid the download # of the whole file which can be very huge (like 2 Go) # diff --git a/lib/wpscan/wp_target.rb b/lib/wpscan/wp_target.rb index 32399056..f9f7c688 100644 --- a/lib/wpscan/wp_target.rb +++ b/lib/wpscan/wp_target.rb @@ -9,6 +9,7 @@ require 'wp_target/wp_login_protection' require 'wp_target/wp_must_use_plugins' require 'wp_target/wp_readme' require 'wp_target/wp_registrable' +require 'wp_target/wp_rss' class WpTarget < WebSite include WpTarget::WpAPI @@ -19,6 +20,7 @@ class WpTarget < WebSite include WpTarget::WpMustUsePlugins include WpTarget::WpReadme include WpTarget::WpRegistrable + include WpTarget::WpRSS attr_reader :verbose diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb new file mode 100644 index 00000000..22a53176 --- /dev/null +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -0,0 +1,59 @@ +# encoding: UTF-8 + +class WpTarget < WebSite + module WpRSS + + # Checks to see if there is an rss feed + # Will try to find the rss url in the homepage + # Only the first one found is returned + # + # This file comes by default in a WordPress installation + # + # @return [ Boolean ] + def rss_url + homepage_body = Browser.get(@uri.to_s).body + # Format: + homepage_body[%r{}i, 1] + end + + + # Gets all the authors from the RSS feed + # + # @return [ string ] + def rss_authors(url) + # Variables + users = [] + + # Make the request + response = Browser.get(url) + + # Valid repose to view? HTTP 200? + return false unless response.code == 200 + + # Get output + data = response.body + + # Read in RSS/XML + xml = Nokogiri::XML(data) + + # Look for item + xml.xpath('//item/dc:creator').each do |node| + #Format: + users << [%r{.*}i.match(node).to_s] + end + + if users + # Feedback + puts warning("Detected users from RSS feed:") + + # Sort and uniq + users = users.sort_by { |user| user.to_s.downcase }.uniq + + # Print results + table = Terminal::Table.new(headings: ['Name'], + rows: users) + puts table + end + end + end +end diff --git a/wpscan.rb b/wpscan.rb index 3c480d54..bf8d2b66 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -323,6 +323,18 @@ def main spacer() end + # Get RSS + rss = wp_target.rss_url + if rss + # Feedback + puts info("RSS Feed: #{rss}") + + # Print users from RSS feed + wp_target.rss_authors(rss) + + spacer() + end + if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") spacer() From e41aab3a80b8dbe1cda8e9b88f27c6ddfe4d87b5 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 15:12:20 +0100 Subject: [PATCH 35/52] Re-worked off-line update only as a fall back (when possible) --- wpscan.rb | 45 +++++++++++++++++---------------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/wpscan.rb b/wpscan.rb index bf8d2b66..6ef9aadd 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -87,52 +87,41 @@ def main # Check if database needs upgrade (if its older than 5 days) and we are not running in --batch mode # Also no need to check if the user supplied the --update switch - if update_required? and not wpscan_options.batch and not wpscan_options.update + if update_required? and not wpscan_options.batch and not wpscan_options.update # Banner puts puts notice('It seems like you have not updated the database for some time') puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil? # User prompt - print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N] > ' + print '[?] Do you want to update now? [Y]es [N]o [A]bort update, default: [N] > ' if (input = Readline.readline) =~ /^a/i - puts 'Scan aborted' - exit(1) + puts 'Update aborted' elsif input =~ /^y/i wpscan_options.update = true end # Is there a database to go on with? if missing_db_files? and not wpscan_options.update - puts critical('You can not run a scan without any databases') - exit(1) + # Check for data.zip + if has_db_zip? + puts notice('Extracting the Database ...') + # Extract data.zip + extract_db_zip + puts notice('Extraction completed') + # Missing, so can't go on! + else + puts critical('You can not run a scan without any databases') + exit(1) + end end end # Should we update? if wpscan_options.update - online_update = true - - # Check for data.zip - if has_db_zip? - # User prompt - print '[?] Use the latest on-line database? Or use the off-line copy? [O]n-line O[f]f-line [A]bort update, default: [O] > ' - if (input = Readline.readline) =~ /^a/i - puts 'Update aborted' - elsif input =~ /^f/i - online_update = false - end - end - - if online_update - puts notice('Updating the Database ...') - DbUpdater.new(DATA_DIR).update(wpscan_options.verbose) - puts notice('Update completed') - else - puts notice('Extracting the Database ...') - extract_db_zip - puts notice('Extraction completed') - end + puts notice('Updating the Database ...') + DbUpdater.new(DATA_DIR).update(wpscan_options.verbose) + puts notice('Update completed') # Exit program if only option --update is used exit(0) unless wpscan_options.url From 3b94fc49a7804a585324dd83541f5c2036d6ab8f Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 15:12:35 +0100 Subject: [PATCH 36/52] Fix EOL issue when checking /robots.txt --- lib/wpscan/web_site/robots_txt.rb | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/wpscan/web_site/robots_txt.rb b/lib/wpscan/web_site/robots_txt.rb index 2e928152..d8d52cfb 100644 --- a/lib/wpscan/web_site/robots_txt.rb +++ b/lib/wpscan/web_site/robots_txt.rb @@ -23,20 +23,32 @@ class WebSite return_object = [] response = Browser.get(robots_url.to_s) body = response.body + # Get all allow and disallow urls entries = body.scan(/^(?:dis)?allow:\s*(.*)$/i) 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! + # Wordpress URL wordpress_path = @uri.path + + # Each "boring" value as defined below, remove RobotsTxt.known_dirs.each do |d| entries.delete(d) - # also delete when wordpress is installed in subdir + # Also delete when wordpress is installed in subdir dir_with_subdir = "#{wordpress_path}/#{d}".gsub(/\/+/, '/') entries.delete(dir_with_subdir) end + # Each value now, try and make it a full URL entries.each do |d| begin temp = @uri.clone @@ -46,17 +58,21 @@ class WebSite end return_object << temp.to_s end + end return_object end protected + # Useful ~ "function do_robots()" -> https://github.com/WordPress/WordPress/blob/master/wp-includes/functions.php + # # @return [ Array ] def self.known_dirs %w{ / /wp-admin/ + /wp-admin/admin-ajax.php /wp-includes/ /wp-content/ } From 4b4b9687100eba4d9b499fbfe84fcd42118c5dc2 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 15:57:33 +0100 Subject: [PATCH 37/52] Check HTTP status of each value in /robots.txt --- lib/wpscan/web_site/robots_txt.rb | 6 ++++++ wpscan.rb | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/wpscan/web_site/robots_txt.rb b/lib/wpscan/web_site/robots_txt.rb index d8d52cfb..2a10893d 100644 --- a/lib/wpscan/web_site/robots_txt.rb +++ b/lib/wpscan/web_site/robots_txt.rb @@ -15,6 +15,12 @@ class WebSite @uri.clone.merge('robots.txt').to_s end + # Check status code for each robots.txt entry + def header_robots_txt(url) + code = Browser.get(url).code + puts info("Interesting entry from robots.txt: #{url} [HTTP #{code}]") + end + # Parse robots.txt # @return [ Array ] URLs generated from robots.txt def parse_robots_txt diff --git a/wpscan.rb b/wpscan.rb index 6ef9aadd..f9b40364 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -240,7 +240,7 @@ def main puts info("robots.txt available under: #{wp_target.robots_url}") wp_target.parse_robots_txt.each do |dir| - puts info("Interesting entry from robots.txt: #{dir}") + wp_target.header_robots_txt(dir) end spacer() end From 38f70a88aec742fa15ebe722627eb12c9152e4c0 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 16:17:12 +0100 Subject: [PATCH 38/52] Follow any redirections (e.g. http -> https) --- lib/wpscan/wp_target/wp_rss.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb index 22a53176..334eaf19 100644 --- a/lib/wpscan/wp_target/wp_rss.rb +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -25,7 +25,7 @@ class WpTarget < WebSite users = [] # Make the request - response = Browser.get(url) + response = Browser.get(url, followlocation: true) # Valid repose to view? HTTP 200? return false unless response.code == 200 From 715d3d4ad6431391483614941081d74b6b6eb271 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 16:35:41 +0100 Subject: [PATCH 39/52] Moved http response to a function --- lib/common/common_helper.rb | 5 +++++ lib/wpscan/web_site/robots_txt.rb | 6 ------ wpscan.rb | 23 +++++++++++++++++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 37a134f2..d564c8d5 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -326,4 +326,9 @@ def valid_json?(json) return true rescue JSON::ParserError => e return false +end + +# Get the HTTP response code +def get_http_status(url) + Browser.get(url.to_s).code end \ No newline at end of file diff --git a/lib/wpscan/web_site/robots_txt.rb b/lib/wpscan/web_site/robots_txt.rb index 2a10893d..d8d52cfb 100644 --- a/lib/wpscan/web_site/robots_txt.rb +++ b/lib/wpscan/web_site/robots_txt.rb @@ -15,12 +15,6 @@ class WebSite @uri.clone.merge('robots.txt').to_s end - # Check status code for each robots.txt entry - def header_robots_txt(url) - code = Browser.get(url).code - puts info("Interesting entry from robots.txt: #{url} [HTTP #{code}]") - end - # Parse robots.txt # @return [ Array ] URLs generated from robots.txt def parse_robots_txt diff --git a/wpscan.rb b/wpscan.rb index f9b40364..67f7bc90 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -237,16 +237,29 @@ def main spacer() if wp_target.has_robots? - puts info("robots.txt available under: #{wp_target.robots_url}") + code = get_http_status(wp_target.robots_url) + puts info("robots.txt available under: #{wp_target.robots_url} [HTTP #{code}]") wp_target.parse_robots_txt.each do |dir| - wp_target.header_robots_txt(dir) + code = get_http_status(dir) + puts info("Interesting entry from robots.txt: #{dir} [HTTP #{code}]") + end + spacer() + end + + if wp_target.has_sitemap? + puts info("Sitemap found: #{wp_target.sitemap_url}") + + wp_target.parse_sitemap.each do |dir| + code = get_http_status(dir) + puts info("Sitemap entry: #{dir} [HTTP #{code}]") end spacer() end if wp_target.has_humans? - puts info("humans.txt available under: #{wp_target.humans_url}") + code = get_http_status(wp_target.humans_url) + puts info("humans.txt available under: #{wp_target.humans_url} [HTTP #{code}]") wp_target.parse_humans_txt.each do |dir| puts info("Interesting entry from humans.txt: #{dir}") @@ -315,8 +328,10 @@ def main # Get RSS rss = wp_target.rss_url if rss + code = get_http_status(rss) + # Feedback - puts info("RSS Feed: #{rss}") + puts info("Found an RSS Feed: #{rss} [HTTP #{code}]") # Print users from RSS feed wp_target.rss_authors(rss) From 4333ecb989bb9cdde33d60aa6b1561b712b9082d Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 16:36:52 +0100 Subject: [PATCH 40/52] Check for sitemaps (using /robots.txt) --- lib/wpscan/web_site.rb | 2 + lib/wpscan/web_site/sitemap.rb | 68 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 lib/wpscan/web_site/sitemap.rb diff --git a/lib/wpscan/web_site.rb b/lib/wpscan/web_site.rb index 5b2b445c..cfa67389 100644 --- a/lib/wpscan/web_site.rb +++ b/lib/wpscan/web_site.rb @@ -4,12 +4,14 @@ require 'web_site/robots_txt' require 'web_site/humans_txt' require 'web_site/security_txt' require 'web_site/interesting_headers' +require 'web_site/sitemap' class WebSite include WebSite::RobotsTxt include WebSite::HumansTxt include WebSite::SecurityTxt include WebSite::InterestingHeaders + include WebSite::Sitemap attr_reader :uri diff --git a/lib/wpscan/web_site/sitemap.rb b/lib/wpscan/web_site/sitemap.rb new file mode 100644 index 00000000..f6aae16e --- /dev/null +++ b/lib/wpscan/web_site/sitemap.rb @@ -0,0 +1,68 @@ +# encoding: UTF-8 + +class WebSite + module Sitemap + + # Checks if a sitemap.txt file exists + # @return [ Boolean ] + def has_sitemap? + # Make the request + response = Browser.get(sitemap_url) + + # Make sure its HTTP 200 + return false unless response.code == 200 + + # Is there a sitemap value? + result = response.body.scan(/^sitemap\s*:\s*(.*)$/i) + return true if result[0] + return false + end + + # Gets a robots.txt URL + # @return [ String ] + def sitemap_url + @uri.clone.merge('robots.txt').to_s + end + + # Parse robots.txt + # @return [ Array ] URLs generated from robots.txt + def parse_sitemap + return_object = [] + + # 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) + + # 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! + + # 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 + + end + return_object + end + + end +end From b9fa1e35876f2078b5c1ccc6c6f2d72e4ad8880e Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 16:37:14 +0100 Subject: [PATCH 41/52] Misc fixes and typos --- lib/common/common_helper.rb | 6 +++--- lib/wpscan/web_site.rb | 8 ++++---- lib/wpscan/web_site/humans_txt.rb | 7 ++++--- lib/wpscan/web_site/robots_txt.rb | 9 +++++---- lib/wpscan/web_site/security_txt.rb | 8 +++++--- lib/wpscan/wp_target/wp_api.rb | 15 ++++++++------- wpscan.rb | 3 ++- 7 files changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index d564c8d5..2580a70e 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -8,7 +8,7 @@ USER_DIR = File.expand_path(Dir.home) # ~/ # Core WPScan directories CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/ DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/ -CONF_DIR = File.join(ROOT_DIR, '.wpscan/conf') # ~/.wpscan/conf/ +CONF_DIR = File.join(USER_DIR, '.wpscan/conf') # ~/.wpscan/conf/ COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/ UPDATER_LIB_DIR = File.join(LIB_DIR, 'updater') # wpscan/lib/updater/ - Not used ATM WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/ @@ -103,14 +103,14 @@ def extract_db_zip Zip::File.open(DATA_FILE) do |zip_file| zip_file.each do |f| # Feedback to the user - puts "[+] Extracting: #{File.basename(f.name)}" if verbose + #puts "[+] Extracting: #{File.basename(f.name)}" f_path = File.join(DATA_DIR, File.basename(f.name)) # Create folder FileUtils.mkdir_p(File.dirname(f_path)) # Delete if already there - puts "[+] Deleting: #{File.basename(f.name)}" if verbose and File.exist?(f_path) + #puts "[+] Deleting: #{File.basename(f.name)}" if File.exist?(f_path) FileUtils.rm(f_path) if File.exist?(f_path) # Extract diff --git a/lib/wpscan/web_site.rb b/lib/wpscan/web_site.rb index cfa67389..a377e5e8 100644 --- a/lib/wpscan/web_site.rb +++ b/lib/wpscan/web_site.rb @@ -1,16 +1,16 @@ # encoding: UTF-8 -require 'web_site/robots_txt' require 'web_site/humans_txt' -require 'web_site/security_txt' require 'web_site/interesting_headers' +require 'web_site/robots_txt' +require 'web_site/security_txt' require 'web_site/sitemap' class WebSite - include WebSite::RobotsTxt include WebSite::HumansTxt - include WebSite::SecurityTxt include WebSite::InterestingHeaders + include WebSite::RobotsTxt + include WebSite::SecurityTxt include WebSite::Sitemap attr_reader :uri diff --git a/lib/wpscan/web_site/humans_txt.rb b/lib/wpscan/web_site/humans_txt.rb index 4a4a23d9..0c25de50 100644 --- a/lib/wpscan/web_site/humans_txt.rb +++ b/lib/wpscan/web_site/humans_txt.rb @@ -18,11 +18,12 @@ class WebSite # Parse humans.txt # @return [ Array ] URLs generated from humans.txt def parse_humans_txt - return unless has_humans? - return_object = [] response = Browser.get(humans_url.to_s) - entries = response.body.split(/\n/) + body = response.body + + entries = body.split(/\n/) + if entries entries.flatten! entries.uniq! diff --git a/lib/wpscan/web_site/robots_txt.rb b/lib/wpscan/web_site/robots_txt.rb index d8d52cfb..1a4cae85 100644 --- a/lib/wpscan/web_site/robots_txt.rb +++ b/lib/wpscan/web_site/robots_txt.rb @@ -18,16 +18,18 @@ class WebSite # Parse robots.txt # @return [ Array ] URLs generated from robots.txt def parse_robots_txt - return unless has_robots? - return_object = [] + + # Make request response = Browser.get(robots_url.to_s) body = response.body # Get all allow and disallow urls entries = body.scan(/^(?:dis)?allow:\s*(.*)$/i) + + # Did we get something? if entries - #extract elements + # Extract elements entries.flatten! # Remove any leading/trailing spaces entries.collect{|x| x.strip || x } @@ -77,6 +79,5 @@ class WebSite /wp-content/ } end - end end diff --git a/lib/wpscan/web_site/security_txt.rb b/lib/wpscan/web_site/security_txt.rb index 3464fd72..e19be594 100644 --- a/lib/wpscan/web_site/security_txt.rb +++ b/lib/wpscan/web_site/security_txt.rb @@ -18,11 +18,13 @@ class WebSite # Parse security.txt # @return [ Array ] URLs generated from security.txt def parse_security_txt - return unless has_security? - return_object = [] response = Browser.get(security_url.to_s) - entries = response.body.split(/\n/) + body = response.body + + # Get all non-comments + entries = body.split(/\n/) + if entries entries.flatten! entries.uniq! diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index ca1cfe36..fe1c7942 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -66,14 +66,15 @@ class WpTarget < WebSite users << row end - # Sort and uniq - users = users.sort.uniq + if users + # Sort and uniq + users = users.sort.uniq - # Print results - table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], - rows: users) - puts table + # Print results + table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], + rows: users) + puts table + end end - end end diff --git a/wpscan.rb b/wpscan.rb index 67f7bc90..d9fcbfdc 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -268,7 +268,8 @@ def main end if wp_target.has_security? - puts info("security.txt available under: #{wp_target.security_url}") + code = get_http_status(wp_target.humans_url) + puts info("security.txt available under: #{wp_target.security_url} [HTTP #{code}]") wp_target.parse_security_txt.each do |dir| puts info("Interesting entry from security.txt: #{dir}") From f90a64ce81fa45497c073d22fe243c2f97d3c228 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 17:56:49 +0100 Subject: [PATCH 42/52] Tried to make code climate happy --- lib/common/collections/wp_items/detectable.rb | 2 +- lib/common/common_helper.rb | 22 +++++++--- lib/wpscan/web_site/humans_txt.rb | 19 +++------ lib/wpscan/web_site/robots_txt.rb | 31 ++++---------- lib/wpscan/web_site/security_txt.rb | 18 ++------ lib/wpscan/web_site/sitemap.rb | 31 ++++---------- lib/wpscan/wp_target/wp_api.rb | 16 +++++--- lib/wpscan/wp_target/wp_rss.rb | 7 ++-- lib/wpscan/wpscan_helper.rb | 34 +++++++++++++++ wpscan.rb | 41 +++++++++---------- 10 files changed, 113 insertions(+), 108 deletions(-) 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 From b5e3e6280e2ffedeb30c2f2bb436ea689e24f85e Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Mon, 14 May 2018 18:08:42 +0100 Subject: [PATCH 43/52] Trying to make code climate happier --- lib/wpscan/web_site/humans_txt.rb | 20 +------------------- lib/wpscan/web_site/security_txt.rb | 20 +------------------- lib/wpscan/wpscan_helper.rb | 19 +++++++++++++++++++ wpscan.rb | 10 +++++----- 4 files changed, 26 insertions(+), 43 deletions(-) diff --git a/lib/wpscan/web_site/humans_txt.rb b/lib/wpscan/web_site/humans_txt.rb index 18d9386b..e9eceaad 100644 --- a/lib/wpscan/web_site/humans_txt.rb +++ b/lib/wpscan/web_site/humans_txt.rb @@ -3,29 +3,11 @@ class WebSite module HumansTxt - # Gets a humans.txt URL + # Gets the humans.txt URL # @return [ String ] def humans_url @uri.clone.merge('humans.txt').to_s end - # Parse humans.txt - # @return [ Array ] URLs generated from humans.txt - def parse_humans_txt - return_object = [] - response = Browser.get(humans_url.to_s) - body = response.body - - # Get all non-comments - entries = body.split(/\n/) - - # Did we get something? - if entries - # Remove any rubbish - entries = clean_uri(entries) - end - return return_object - end - end end diff --git a/lib/wpscan/web_site/security_txt.rb b/lib/wpscan/web_site/security_txt.rb index 77a686ef..c8f8687e 100644 --- a/lib/wpscan/web_site/security_txt.rb +++ b/lib/wpscan/web_site/security_txt.rb @@ -3,29 +3,11 @@ class WebSite module SecurityTxt - # Gets a security.txt URL + # Gets the security.txt URL # @return [ String ] def security_url @uri.clone.merge('.well-known/security.txt').to_s end - # Parse security.txt - # @return [ Array ] URLs generated from security.txt - def parse_security_txt - return_object = [] - response = Browser.get(security_url.to_s) - body = response.body - - # Get all non-comments - entries = body.split(/\n/) - - # Did we get something? - if entries - # Remove any rubbish - entries = clean_uri(entries) - end - return return_object - end - end end diff --git a/lib/wpscan/wpscan_helper.rb b/lib/wpscan/wpscan_helper.rb index 02626441..f2fc5470 100644 --- a/lib/wpscan/wpscan_helper.rb +++ b/lib/wpscan/wpscan_helper.rb @@ -153,6 +153,25 @@ def full_uri(entries) return return_object end +# Parse humans.txt +# @return [ Array ] URLs generated from humans.txt +def parse_txt(url) + return_object = [] + response = Browser.get(url.to_s) + body = response.body + + # Get all non-comments + entries = body.split(/\n/) + + # Did we get something? + if entries + # Remove any rubbish + entries = clean_uri(entries) + 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 diff --git a/wpscan.rb b/wpscan.rb index 7e64c7bb..92e4513a 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -248,7 +248,7 @@ def main end if wp_target.has_sitemap? - code = get_http_status(wp_target.robots_url) + code = get_http_status(wp_target.sitemap_url) puts info("Sitemap found: #{wp_target.sitemap_url} [HTTP #{code}]") wp_target.parse_sitemap.each do |dir| @@ -262,8 +262,8 @@ def main if code == 200 puts info("humans.txt available under: #{wp_target.humans_url} [HTTP #{code}]") - wp_target.parse_humans_txt.each do |dir| - puts info("Interesting entry from humans.txt: #{dir}") + wp_target.parse_txt(humans_url).each do |dir| + puts info("Entry from humans.txt: #{dir}") end spacer() end @@ -272,8 +272,8 @@ def main if code == 200 puts info("security.txt available under: #{wp_target.security_url} [HTTP #{code}]") - wp_target.parse_security_txt.each do |dir| - puts info("Interesting entry from security.txt: #{dir}") + wp_target.parse_txt(security_url).each do |dir| + puts info("Entry from security.txt: #{dir}") end spacer() end From fe277c1e8995aecfe2d9d13b44dcea2f8f3e6f86 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 07:12:02 +0100 Subject: [PATCH 44/52] Make travis happy --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index dcb67560..365402fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,9 +12,7 @@ GEM ffi (1.9.23) hashdiff (0.3.7) json (2.1.0) - mini_portile2 (2.3.0) nokogiri (1.8.2) - mini_portile2 (~> 2.3.0) public_suffix (3.0.2) rspec (3.7.0) rspec-core (~> 3.7.0) @@ -33,6 +31,7 @@ GEM rspec-support (~> 3.7.0) rspec-support (3.7.1) ruby-progressbar (1.9.0) + rubyzip (1.2.1) safe_yaml (1.0.4) simplecov (0.16.1) docile (~> 1.1) @@ -59,6 +58,7 @@ DEPENDENCIES rspec (>= 3.5.0) rspec-its (>= 1.2.0) ruby-progressbar (>= 1.8.1) + rubyzip (>= 1.2.1) simplecov (>= 0.13.0) terminal-table (>= 1.6.0) typhoeus (>= 1.1.2) From 105e9cbcac45de862c87ea3a2d2204c735f69800 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 07:52:40 +0100 Subject: [PATCH 45/52] Sorted out .*ignore & *files + removed some fat --- .dockerignore | 23 ++++------------------- .gitignore | 30 +++++++++++++++--------------- DISCLAIMER.txt => DISCLAIMER.md | 0 Dockerfile | 20 ++++++++++++++------ Gemfile.lock | 2 ++ data/.gitignore | 2 -- lib/common/common_helper.rb | 13 +++---------- 7 files changed, 38 insertions(+), 52 deletions(-) rename DISCLAIMER.txt => DISCLAIMER.md (100%) delete mode 100644 data/.gitignore diff --git a/.dockerignore b/.dockerignore index abf6bb87..f561920d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,21 +1,6 @@ -git/ -bundle/ -.idea/ -.yardoc/ -cache/ -coverage/ -spec/ -dev/ .* -**/*.md -*.md -Dockerfile -**/*.orig -*.orig -CREDITS -data.zip -DISCLAIMER.txt -example.conf.json bin/ -log.txt - +dev/ +spec/ +*.md +Dockerfile \ No newline at end of file diff --git a/.gitignore b/.gitignore index 15d856b1..ee7fb0af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,21 @@ -# OS Rubbish +# WPScan (If not using ~/.wpscan/) +cache/ +data/ +log.txt +output.txt + +# WPScan (Deployment) +debug.log +rspec_results.html +wordlist.txt + +# OS/IDE Rubbish coverage/ +.yardoc/ +.idea/ *.sublime-* .*.swp .ash_history .bundle .DS_Store -.DS_Store? -.idea -.yardoc - -# WPScan -cache/ -data/ -log.txt -wordlist.txt - -# WPScan (Dev) -vendor/ -debug.log -rspec_results.html +.DS_Store? \ No newline at end of file diff --git a/DISCLAIMER.txt b/DISCLAIMER.md similarity index 100% rename from DISCLAIMER.txt rename to DISCLAIMER.md diff --git a/Dockerfile b/Dockerfile index b3390607..4af7010b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,37 @@ FROM ruby:2.5-alpine -MAINTAINER WPScan Team +LABEL maintainer="WPScan Team " ARG BUNDLER_ARGS="--jobs=8 --without test" +# Add a new user RUN adduser -h /wpscan -g WPScan -D wpscan + +# Setup gems RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc COPY Gemfile /wpscan COPY Gemfile.lock /wpscan -# runtime dependencies +# Runtime dependencies RUN apk add --no-cache libcurl procps && \ # build dependencies apk add --no-cache --virtual build-deps alpine-sdk ruby-dev libffi-dev zlib-dev && \ bundle install --system --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \ apk del --no-cache build-deps +# Copy over data & set permissions COPY . /wpscan RUN chown -R wpscan:wpscan /wpscan -USER wpscan - -RUN /wpscan/wpscan.rb --update --verbose --no-color - +# Switch directory WORKDIR /wpscan +# Switch users +USER wpscan + +# Update WPScan +RUN /wpscan/wpscan.rb --update --verbose --no-color + +# Run WPScan ENTRYPOINT ["/wpscan/wpscan.rb"] CMD ["--help"] diff --git a/Gemfile.lock b/Gemfile.lock index 365402fb..fbff01db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -12,7 +12,9 @@ GEM ffi (1.9.23) hashdiff (0.3.7) json (2.1.0) + mini_portile2 (2.3.0) nokogiri (1.8.2) + mini_portile2 (~> 2.3.0) public_suffix (3.0.2) rspec (3.7.0) rspec-core (~> 3.7.0) diff --git a/data/.gitignore b/data/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/data/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 53a1e51d..01799fe5 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -8,32 +8,25 @@ USER_DIR = File.expand_path(Dir.home) # ~/ # Core WPScan directories CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/ DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/ -CONF_DIR = File.join(USER_DIR, '.wpscan/conf') # ~/.wpscan/conf/ COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/ -UPDATER_LIB_DIR = File.join(LIB_DIR, 'updater') # wpscan/lib/updater/ - Not used ATM WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/ -COLLECTIONS_LIB_DIR = File.join(COMMON_LIB_DIR, 'collections') # wpscan/lib/common/collections/ - Not used ATM MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/ # Core WPScan files DEFAULT_LOG_FILE = File.join(USER_DIR, '.wpscan/log.txt') # ~/.wpscan/log.txt DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip -# WPScan Plugins directories -COMMON_PLUGINS_DIR = File.join(COMMON_LIB_DIR, 'plugins') # wpscan/lib/common/plugins/ - Not used ATM -WPSCAN_PLUGINS_DIR = File.join(WPSCAN_LIB_DIR, 'plugins') # wpscan/lib/common/plugins/ - Not used ATM - # WPScan Data files (data.zip) LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update -LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') # ~/.wpscan/data/local_vulnerable_files.xml - Not used ATM -LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd') # ~/.wpscan/data/local_vulnerable_files.xsd - Not used ATM +LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') # ~/.wpscan/data/local_vulnerable_files.xml - Not ref ATM +LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd') # ~/.wpscan/data/local_vulnerable_files.xsd - Not ref ATM PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') # ~/.wpscan/data/plugins.json THEMES_FILE = File.join(DATA_DIR, 'themes.json') # ~/.wpscan/data/themes.json TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') # ~/.wpscan/data/timthumbs.txt USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') # ~/.wpscan/data/user-agents.txt WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') # ~/.wpscan/data/wordpresses.json WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') # ~/.wpscan/data/wp_versions.xml -WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd') # ~/.wpscan/data/wp_versions.xsd - Not used ATM +WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd') # ~/.wpscan/data/wp_versions.xsd - Not ref ATM MIN_RUBY_VERSION = '2.1.9' From ba065d59748c44ca01c148bf747d223a845dcb85 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 08:09:24 +0100 Subject: [PATCH 46/52] ...Removed too much fat. --- lib/common/common_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 01799fe5..92f8bef0 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -8,6 +8,7 @@ USER_DIR = File.expand_path(Dir.home) # ~/ # Core WPScan directories CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/ DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/ +CONF_DIR = File.join(USER_DIR, '.wpscan/conf') # ~/.wpscan/conf/ - Not used ATM (only ref via ./spec/ for travis) COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/ WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/ MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/ From 44557797b08fe72dca525379d224295ed9a8695c Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 08:19:44 +0100 Subject: [PATCH 47/52] Update data.zip location to be $HOME --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 00cdf5fe..be5f473d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_install: - "gem regenerate_binstubs" - "bundle --version" before_script: - - "unzip -o $TRAVIS_BUILD_DIR/data.zip -d $TRAVIS_BUILD_DIR" + - "unzip -o $TRAVIS_BUILD_DIR/data.zip -d $HOME" script: - "bundle exec rspec" notifications: From 439900a1ea6549b6d22c07a3893ff735e6d36b24 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 09:05:58 +0100 Subject: [PATCH 48/52] Misc fixes --- lib/wpscan/wp_target/wp_api.rb | 6 +++--- lib/wpscan/wp_target/wp_rss.rb | 17 ++++++++++------- wpscan.rb | 4 ++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index 09942573..6f8332c1 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -68,10 +68,10 @@ class WpTarget < WebSite users << row end - if users - # Sort and uniq - users = users.sort.uniq + # Sort and uniq + users = users.sort.uniq + if users and users.size > 1 # Feedback grammar = grammar_s(users.size) puts warning("#{users.size} user#{grammar} exposed via API: #{json_users_url}") diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb index 5aef1127..fc132cd5 100644 --- a/lib/wpscan/wp_target/wp_rss.rb +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -36,16 +36,19 @@ class WpTarget < WebSite # Read in RSS/XML xml = Nokogiri::XML(data) - # Look for item - xml.xpath('//item/dc:creator').each do |node| - #Format: - users << [%r{.*}i.match(node).to_s] + begin + # Look for item + xml.xpath('//item/dc:creator').each do |node| + #Format: + users << [%r{.*}i.match(node).to_s] + end + rescue end - if users - # Sort and uniq - users = users.sort_by { |user| user.to_s.downcase }.uniq + # Sort and uniq + users = users.sort_by { |user| user.to_s.downcase }.uniq + if users and users.size > 1 # Feedback grammar = grammar_s(users.size) puts warning("Detected #{users.size} user#{grammar} from RSS feed:") diff --git a/wpscan.rb b/wpscan.rb index 92e4513a..936923c0 100755 --- a/wpscan.rb +++ b/wpscan.rb @@ -262,7 +262,7 @@ def main if code == 200 puts info("humans.txt available under: #{wp_target.humans_url} [HTTP #{code}]") - wp_target.parse_txt(humans_url).each do |dir| + parse_txt(wp_target.humans_url).each do |dir| puts info("Entry from humans.txt: #{dir}") end spacer() @@ -272,7 +272,7 @@ def main if code == 200 puts info("security.txt available under: #{wp_target.security_url} [HTTP #{code}]") - wp_target.parse_txt(security_url).each do |dir| + parse_txt(wp_target.security_url).each do |dir| puts info("Entry from security.txt: #{dir}") end spacer() From 59368a72bd6bf792442e4a4ed659828473233c66 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 10:39:16 +0100 Subject: [PATCH 49/52] Don't fail silent. --- .travis.yml | 2 +- lib/wpscan/wp_target/wp_api.rb | 21 +++++++++++++-------- lib/wpscan/wp_target/wp_rss.rb | 9 ++++++++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index be5f473d..e79a6973 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,7 +24,7 @@ before_install: - "gem regenerate_binstubs" - "bundle --version" before_script: - - "unzip -o $TRAVIS_BUILD_DIR/data.zip -d $HOME" + - "unzip -o $TRAVIS_BUILD_DIR/data.zip -d $HOME/.wpscan/" script: - "bundle exec rspec" notifications: diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index 6f8332c1..717d6ad9 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -46,22 +46,25 @@ class WpTarget < WebSite def json_get_users(url) # Variables users = [] + data = "" # Make the request response = Browser.get(url) - # Able to view the output? - return false if not valid_json?(response.body) + # If not HTTP 200, return false + return false if response.code != 200 - # Read in JSON - data = JSON.parse(response.body) + # Able to view the output? + if valid_json?(response.body) + # Read in JSON + data = JSON.parse(response.body) + else + return false + end # If there is nothing there, return false return false if data.empty? - # If not HTTP 200, return false - return false if response.code != 200 - # Add to array data.each do |child| row = [ child['id'], child['name'], child['link'] ] @@ -71,7 +74,7 @@ class WpTarget < WebSite # Sort and uniq users = users.sort.uniq - if users and users.size > 1 + if users and users.size >= 1 # Feedback grammar = grammar_s(users.size) puts warning("#{users.size} user#{grammar} exposed via API: #{json_users_url}") @@ -80,6 +83,8 @@ class WpTarget < WebSite table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], rows: users) puts table + else + return false end end end diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb index fc132cd5..418c1456 100644 --- a/lib/wpscan/wp_target/wp_rss.rb +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -33,6 +33,9 @@ class WpTarget < WebSite # Get output data = response.body + # If there is nothing there, return false + return false if data.empty? + # Read in RSS/XML xml = Nokogiri::XML(data) @@ -43,12 +46,14 @@ class WpTarget < WebSite users << [%r{.*}i.match(node).to_s] end rescue + puts critical("Missing Author field. Maybe non-standard WordPress RSS feed?") + return false end # Sort and uniq users = users.sort_by { |user| user.to_s.downcase }.uniq - if users and users.size > 1 + if users and users.size >= 1 # Feedback grammar = grammar_s(users.size) puts warning("Detected #{users.size} user#{grammar} from RSS feed:") @@ -57,6 +62,8 @@ class WpTarget < WebSite table = Terminal::Table.new(headings: ['Name'], rows: users) puts table + else + return false end end end From cf2881fda6c401c2d317efd04f05930ba3ac3c20 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 10:47:55 +0100 Subject: [PATCH 50/52] Fix bots issues? ...Happy now? Please? --- lib/wpscan/wp_target/wp_api.rb | 17 +++++++---------- lib/wpscan/wp_target/wp_rss.rb | 4 ++-- spec/lib/wpscan/web_site_spec.rb | 12 ------------ 3 files changed, 9 insertions(+), 24 deletions(-) diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index 717d6ad9..de80ac86 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -46,21 +46,18 @@ class WpTarget < WebSite def json_get_users(url) # Variables users = [] - data = "" # Make the request response = Browser.get(url) # If not HTTP 200, return false - return false if response.code != 200 + return false unless response.code == 200 # Able to view the output? - if valid_json?(response.body) - # Read in JSON - data = JSON.parse(response.body) - else - return false - end + return false unless valid_json?(response.body) + + # Read in JSON + data = JSON.parse(response.body) # If there is nothing there, return false return false if data.empty? @@ -83,9 +80,9 @@ class WpTarget < WebSite table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], rows: users) puts table - else - return false + return true end + return false end end end diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb index 418c1456..5239c045 100644 --- a/lib/wpscan/wp_target/wp_rss.rb +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -62,9 +62,9 @@ class WpTarget < WebSite table = Terminal::Table.new(headings: ['Name'], rows: users) puts table - else - return false + return true end + return false end end end diff --git a/spec/lib/wpscan/web_site_spec.rb b/spec/lib/wpscan/web_site_spec.rb index 4eeda2eb..56b150b8 100644 --- a/spec/lib/wpscan/web_site_spec.rb +++ b/spec/lib/wpscan/web_site_spec.rb @@ -207,18 +207,6 @@ describe 'WebSite' do end end - describe '#rss_url' do - it 'returns nil if the url is not found' do - stub_request(:get, web_site.url).to_return(body: 'No RSS link in this body !') - expect(web_site.rss_url).to be_nil - end - - it "returns 'http://lamp-wp/wordpress-3.5/?feed=rss2'" do - stub_request_to_fixture(url: web_site.url, fixture: fixtures_dir + '/rss_url/wordpress-3.5.htm') - expect(web_site.rss_url).to be === 'http://lamp-wp/wordpress-3.5/?feed=rss2' - end - end - describe '::has_log?' do let(:log_url) { web_site.uri.merge('log.txt').to_s } let(:pattern) { %r{PHP Fatal error} } From a783b5310751fbb03bd1693fffd755365f11ec15 Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 15 May 2018 11:17:03 +0100 Subject: [PATCH 51/52] Fix grammar ..and bots --- lib/common/common_helper.rb | 2 +- lib/wpscan/wp_target/wp_api.rb | 2 -- lib/wpscan/wp_target/wp_rss.rb | 2 -- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 92f8bef0..30f48a64 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -336,5 +336,5 @@ end # Check to see if we need a "s" def grammar_s(size) - size.to_i >= 1 ? "s" : "" + size.to_i >= 2 ? "s" : "" end \ No newline at end of file diff --git a/lib/wpscan/wp_target/wp_api.rb b/lib/wpscan/wp_target/wp_api.rb index de80ac86..2a52c4df 100644 --- a/lib/wpscan/wp_target/wp_api.rb +++ b/lib/wpscan/wp_target/wp_api.rb @@ -80,9 +80,7 @@ class WpTarget < WebSite table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'], rows: users) puts table - return true end - return false end end end diff --git a/lib/wpscan/wp_target/wp_rss.rb b/lib/wpscan/wp_target/wp_rss.rb index 5239c045..1fc34839 100644 --- a/lib/wpscan/wp_target/wp_rss.rb +++ b/lib/wpscan/wp_target/wp_rss.rb @@ -62,9 +62,7 @@ class WpTarget < WebSite table = Terminal::Table.new(headings: ['Name'], rows: users) puts table - return true end - return false end end end From a981c2b17b420323d4318360ff3393ec3fb7eadb Mon Sep 17 00:00:00 2001 From: g0tmi1k Date: Tue, 22 May 2018 10:06:57 +0100 Subject: [PATCH 52/52] @FireFart's suggestions --- .dockerignore | 17 ++++++++++++++++- lib/common/common_helper.rb | 11 ++++------- lib/wpscan/web_site/sitemap.rb | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.dockerignore b/.dockerignore index f561920d..9db627ea 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,4 +3,19 @@ bin/ dev/ spec/ *.md -Dockerfile \ No newline at end of file +Dockerfile + +## TEMP +.idea/ +.yardoc/ +bundle/ +cache/ +coverage/ +git/ +**/*.md +**/*.orig +*.orig +CREDITS +data.zip +DISCLAIMER.txt +example.conf.json diff --git a/lib/common/common_helper.rb b/lib/common/common_helper.rb index 30f48a64..5431ef2a 100644 --- a/lib/common/common_helper.rb +++ b/lib/common/common_helper.rb @@ -19,15 +19,12 @@ DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip # WPScan Data files (data.zip) LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update -LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml') # ~/.wpscan/data/local_vulnerable_files.xml - Not ref ATM -LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd') # ~/.wpscan/data/local_vulnerable_files.xsd - Not ref ATM PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') # ~/.wpscan/data/plugins.json THEMES_FILE = File.join(DATA_DIR, 'themes.json') # ~/.wpscan/data/themes.json TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') # ~/.wpscan/data/timthumbs.txt USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') # ~/.wpscan/data/user-agents.txt WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') # ~/.wpscan/data/wordpresses.json WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') # ~/.wpscan/data/wp_versions.xml -WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd') # ~/.wpscan/data/wp_versions.xsd - Not ref ATM MIN_RUBY_VERSION = '2.1.9' @@ -89,20 +86,20 @@ end # Find data.zip? def has_db_zip? - return File.exist?(DATA_FILE)? true : false + return File.exist?(DATA_FILE) end # Extract data.zip def extract_db_zip + # Create data folder + FileUtils.mkdir_p(DATA_DIR) + Zip::File.open(DATA_FILE) do |zip_file| zip_file.each do |f| # Feedback to the user #puts "[+] Extracting: #{File.basename(f.name)}" f_path = File.join(DATA_DIR, File.basename(f.name)) - # Create folder - FileUtils.mkdir_p(File.dirname(f_path)) - # Delete if already there #puts "[+] Deleting: #{File.basename(f.name)}" if File.exist?(f_path) FileUtils.rm(f_path) if File.exist?(f_path) diff --git a/lib/wpscan/web_site/sitemap.rb b/lib/wpscan/web_site/sitemap.rb index bc3a3736..46140a03 100644 --- a/lib/wpscan/web_site/sitemap.rb +++ b/lib/wpscan/web_site/sitemap.rb @@ -18,7 +18,7 @@ class WebSite return false end - # Gets a robots.txt URL + # Get the robots.txt URL # @return [ String ] def sitemap_url @uri.clone.merge('robots.txt').to_s