From 5f2b8f8a2eb7a76817f1ce81df4d572ca7eaa9b0 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 20 Mar 2019 07:47:28 +0000 Subject: [PATCH 01/13] Fixes #1317 --- app/finders/users/rss_generator.rb | 10 +++++----- .../finders/users/rss_generator/feed.xml | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/finders/users/rss_generator.rb b/app/finders/users/rss_generator.rb index dc936ddc..f090acb0 100644 --- a/app/finders/users/rss_generator.rb +++ b/app/finders/users/rss_generator.rb @@ -17,20 +17,20 @@ module WPScan begin res.xml.xpath('//item/dc:creator').each do |node| - potential_username = node.text.to_s + username = node.text.to_s # Ignoring potential username longer than 60 characters and containing accents # as they are considered invalid. See https://github.com/wpscanteam/wpscan/issues/1215 - next if potential_username.length > 60 || potential_username =~ /[^\x00-\x7F]/ + next if username.strip.empty? || username.length > 60 || username =~ /[^\x00-\x7F]/ - potential_usernames << potential_username + potential_usernames << username end rescue Nokogiri::XML::XPath::SyntaxError next end - potential_usernames.uniq.each do |potential_username| - found << CMSScanner::User.new(potential_username, found_by: found_by, confidence: 50) + potential_usernames.uniq.each do |username| + found << CMSScanner::User.new(username, found_by: found_by, confidence: 50) end break diff --git a/spec/fixtures/finders/users/rss_generator/feed.xml b/spec/fixtures/finders/users/rss_generator/feed.xml index a9ab6453..46841668 100644 --- a/spec/fixtures/finders/users/rss_generator/feed.xml +++ b/spec/fixtures/finders/users/rss_generator/feed.xml @@ -59,5 +59,23 @@ Michael Schrage is a researcher at the MIT Sloan School of Management Initiative on the Digital Economy, where he does research and advisory work on how digital media transforms agency, human capital, and innovation.

]]>
+ + + Hello world! + http://ex.lo/2018/09/23/hello-world/ + http://ex.lo/2018/09/23/hello-world/#comments + Sun, 23 Sep 2018 11:31:56 +0000 + + + + + + Hello world! + http://ex.lo/2018/09/23/hello-world/ + http://ex.lo/2018/09/23/hello-world/#comments + Sun, 23 Sep 2018 11:31:56 +0000 + + + From 6304fe4c19c8fec196dcea952544e2a63bd6b475 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 20 Mar 2019 08:41:39 +0000 Subject: [PATCH 02/13] Fixes #1318 --- lib/wpscan/target/platform/wordpress/custom_directories.rb | 2 +- .../wordpress/custom_directories/relative_two_sub_dir.html | 6 ++++++ .../target/platform/wordpress/custom_directories.rb | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 spec/fixtures/target/platform/wordpress/custom_directories/relative_two_sub_dir.html diff --git a/lib/wpscan/target/platform/wordpress/custom_directories.rb b/lib/wpscan/target/platform/wordpress/custom_directories.rb index 0aba4577..17be3d49 100644 --- a/lib/wpscan/target/platform/wordpress/custom_directories.rb +++ b/lib/wpscan/target/platform/wordpress/custom_directories.rb @@ -15,7 +15,7 @@ module WPScan def content_dir unless @content_dir escaped_url = Regexp.escape(url).gsub(/https?/i, 'https?') - pattern = %r{#{escaped_url}([^\/]+)\/(?:themes|plugins|uploads|cache)\/}i + pattern = %r{#{escaped_url}([\w\s\-\/]+)\/(?:themes|plugins|uploads|cache)\/}i in_scope_urls(homepage_res) do |url| return @content_dir = Regexp.last_match[1] if url.match(pattern) diff --git a/spec/fixtures/target/platform/wordpress/custom_directories/relative_two_sub_dir.html b/spec/fixtures/target/platform/wordpress/custom_directories/relative_two_sub_dir.html new file mode 100644 index 00000000..872d5736 --- /dev/null +++ b/spec/fixtures/target/platform/wordpress/custom_directories/relative_two_sub_dir.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/spec/shared_examples/target/platform/wordpress/custom_directories.rb b/spec/shared_examples/target/platform/wordpress/custom_directories.rb index 9d4cfe67..d08d03a5 100644 --- a/spec/shared_examples/target/platform/wordpress/custom_directories.rb +++ b/spec/shared_examples/target/platform/wordpress/custom_directories.rb @@ -5,7 +5,7 @@ shared_examples 'WordPress::CustomDirectories' do { default: 'wp-content', https: 'wp-content', custom_w_spaces: 'custom content spaces', relative_one: 'wp-content', relative_two: 'wp-content', cache: 'wp-content', - in_raw_js: 'wp-content', with_sub_dir: 'app' + in_raw_js: 'wp-content', with_sub_dir: 'app', relative_two_sub_dir: 'cms/wp-content' }.each do |file, expected| it "returns #{expected} for #{file}.html" do stub_request(:get, target.url).to_return(body: File.read(fixtures.join("#{file}.html"))) @@ -47,7 +47,7 @@ shared_examples 'WordPress::CustomDirectories' do end describe '#sub_dir' do - { default: false, with_sub_dir: 'wp' }.each do |file, expected| + { default: false, with_sub_dir: 'wp', relative_two_sub_dir: 'cms' }.each do |file, expected| it "returns #{expected} for #{file}.html" do fixture = File.join(fixtures, "#{file}.html") From f09606cfa3ee56a0fac22715313d8e98038b0059 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 20 Mar 2019 15:21:02 +0000 Subject: [PATCH 03/13] Fixes #1319 --- app/finders/users/wp_json_api.rb | 10 +++- spec/app/finders/users/wp_json_api_spec.rb | 55 ++++++++++++++++++- .../users/wp_json_api/api_url/in_scope.html | 1 + .../wp_json_api/api_url/in_scope_subdir.html | 6 ++ .../api_url/in_scope_subdir_ignored.html | 6 ++ .../wp_json_api/api_url/out_of_scope.html | 1 + 6 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/finders/users/wp_json_api/api_url/in_scope.html create mode 100644 spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir.html create mode 100644 spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir_ignored.html create mode 100644 spec/fixtures/finders/users/wp_json_api/api_url/out_of_scope.html diff --git a/app/finders/users/wp_json_api.rb b/app/finders/users/wp_json_api.rb index 91b0ea85..a795fc6e 100644 --- a/app/finders/users/wp_json_api.rb +++ b/app/finders/users/wp_json_api.rb @@ -53,7 +53,15 @@ module WPScan # @return [ String ] The URL of the API listing the Users def api_url - @api_url ||= target.url('wp-json/wp/v2/users/') + return @api_url if @api_url + + target.in_scope_urls(target.homepage_res, "//link[@rel='https://api.w.org/']/@href").each do |url, _tag| + uri = Addressable::URI.parse(url.strip) + + return @api_url = uri.join('wp/v2/users/').to_s if uri.path.include?('wp-json') + end + + @api_url = target.url('wp-json/wp/v2/users/') end end end diff --git a/spec/app/finders/users/wp_json_api_spec.rb b/spec/app/finders/users/wp_json_api_spec.rb index 6f7e2cb4..3d14c503 100644 --- a/spec/app/finders/users/wp_json_api_spec.rb +++ b/spec/app/finders/users/wp_json_api_spec.rb @@ -5,7 +5,10 @@ describe WPScan::Finders::Users::WpJsonApi do let(:fixtures) { FINDERS_FIXTURES.join('users', 'wp_json_api') } describe '#aggressive' do - before { allow(target).to receive(:sub_dir).and_return(false) } + before do + allow(target).to receive(:sub_dir).and_return(false) + allow(finder).to receive(:api_url).and_return(target.url('wp-json/wp/v2/users/')) + end context 'when only one page of results' do before do @@ -78,4 +81,54 @@ describe WPScan::Finders::Users::WpJsonApi do end end end + + describe '#api_url' do + let(:fixtures) { super().join('api_url') } + + context 'when url in the homepage' do + { + in_scope: 'https://wp.lab/wp-json/wp/v2/users/', + out_of_scope: 'http://wp.lab/wp-json/wp/v2/users/' + }.each do |fixture, expected| + it "returns #{expected} for #{fixture}.html" do + stub_request(:get, target.url).to_return(body: File.read(fixtures.join("#{fixture}.html"))) + + expect(finder.api_url).to eql expected + end + end + + context 'when subdir' do + before { allow(target).to receive(:subdir).and_return('cms') } + + { + in_scope_subdir: 'https://wp.lab/cms/wp-json/wp/v2/users/', + in_scope_subdir_ignored: 'https://wp.lab/wp-json/wp/v2/users/' + }.each do |fixture, expected| + it "returns #{expected} for #{fixture}.html" do + stub_request(:get, target.url).to_return(body: File.read(fixtures.join("#{fixture}.html"))) + + expect(finder.api_url).to eql expected + end + end + end + end + + context 'when not in the homepage' do + before { stub_request(:get, target.url) } + + its(:api_url) { should eql target.url('wp-json/wp/v2/users/') } + end + + context 'when api_url already found' do + before { allow(target).to receive(:sub_dir).and_return(false) } + + it 'does not check the homepage again' do + url = target.url('wp-json/wp/v2/users/') + + finder.instance_variable_set(:@api_url, url) + + expect(finder.api_url).to eql url + end + end + end end diff --git a/spec/fixtures/finders/users/wp_json_api/api_url/in_scope.html b/spec/fixtures/finders/users/wp_json_api/api_url/in_scope.html new file mode 100644 index 00000000..22c182e7 --- /dev/null +++ b/spec/fixtures/finders/users/wp_json_api/api_url/in_scope.html @@ -0,0 +1 @@ + diff --git a/spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir.html b/spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir.html new file mode 100644 index 00000000..71525fe0 --- /dev/null +++ b/spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir_ignored.html b/spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir_ignored.html new file mode 100644 index 00000000..218372c8 --- /dev/null +++ b/spec/fixtures/finders/users/wp_json_api/api_url/in_scope_subdir_ignored.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/spec/fixtures/finders/users/wp_json_api/api_url/out_of_scope.html b/spec/fixtures/finders/users/wp_json_api/api_url/out_of_scope.html new file mode 100644 index 00000000..b793dc25 --- /dev/null +++ b/spec/fixtures/finders/users/wp_json_api/api_url/out_of_scope.html @@ -0,0 +1 @@ + From f414e6eeb75f2e687b63f4a57cb853e1b28c35ee Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 20 Mar 2019 20:10:30 +0000 Subject: [PATCH 04/13] Better code for WpVersion#all --- app/models/wp_version.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/models/wp_version.rb b/app/models/wp_version.rb index c14cbd21..439593a4 100644 --- a/app/models/wp_version.rb +++ b/app/models/wp_version.rb @@ -23,13 +23,12 @@ module WPScan @all_numbers = [] DB::Fingerprints.wp_fingerprints.each_value do |fp| - fp.each_value do |versions| - versions.each do |version| - @all_numbers << version unless @all_numbers.include?(version) - end - end + @all_numbers << fp.values end + # @all_numbers.flatten.uniq.sort! {} doesn't produce the same result here. + @all_numbers.flatten! + @all_numbers.uniq! @all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) } end From 496fc4ebee7c34b898d5054cadfb84d6274e7ed4 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 20 Mar 2019 20:12:18 +0000 Subject: [PATCH 05/13] Typo --- lib/wpscan/db/dynamic_finders/plugin.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/wpscan/db/dynamic_finders/plugin.rb b/lib/wpscan/db/dynamic_finders/plugin.rb index 16e25a95..59e8de29 100644 --- a/lib/wpscan/db/dynamic_finders/plugin.rb +++ b/lib/wpscan/db/dynamic_finders/plugin.rb @@ -60,7 +60,7 @@ module WPScan # @param [ String ] slug # @return [ Constant ] - def self.maybe_create_modudle(slug) + def self.maybe_create_module(slug) # What about slugs such as js_composer which will be done as JsComposer, just like js-composer constant_name = classify_slug(slug) @@ -76,7 +76,7 @@ module WPScan # Kind of an issue here, module is created even if there is no valid classes # Could put the #maybe_ directly in the #send() BUT it would be checked everytime, # which is kind of a waste - mod = maybe_create_modudle(slug) + mod = maybe_create_module(slug) finders.each do |finder_class, config| klass = config['class'] || finder_class From 72bddca3143f6f6a414ccb51cab4bb927ce73b5c Mon Sep 17 00:00:00 2001 From: erwanlr Date: Wed, 20 Mar 2019 21:08:50 +0000 Subject: [PATCH 06/13] Adds profiling binary for dev [WIP] - Ref #1321 --- bin/wpscan-stackprof | 24 ++++++++++++++++++++++++ wpscan.gemspec | 1 + 2 files changed, 25 insertions(+) create mode 100755 bin/wpscan-stackprof diff --git a/bin/wpscan-stackprof b/bin/wpscan-stackprof new file mode 100755 index 00000000..8e25adc6 --- /dev/null +++ b/bin/wpscan-stackprof @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby + +require 'stackprof' # https://github.com/tmm1/stackprof +require 'wpscan' + +# The object mode produces a segfault currently: https://github.com/jfelchner/ruby-progressbar/issues/153 +# StackProf.run(mode: :object, out: '/tmp/stackprof-object.dump') do +# StackProf.run(mode: :wall, out: '/tmp/stackprof-wall.dump') do +StackProf.run(mode: :cpu, out: '/tmp/stackprof-cpu.dump', interval: 500) do + # Couldn't we just load the ./wpscan here ? + # require_relative 'wpscan' doesn't work + WPScan::Scan.new do |s| + s.controllers << + WPScan::Controller::CustomDirectories.new << + WPScan::Controller::InterestingFindings.new << + WPScan::Controller::WpVersion.new << + WPScan::Controller::MainTheme.new << + WPScan::Controller::Enumeration.new << + WPScan::Controller::PasswordAttack.new << + WPScan::Controller::Aliases.new + + s.run + end +end diff --git a/wpscan.gemspec b/wpscan.gemspec index 8c1ec265..c137d7fd 100644 --- a/wpscan.gemspec +++ b/wpscan.gemspec @@ -30,5 +30,6 @@ Gem::Specification.new do |s| s.add_development_dependency 'rspec-its', '~> 1.2.0' s.add_development_dependency 'rubocop', '~> 0.66.0' s.add_development_dependency 'simplecov', '~> 0.16.1' + s.add_development_dependency 'stackprof', '~> 0.2.12' s.add_development_dependency 'webmock', '~> 3.5.1' end From c15ff4e32eb8052aff1432f6f725094c2aeb788d Mon Sep 17 00:00:00 2001 From: erwanlr Date: Thu, 21 Mar 2019 11:31:04 +0000 Subject: [PATCH 07/13] Adds memprof binary - Ref #1321 --- .gitignore | 3 +++ bin/wpscan-memprof | 23 +++++++++++++++++++++++ wpscan.gemspec | 1 + 3 files changed, 27 insertions(+) create mode 100755 bin/wpscan-memprof diff --git a/.gitignore b/.gitignore index 9932d837..c0cf4aea 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ doc/ # Old files from v2 cache/ data/ + +# Profiling reports +bin/memprof*.report diff --git a/bin/wpscan-memprof b/bin/wpscan-memprof new file mode 100755 index 00000000..b4c8ce9c --- /dev/null +++ b/bin/wpscan-memprof @@ -0,0 +1,23 @@ +#!/usr/bin/env ruby + +require 'memory_profiler' # https://github.com/SamSaffron/memory_profiler +require 'wpscan' + +report = MemoryProfiler.report(top: 10) do + # Couldn't we just load the ./wpscan here ? + # require_relative 'wpscan' doesn't work + WPScan::Scan.new do |s| + s.controllers << + WPScan::Controller::CustomDirectories.new << + WPScan::Controller::InterestingFindings.new << + WPScan::Controller::WpVersion.new << + WPScan::Controller::MainTheme.new << + WPScan::Controller::Enumeration.new << + WPScan::Controller::PasswordAttack.new << + WPScan::Controller::Aliases.new + + s.run + end +end + +report.pretty_print(scale_bytes: true, detailed_report: true, to_file: 'memprof.report') diff --git a/wpscan.gemspec b/wpscan.gemspec index c137d7fd..505edec3 100644 --- a/wpscan.gemspec +++ b/wpscan.gemspec @@ -25,6 +25,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'bundler', '>= 1.6' s.add_development_dependency 'coveralls', '~> 0.8.0' + s.add_development_dependency 'memory_profiler', '~> 0.9.12' s.add_development_dependency 'rake', '~> 12.3' s.add_development_dependency 'rspec', '~> 3.8.0' s.add_development_dependency 'rspec-its', '~> 1.2.0' From 1f0f87633bb8c65caec1e40eb47c5761f64de63b Mon Sep 17 00:00:00 2001 From: erwanlr Date: Thu, 21 Mar 2019 13:52:34 +0000 Subject: [PATCH 08/13] Reduces memory allocation with creating DFs --- lib/wpscan/db/dynamic_finders/plugin.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/wpscan/db/dynamic_finders/plugin.rb b/lib/wpscan/db/dynamic_finders/plugin.rb index 59e8de29..7500f7c2 100644 --- a/lib/wpscan/db/dynamic_finders/plugin.rb +++ b/lib/wpscan/db/dynamic_finders/plugin.rb @@ -64,18 +64,24 @@ module WPScan # What about slugs such as js_composer which will be done as JsComposer, just like js-composer constant_name = classify_slug(slug) - unless version_finder_module.constants.include?(constant_name) + # version_finder_module.constants.include? could be used here + # however, it increases the memory allocated doing so. + unless version_finder_modules.include?(constant_name) version_finder_module.const_set(constant_name, Module.new) + + version_finder_modules << constant_name end version_finder_module.const_get(constant_name) end + # @return [ Array ] + def self.version_finder_modules + @version_finder_modules ||= version_finder_module.constants + end + def self.create_versions_finders versions_finders_configs.each do |slug, finders| - # Kind of an issue here, module is created even if there is no valid classes - # Could put the #maybe_ directly in the #send() BUT it would be checked everytime, - # which is kind of a waste mod = maybe_create_module(slug) finders.each do |finder_class, config| From d407815c306dd047ff68e70efe31dc63334e678f Mon Sep 17 00:00:00 2001 From: erwanlr Date: Thu, 21 Mar 2019 16:54:06 +0000 Subject: [PATCH 09/13] Adds comment about scale_bytes in memory_profiler --- bin/wpscan-memprof | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/wpscan-memprof b/bin/wpscan-memprof index b4c8ce9c..cd768494 100755 --- a/bin/wpscan-memprof +++ b/bin/wpscan-memprof @@ -20,4 +20,6 @@ report = MemoryProfiler.report(top: 10) do end end -report.pretty_print(scale_bytes: true, detailed_report: true, to_file: 'memprof.report') +# scale_bytes option not yet supported in the latest stable +# See https://github.com/SamSaffron/memory_profiler/issues/68 +report.pretty_print($stdout, scale_bytes: true, detailed_report: true, to_file: 'memprof.report') From 95eb6a732cb3531c714a66503ab85c891ebbcb3a Mon Sep 17 00:00:00 2001 From: erwanlr Date: Thu, 21 Mar 2019 20:50:57 +0000 Subject: [PATCH 10/13] Memprofiling - Increases the top to be displayed to 15 --- bin/wpscan-memprof | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bin/wpscan-memprof b/bin/wpscan-memprof index cd768494..7bcce447 100755 --- a/bin/wpscan-memprof +++ b/bin/wpscan-memprof @@ -3,9 +3,7 @@ require 'memory_profiler' # https://github.com/SamSaffron/memory_profiler require 'wpscan' -report = MemoryProfiler.report(top: 10) do - # Couldn't we just load the ./wpscan here ? - # require_relative 'wpscan' doesn't work +report = MemoryProfiler.report(top: 15) do WPScan::Scan.new do |s| s.controllers << WPScan::Controller::CustomDirectories.new << From 8b18204a697d5255823d4fca197fb30aa3f19bd1 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Fri, 22 Mar 2019 06:56:10 +0000 Subject: [PATCH 11/13] Updates memory_profiler dep, revert changes to memory allocated commit (increased retained memory too much) --- bin/wpscan-memprof | 4 +--- lib/wpscan/db/dynamic_finders/plugin.rb | 11 +---------- wpscan.gemspec | 2 +- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/bin/wpscan-memprof b/bin/wpscan-memprof index 7bcce447..2397ce7e 100755 --- a/bin/wpscan-memprof +++ b/bin/wpscan-memprof @@ -18,6 +18,4 @@ report = MemoryProfiler.report(top: 15) do end end -# scale_bytes option not yet supported in the latest stable -# See https://github.com/SamSaffron/memory_profiler/issues/68 -report.pretty_print($stdout, scale_bytes: true, detailed_report: true, to_file: 'memprof.report') +report.pretty_print(scale_bytes: true, to_file: 'memprof.report') diff --git a/lib/wpscan/db/dynamic_finders/plugin.rb b/lib/wpscan/db/dynamic_finders/plugin.rb index 7500f7c2..a839c00f 100644 --- a/lib/wpscan/db/dynamic_finders/plugin.rb +++ b/lib/wpscan/db/dynamic_finders/plugin.rb @@ -64,22 +64,13 @@ module WPScan # What about slugs such as js_composer which will be done as JsComposer, just like js-composer constant_name = classify_slug(slug) - # version_finder_module.constants.include? could be used here - # however, it increases the memory allocated doing so. - unless version_finder_modules.include?(constant_name) + unless version_finder_module.constants.include?(constant_name) version_finder_module.const_set(constant_name, Module.new) - - version_finder_modules << constant_name end version_finder_module.const_get(constant_name) end - # @return [ Array ] - def self.version_finder_modules - @version_finder_modules ||= version_finder_module.constants - end - def self.create_versions_finders versions_finders_configs.each do |slug, finders| mod = maybe_create_module(slug) diff --git a/wpscan.gemspec b/wpscan.gemspec index 505edec3..a4cc4cfa 100644 --- a/wpscan.gemspec +++ b/wpscan.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |s| s.add_development_dependency 'bundler', '>= 1.6' s.add_development_dependency 'coveralls', '~> 0.8.0' - s.add_development_dependency 'memory_profiler', '~> 0.9.12' + s.add_development_dependency 'memory_profiler', '~> 0.9.13' s.add_development_dependency 'rake', '~> 12.3' s.add_development_dependency 'rspec', '~> 3.8.0' s.add_development_dependency 'rspec-its', '~> 1.2.0' From 231f5157bfd9458a03b640bb8a03ceb6969ab023 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Fri, 22 Mar 2019 20:20:07 +0000 Subject: [PATCH 12/13] Fixes #1322 --- app/finders/db_exports/known_locations.rb | 15 ++++++++-- .../interesting_findings/tmm_db_migrate.rb | 2 +- .../interesting_findings/upload_sql_dump.rb | 13 +++++---- app/finders/timthumbs/known_locations.rb | 6 ++-- lib/wpscan/finders.rb | 29 +++++-------------- lib/wpscan/target.rb | 9 ++++++ .../db_exports/known_locations_spec.rb | 20 +++++++++---- .../upload_sql_dump_spec.rb | 14 ++++++--- 8 files changed, 63 insertions(+), 45 deletions(-) diff --git a/app/finders/db_exports/known_locations.rb b/app/finders/db_exports/known_locations.rb index 24f26227..1836abba 100644 --- a/app/finders/db_exports/known_locations.rb +++ b/app/finders/db_exports/known_locations.rb @@ -4,7 +4,9 @@ module WPScan # DB Exports finder # See https://github.com/wpscanteam/wpscan-v3/issues/62 class KnownLocations < CMSScanner::Finders::Finder - include CMSScanner::Finders::Finder::Enumerator + include Finders::Finder::Enumerator + + SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE) TABLE|INSERT INTO/.freeze # @param [ Hash ] opts # @option opts [ String ] :list @@ -15,14 +17,21 @@ module WPScan found = [] enumerate(potential_urls(opts), opts) do |res| - next unless res.code == 200 && res.body =~ /INSERT INTO/ - found << WPScan::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100) end found end + def valid_response?(res, _exclude_content = nil) + return false unless res.code == 200 + + return true if res.effective_url.end_with?('.zip') && + res.headers['Content-Type'] =~ %r{\Aapplication/zip}i + + Browser.get(res.effective_url, headers: { 'Range' => 'bytes=0-3000' }).body =~ SQL_PATTERN ? true : false + end + # @param [ Hash ] opts # @option opts [ String ] :list Mandatory # diff --git a/app/finders/interesting_findings/tmm_db_migrate.rb b/app/finders/interesting_findings/tmm_db_migrate.rb index d388f6e0..787140ba 100644 --- a/app/finders/interesting_findings/tmm_db_migrate.rb +++ b/app/finders/interesting_findings/tmm_db_migrate.rb @@ -7,7 +7,7 @@ module WPScan def aggressive(_opts = {}) path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip' url = target.url(path) - res = Browser.get(url) + res = browser.forge_request(url, target.head_or_get_request_params).run return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i diff --git a/app/finders/interesting_findings/upload_sql_dump.rb b/app/finders/interesting_findings/upload_sql_dump.rb index 9d45398f..7119088d 100644 --- a/app/finders/interesting_findings/upload_sql_dump.rb +++ b/app/finders/interesting_findings/upload_sql_dump.rb @@ -3,24 +3,25 @@ module WPScan module InterestingFindings # UploadSQLDump finder class UploadSQLDump < CMSScanner::Finders::Finder - SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/.freeze + SQL_PATTERN = /(?:DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO/.freeze # @return [ InterestingFinding ] def aggressive(_opts = {}) - url = dump_url - res = Browser.get(url) + head_res = browser.forge_request(dump_url, target.head_or_get_request_params).run - return unless res.code == 200 && res.body =~ SQL_PATTERN + return unless head_res.code == 200 + + return unless Browser.get(dump_url, headers: { 'Range' => 'bytes=0-3000' }).body =~ SQL_PATTERN WPScan::UploadSQLDump.new( - url, + dump_url, confidence: 100, found_by: DIRECT_ACCESS ) end def dump_url - target.url('wp-content/uploads/dump.sql') + @dump_url ||= target.url('wp-content/uploads/dump.sql') end end end diff --git a/app/finders/timthumbs/known_locations.rb b/app/finders/timthumbs/known_locations.rb index e6923436..a0ebc64a 100644 --- a/app/finders/timthumbs/known_locations.rb +++ b/app/finders/timthumbs/known_locations.rb @@ -22,15 +22,13 @@ module WPScan end # @param [ Typhoeus::Response ] res - # @param [ Regexp,nil ] exclude_content + # @param [ Regexp, nil ] exclude_content # # @return [ Boolean ] def valid_response?(res, _exclude_content = nil) return false unless res.code == 400 - full_res = Browser.get(res.effective_url, cache_ttl: 0) - - full_res.body =~ /no image specified/i ? true : false + Browser.get(res.effective_url).body =~ /no image specified/i ? true : false end # @param [ Hash ] opts diff --git a/lib/wpscan/finders.rb b/lib/wpscan/finders.rb index c191f5f6..97bc25ef 100644 --- a/lib/wpscan/finders.rb +++ b/lib/wpscan/finders.rb @@ -39,9 +39,6 @@ module WPScan # # @yield [ Typhoeus::Response, String ] def enumerate(urls, opts = {}) - determine_request_params(urls, opts) - # determine_valid_response_codes(opts) - create_progress_bar(opts.merge(total: urls.size)) urls.each do |url, slug| @@ -59,6 +56,11 @@ module WPScan hydra.run end + # @return [ Hash ] + def request_params + @request_params ||= target.head_or_get_request_params.merge(cache_ttl: 0) + end + # @param [ Typhoeus::Response ] res # @param [ Regexp,nil ] exclude_content # @@ -71,7 +73,9 @@ module WPScan # Perform a full get to check if homepage or custom 404 if res.code == 200 - full_res = Browser.get(res.effective_url, cache_ttl: 0) + # The cache is not disabled to avoid additional request/s when checking + # for directory listing + full_res = Browser.get(res.effective_url) return false if target.homepage_or_404?(full_res) || exclude_content && full_res.body.match(exclude_content) @@ -81,23 +85,6 @@ module WPScan end # rubocop:enable Metrics/PerceivedComplexity - # @return [ Hash ] - def request_params - @request_params ||= { cache_ttl: 0 } - end - - # @param [ Hash ] urls - # @param [ Hash ] opts - def determine_request_params(urls, _opts) - head_res = Browser.head(urls.first[0], cache_ttl: 0) - - @request_params = if head_res.code == 405 - { method: :get, maxfilesize: 1, cache_ttl: 0 } - else - { method: :head, cache_ttl: 0 } - end - end - # @return [ Array ] def valid_response_codes @valid_response_codes ||= [200, 401, 403, 301] diff --git a/lib/wpscan/target.rb b/lib/wpscan/target.rb index 4adea28a..b23cf53a 100644 --- a/lib/wpscan/target.rb +++ b/lib/wpscan/target.rb @@ -5,6 +5,15 @@ module WPScan class Target < CMSScanner::Target include Platform::WordPress + # @return [ Hash ] + def head_or_get_request_params + @head_or_get_request_params ||= if Browser.head(url).code == 405 + { method: :get, maxfilesize: 1 } + else + { method: :head } + end + end + # @return [ Boolean ] def vulnerable? [@wp_version, @main_theme, @plugins, @themes, @timthumbs].each do |e| diff --git a/spec/app/finders/db_exports/known_locations_spec.rb b/spec/app/finders/db_exports/known_locations_spec.rb index bd58713e..080b1e73 100644 --- a/spec/app/finders/db_exports/known_locations_spec.rb +++ b/spec/app/finders/db_exports/known_locations_spec.rb @@ -25,10 +25,10 @@ describe WPScan::Finders::DbExports::KnownLocations do describe '#aggressive' do before do expect(target).to receive(:sub_dir).at_least(1).and_return(false) - expect(target).to receive(:homepage_or_404?).at_least(1).and_return(false) + expect(target).to receive(:head_or_get_request_params).and_return(method: :head) finder.potential_urls(opts).each_key do |url| - stub_request(:get, url).to_return(status: 404) + stub_request(:head, url).to_return(status: 404) end end @@ -38,20 +38,28 @@ describe WPScan::Finders::DbExports::KnownLocations do end end + context 'when a zip returns a 200' do + xit + end + context 'when some files exist' do - let(:files) { %w[ex.sql backups/db_backup.sql] } + let(:found_files) { %w[ex.sql backups/db_backup.sql] } let(:db_export) { File.read(fixtures.join('dump.sql')) } before do - files.each do |file| - stub_request(:get, "#{url}#{file}").to_return(body: db_export) + found_files.each do |file| + stub_request(:head, "#{url}#{file}").to_return(status: 200) + + stub_request(:get, "#{url}#{file}") + .with(headers: { 'Range' => 'bytes=0-3000' }) + .to_return(body: db_export) end end it 'returns the expected Array' do expected = [] - files.each do |file| + found_files.each do |file| url = "#{target.url}#{file}" expected << WPScan::DbExport.new( url, diff --git a/spec/app/finders/interesting_findings/upload_sql_dump_spec.rb b/spec/app/finders/interesting_findings/upload_sql_dump_spec.rb index c8881604..a83718ba 100644 --- a/spec/app/finders/interesting_findings/upload_sql_dump_spec.rb +++ b/spec/app/finders/interesting_findings/upload_sql_dump_spec.rb @@ -6,13 +6,16 @@ describe WPScan::Finders::InterestingFindings::UploadSQLDump do let(:wp_content) { 'wp-content' } describe '#aggressive' do - before { expect(target).to receive(:content_dir).at_least(1).and_return(wp_content) } + before do + expect(target).to receive(:content_dir).at_least(1).and_return(wp_content) + expect(target).to receive(:head_or_get_request_params).and_return(method: :head) + end - after { expect(finder.aggressive).to eql @expected } + after { expect(finder.aggressive).to eql @expected } context 'when not a 200' do it 'returns nil' do - stub_request(:get, finder.dump_url).to_return(status: 404) + stub_request(:head, finder.dump_url).to_return(status: 404) @expected = nil end @@ -20,8 +23,11 @@ describe WPScan::Finders::InterestingFindings::UploadSQLDump do context 'when a 200' do before do + stub_request(:head, finder.dump_url).to_return(status: 200) + stub_request(:get, finder.dump_url) - .to_return(status: 200, body: File.read(fixtures.join(fixture))) + .with(headers: { 'Range' => 'bytes=0-3000' }) + .to_return(body: File.read(fixtures.join(fixture))) end context 'when the body does not match a SQL dump' do From fa0582ce0b212b60cfbfb165209edb60060803b3 Mon Sep 17 00:00:00 2001 From: erwanlr Date: Fri, 22 Mar 2019 20:35:22 +0000 Subject: [PATCH 13/13] Uses head or get method to enumerate config backups --- app/finders/config_backups/known_filenames.rb | 13 +++++++++---- .../finders/config_backups/known_filenames_spec.rb | 11 ++++++----- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/app/finders/config_backups/known_filenames.rb b/app/finders/config_backups/known_filenames.rb index 6dc9f38f..f93503f5 100644 --- a/app/finders/config_backups/known_filenames.rb +++ b/app/finders/config_backups/known_filenames.rb @@ -3,7 +3,7 @@ module WPScan module ConfigBackups # Config Backup finder class KnownFilenames < CMSScanner::Finders::Finder - include CMSScanner::Finders::Finder::Enumerator + include Finders::Finder::Enumerator # @param [ Hash ] opts # @option opts [ String ] :list @@ -14,15 +14,20 @@ module WPScan found = [] enumerate(potential_urls(opts), opts) do |res| - # Might need to improve that - next unless res.body =~ /define/i && res.body !~ /<\s?html/i - found << WPScan::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100) end found end + def valid_response?(res, _exclude_content = nil) + return unless res.code == 200 + + full_res = Browser.get(res.effective_url) + + full_res.body =~ /define/i && full_res.body !~ /<\s?html/i + end + # @param [ Hash ] opts # @option opts [ String ] :list Mandatory # diff --git a/spec/app/finders/config_backups/known_filenames_spec.rb b/spec/app/finders/config_backups/known_filenames_spec.rb index ba8c97a7..c1abdaa0 100644 --- a/spec/app/finders/config_backups/known_filenames_spec.rb +++ b/spec/app/finders/config_backups/known_filenames_spec.rb @@ -8,10 +8,10 @@ describe WPScan::Finders::ConfigBackups::KnownFilenames do describe '#aggressive' do before do expect(target).to receive(:sub_dir).at_least(1).and_return(false) - expect(target).to receive(:homepage_or_404?).at_least(1).and_return(false) + expect(target).to receive(:head_or_get_request_params).and_return(method: :head) finder.potential_urls(opts).each_key do |url| - stub_request(:get, url).to_return(status: 404) + stub_request(:head, url).to_return(status: 404) end end @@ -22,11 +22,12 @@ describe WPScan::Finders::ConfigBackups::KnownFilenames do end context 'when some files exist' do - let(:files) { ['%23wp-config.php%23', 'wp-config.bak'] } + let(:found_files) { ['%23wp-config.php%23', 'wp-config.bak'] } let(:config_backup) { File.read(fixtures.join('wp-config.php')) } before do - files.each do |file| + found_files.each do |file| + stub_request(:head, "#{url}#{file}").to_return(status: 200) stub_request(:get, "#{url}#{file}").to_return(body: config_backup) end end @@ -34,7 +35,7 @@ describe WPScan::Finders::ConfigBackups::KnownFilenames do it 'returns the expected Array' do expected = [] - files.each do |file| + found_files.each do |file| url = "#{target.url}#{file}" expected << WPScan::ConfigBackup.new( url,