Improves detection of WP Version, Plugins etc by checking 404

This commit is contained in:
erwanlr
2019-10-31 19:56:05 +00:00
parent 85aa9f61cd
commit 6b5e016770
44 changed files with 456 additions and 146 deletions

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'main_theme/css_style' require_relative 'main_theme/css_style_in_homepage'
require_relative 'main_theme/css_style_in_404_page'
require_relative 'main_theme/woo_framework_meta_generator' require_relative 'main_theme/woo_framework_meta_generator'
require_relative 'main_theme/urls_in_homepage' require_relative 'main_theme/urls_in_homepage'
require_relative 'main_theme/urls_in_404_page'
module WPScan module WPScan
module Finders module Finders
@@ -14,9 +16,11 @@ module WPScan
# @param [ WPScan::Target ] target # @param [ WPScan::Target ] target
def initialize(target) def initialize(target)
finders << finders <<
MainTheme::CssStyle.new(target) << MainTheme::CssStyleInHomepage.new(target) <<
MainTheme::CssStyleIn404Page.new(target) <<
MainTheme::WooFrameworkMetaGenerator.new(target) << MainTheme::WooFrameworkMetaGenerator.new(target) <<
MainTheme::UrlsInHomepage.new(target) MainTheme::UrlsInHomepage.new(target) <<
MainTheme::UrlsIn404Page.new(target)
end end
end end
end end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
# From the CSS style in the 404 page
class CssStyleIn404Page < CssStyleInHomepage
def passive(opts = {})
passive_from_css_href(target.error_404_res, opts) || passive_from_style_code(target.error_404_res, opts)
end
end
end
end
end

View File

@@ -3,9 +3,9 @@
module WPScan module WPScan
module Finders module Finders
module MainTheme module MainTheme
# From the css style # From the CSS style in the homepage
class CssStyle < CMSScanner::Finders::Finder class CssStyleInHomepage < CMSScanner::Finders::Finder
include Finders::WpItems::URLsInHomepage include Finders::WpItems::UrlsInPage # To have the item_code_pattern method available here
def create_theme(slug, style_url, opts) def create_theme(slug, style_url, opts)
Model::Theme.new( Model::Theme.new(

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
# URLs In 404 Page Finder
class UrlsIn404Page < UrlsInHomepage
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.error_404_res
end
end
end
end
end

View File

@@ -5,7 +5,7 @@ module WPScan
module MainTheme module MainTheme
# URLs In Homepage Finder # URLs In Homepage Finder
class UrlsInHomepage < CMSScanner::Finders::Finder class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::URLsInHomepage include WpItems::UrlsInPage
# @param [ Hash ] opts # @param [ Hash ] opts
# #
@@ -21,6 +21,11 @@ module WPScan
found found
end end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end end
end end
end end

View File

@@ -10,7 +10,7 @@ module WPScan
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
def passive(opts = {}) def passive(opts = {})
return unless target.homepage_res.body =~ PATTERN return unless target.homepage_res.body =~ PATTERN || target.error_404_res.body =~ PATTERN
Model::Theme.new( Model::Theme.new(
Regexp.last_match[1], Regexp.last_match[1],

View File

@@ -1,6 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'plugins/urls_in_homepage' require_relative 'plugins/urls_in_homepage'
require_relative 'plugins/urls_in_404_page'
require_relative 'plugins/known_locations' require_relative 'plugins/known_locations'
# From the DynamicFinders # From the DynamicFinders
require_relative 'plugins/comment' require_relative 'plugins/comment'
@@ -22,6 +23,7 @@ module WPScan
def initialize(target) def initialize(target)
finders << finders <<
Plugins::UrlsInHomepage.new(target) << Plugins::UrlsInHomepage.new(target) <<
Plugins::UrlsIn404Page.new(target) <<
Plugins::HeaderPattern.new(target) << Plugins::HeaderPattern.new(target) <<
Plugins::Comment.new(target) << Plugins::Comment.new(target) <<
Plugins::Xpath.new(target) << Plugins::Xpath.new(target) <<

View File

@@ -0,0 +1,16 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
# URLs In 404 Page Finder
# Typically, the items detected from URLs like /wp-content/plugins/<slug>/
class UrlsIn404Page < UrlsInHomepage
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.error_404_res
end
end
end
end
end

View File

@@ -4,10 +4,9 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# URLs In Homepage Finder # URLs In Homepage Finder
# Typically, the items detected from URLs like # Typically, the items detected from URLs like /wp-content/plugins/<slug>/
# /wp-content/plugins/<slug>/
class UrlsInHomepage < CMSScanner::Finders::Finder class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::URLsInHomepage include WpItems::UrlsInPage
# @param [ Hash ] opts # @param [ Hash ] opts
# #
@@ -21,6 +20,11 @@ module WPScan
found found
end end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end end
end end
end end

View File

@@ -1,12 +1,13 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'themes/urls_in_homepage' require_relative 'themes/urls_in_homepage'
require_relative 'themes/urls_in_404_page'
require_relative 'themes/known_locations' require_relative 'themes/known_locations'
module WPScan module WPScan
module Finders module Finders
module Themes module Themes
# themes Finder # Themes Finder
class Base class Base
include CMSScanner::Finders::SameTypeFinder include CMSScanner::Finders::SameTypeFinder
@@ -14,6 +15,7 @@ module WPScan
def initialize(target) def initialize(target)
finders << finders <<
Themes::UrlsInHomepage.new(target) << Themes::UrlsInHomepage.new(target) <<
Themes::UrlsIn404Page.new(target) <<
Themes::KnownLocations.new(target) Themes::KnownLocations.new(target)
end end
end end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module WPScan
module Finders
module Themes
# URLs In 04 Page Finder
class UrlsIn404Page < UrlsInHomepage
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.error_404_res
end
end
end
end
end

View File

@@ -5,7 +5,7 @@ module WPScan
module Themes module Themes
# URLs In Homepage Finder # URLs In Homepage Finder
class UrlsInHomepage < CMSScanner::Finders::Finder class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::URLsInHomepage include WpItems::UrlsInPage
# @param [ Hash ] opts # @param [ Hash ] opts
# #
@@ -19,6 +19,11 @@ module WPScan
found found
end end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end end
end end
end end

View File

@@ -1,3 +1,3 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'wp_items/urls_in_homepage' require_relative 'wp_items/urls_in_page'

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module WpItems module WpItems
# URLs In Homepage Module to use in plugins & themes finders # URLs In Homepage Module to use in plugins & themes finders
module URLsInHomepage module UrlsInPage
# @param [ String ] type plugins / themes # @param [ String ] type plugins / themes
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results # @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
# #
@@ -12,7 +12,7 @@ module WPScan
def items_from_links(type, uniq = true) def items_from_links(type, uniq = true)
found = [] found = []
target.in_scope_uris(target.homepage_res) do |uri| target.in_scope_uris(page_res) do |uri|
next unless uri.to_s =~ item_attribute_pattern(type) next unless uri.to_s =~ item_attribute_pattern(type)
slug = Regexp.last_match[1]&.strip slug = Regexp.last_match[1]&.strip
@@ -30,7 +30,7 @@ module WPScan
def items_from_codes(type, uniq = true) def items_from_codes(type, uniq = true)
found = [] found = []
target.homepage_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag| page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
code = tag.text.to_s code = tag.text.to_s
next if code.empty? next if code.empty?

View File

@@ -14,7 +14,7 @@ module WPScan
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog delegate :homepage_res, :error_404_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog
# @param [ String ] slug The plugin/theme slug # @param [ String ] slug The plugin/theme slug
# @param [ Target ] blog The targeted blog # @param [ Target ] blog The targeted blog

View File

@@ -44,19 +44,27 @@ module WPScan
# #
# @param [ Typhoeus::Response ] response # @param [ Typhoeus::Response ] response
# @param [ Hash ] opts # @param [ Hash ] opts
# @return [ Mixed ] # @return [ Mixed: nil, Object, Array ]
def find(_response, _opts = {}) def find(_response, _opts = {})
raise NoMethodError raise NoMethodError
end end
# @param [ Hash ] opts # @param [ Hash ] opts
# @return [ Mixed ] See #find
def passive(opts = {}) def passive(opts = {})
return if self.class::PATH return if self.class::PATH
find(target.homepage_res, opts) homepage_result = find(target.homepage_res, opts)
if homepage_result
return homepage_result unless homepage_result.is_a?(Array) && homepage_result.empty?
end
find(target.error_404_res, opts)
end end
# @param [ Hash ] opts # @param [ Hash ] opts
# @return [ Mixed ] See #find
def aggressive(opts = {}) def aggressive(opts = {})
return unless self.class::PATH return unless self.class::PATH

View File

@@ -31,11 +31,13 @@ module WPScan
passive_configs.each do |slug, configs| passive_configs.each do |slug, configs|
configs.each do |klass, config| configs.each do |klass, config|
item = process_response(opts, target.homepage_res, slug, klass, config) [target.homepage_res, target.error_404_res].each do |page_res|
item = process_response(opts, page_res, slug, klass, config)
found << item if item.is_a?(Model::WpItem) found << item if item.is_a?(Model::WpItem)
end end
end end
end
found found
end end

View File

@@ -24,21 +24,10 @@ module WPScan
# @param [ Symbol ] detection_mode # @param [ Symbol ] detection_mode
# #
# @return [ Boolean ] # @return [ Boolean ] Whether or not the target is running WordPress
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
def wordpress?(detection_mode) def wordpress?(detection_mode)
in_scope_uris(homepage_res) do |uri| [homepage_res, error_404_res].each do |page_res|
return true if WORDPRESS_PATTERN.match?(uri.path) || WP_JSON_OEMBED_PATTERN.match?(uri.path) return true if wordpress_from_meta_comments_or_scripts?(page_res)
end
return true if homepage_res.html.css('meta[name="generator"]').any? do |node|
/wordpress/i.match?(node['content'])
end
return true unless comments_from_page(/wordpress/i, homepage_res).empty?
return true if homepage_res.html.xpath('//script[not(@src)]').any? do |node|
WP_ADMIN_AJAX_PATTERN.match?(node.text)
end end
if %i[mixed aggressive].include?(detection_mode) if %i[mixed aggressive].include?(detection_mode)
@@ -51,7 +40,26 @@ module WPScan
false false
end end
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
# @param [ Typhoeus::Response ] response
# @return [ Boolean ]
def wordpress_from_meta_comments_or_scripts?(response)
in_scope_uris(response) do |uri|
return true if WORDPRESS_PATTERN.match?(uri.path) || WP_JSON_OEMBED_PATTERN.match?(uri.path)
end
return true if response.html.css('meta[name="generator"]').any? do |node|
/wordpress/i.match?(node['content'])
end
return true unless comments_from_page(/wordpress/i, response).empty?
return true if response.html.xpath('//script[not(@src)]').any? do |node|
WP_ADMIN_AJAX_PATTERN.match?(node.text)
end
false
end
COOKIE_PATTERNS = { COOKIE_PATTERNS = {
'vjs' => /createCookie\('vjs','(?<c_value>\d+)',\d+\);/i 'vjs' => /createCookie\('vjs','(?<c_value>\d+)',\d+\);/i

View File

@@ -19,14 +19,16 @@ module WPScan
# scope_url_pattern is from CMSScanner::Target # scope_url_pattern is from CMSScanner::Target
pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i
in_scope_uris(homepage_res, '//link/@href|//script/@src|//img/@src') do |uri| [homepage_res, error_404_res].each do |page_res|
in_scope_uris(page_res, '//link/@href|//script/@src|//img/@src') do |uri|
return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern) return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
end end
# Checks for the pattern in raw JS code, as well as @content attributes of meta tags # Checks for the pattern in raw JS code, as well as @content attributes of meta tags
xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, homepage_res) do |match| xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, page_res) do |match|
return @content_dir = match[1] return @content_dir = match[1]
end end
end
return @content_dir = 'wp-content' if default_content_dir_exists? return @content_dir = 'wp-content' if default_content_dir_exists?
end end
@@ -104,9 +106,11 @@ module WPScan
# url_pattern is from CMSScanner::Target # url_pattern is from CMSScanner::Target
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i
in_scope_uris(homepage_res) do |uri| [homepage_res, error_404_res].each do |page_res|
in_scope_uris(page_res) do |uri|
return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern) return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
end end
end
@sub_dir = false @sub_dir = false
end end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
describe WPScan::Finders::MainTheme::CssStyleIn404Page do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url).extend(CMSScanner::Target::Server::Apache) }
let(:url) { 'http://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'css_style_in_404_page') }
# This stuff is just a child class of CssStyleInHomepage (using the error_404_res rather than homepage_res)
# which already has a spec
end

View File

@@ -1,10 +1,10 @@
# frozen_string_literal: true # frozen_string_literal: true
describe WPScan::Finders::MainTheme::CssStyle do describe WPScan::Finders::MainTheme::CssStyleInHomepage do
subject(:finder) { described_class.new(target) } subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url).extend(CMSScanner::Target::Server::Apache) } let(:target) { WPScan::Target.new(url).extend(CMSScanner::Target::Server::Apache) }
let(:url) { 'http://wp.lab/' } let(:url) { 'http://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'css_style') } let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'css_style_in_homepage') }
describe '#passive' do describe '#passive' do
after do after do
@@ -33,7 +33,7 @@ describe WPScan::Finders::MainTheme::CssStyle do
@expected = WPScan::Model::Theme.new( @expected = WPScan::Model::Theme.new(
'twentyfifteen', 'twentyfifteen',
target, target,
found_by: 'Css Style (Passive Detection)', found_by: 'Css Style In Homepage (Passive Detection)',
confidence: 70, confidence: 70,
style_url: 'http://wp.lab/wp-content/themes/twentyfifteen/style.css?ver=4.1.1' style_url: 'http://wp.lab/wp-content/themes/twentyfifteen/style.css?ver=4.1.1'
) )
@@ -47,7 +47,7 @@ describe WPScan::Finders::MainTheme::CssStyle do
@expected = WPScan::Model::Theme.new( @expected = WPScan::Model::Theme.new(
'custom', 'custom',
target, target,
found_by: 'Css Style (Passive Detection)', found_by: 'Css Style In Homepage (Passive Detection)',
confidence: 70, confidence: 70,
style_url: 'http://wp.lab/wp-content/themes/custom/style.css' style_url: 'http://wp.lab/wp-content/themes/custom/style.css'
) )

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
describe WPScan::Finders::MainTheme::UrlsIn404Page do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url) }
let(:url) { 'http://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'urls_in_404_page') }
# This stuff is just a child class of URLsInHomepage (using the error_404_res rather than homepage_res)
# which already has a spec
end

View File

@@ -6,7 +6,8 @@ describe WPScan::Finders::MainTheme::UrlsInHomepage do
let(:url) { 'http://wp.lab/' } let(:url) { 'http://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'urls_in_homepage') } let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'urls_in_homepage') }
it_behaves_like 'App::Finders::WpItems::URLsInHomepage' do it_behaves_like 'App::Finders::WpItems::UrlsInPage' do
let(:page_url) { url }
let(:type) { 'themes' } let(:type) { 'themes' }
let(:uniq_links) { false } let(:uniq_links) { false }
let(:uniq_codes) { false } let(:uniq_codes) { false }
@@ -18,6 +19,8 @@ describe WPScan::Finders::MainTheme::UrlsInHomepage do
before do before do
stub_request(:get, /.*.css/) stub_request(:get, /.*.css/)
stub_request(:get, target.url).to_return(body: File.read(fixtures.join('found.html'))) stub_request(:get, target.url).to_return(body: File.read(fixtures.join('found.html')))
allow(target).to receive(:content_dir).and_return('wp-content')
end end
it 'returns the expected Themes' do it 'returns the expected Themes' do

View File

@@ -7,28 +7,45 @@ describe WPScan::Finders::MainTheme::WooFrameworkMetaGenerator do
let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'woo_framework_meta_generator') } let(:fixtures) { FINDERS_FIXTURES.join('main_theme', 'woo_framework_meta_generator') }
describe '#passive' do describe '#passive' do
after do before do
stub_request(:get, url).to_return(body: File.read(fixtures.join(@file))) stub_request(:get, url).to_return(body: File.read(fixtures.join(homepage_fixture)))
stub_request(:get, ERROR_404_URL_PATTERN).to_return(body: File.read(fixtures.join(error_404_fixture)))
expect(finder.passive).to eql @expected
end end
context 'when no Woo generator' do context 'when no Woo generator' do
let(:homepage_fixture) { 'no_woo_generator.html' }
let(:error_404_fixture) { 'no_woo_generator.html' }
it 'returns nil' do it 'returns nil' do
@file = 'no_woo_generator.html' expect(finder.passive).to eql nil
@expected = nil
end end
end end
context 'when Woo generator' do context 'when Woo generator' do
before do before do
expect(target).to receive(:content_dir).at_least(1).and_return('wp-content') allow(target).to receive(:content_dir).and_return('wp-content')
stub_request(:get, "#{url}wp-content/themes/Merchant/style.css") stub_request(:get, "#{url}wp-content/themes/Merchant/style.css")
end end
context 'from the homepage' do
let(:homepage_fixture) { 'woo_generator.html' }
let(:error_404_fixture) { 'no_woo_generator.html' }
it 'returns the expected theme' do it 'returns the expected theme' do
@file = 'woo_generator.html' expect(finder.passive).to eql WPScan::Model::Theme.new(
@expected = WPScan::Model::Theme.new( 'Merchant', target,
found_by: 'Woo Framework Meta Generator (Passive Detection)',
confidence: 80
)
end
end
context 'from the 404 page' do
let(:homepage_fixture) { 'no_woo_generator.html' }
let(:error_404_fixture) { 'woo_generator.html' }
it 'returns the expected theme' do
expect(finder.passive).to eql WPScan::Model::Theme.new(
'Merchant', target, 'Merchant', target,
found_by: 'Woo Framework Meta Generator (Passive Detection)', found_by: 'Woo Framework Meta Generator (Passive Detection)',
confidence: 80 confidence: 80
@@ -36,4 +53,5 @@ describe WPScan::Finders::MainTheme::WooFrameworkMetaGenerator do
end end
end end
end end
end
end end

View File

@@ -8,7 +8,7 @@ describe WPScan::Finders::MainTheme::Base do
describe '#finders' do describe '#finders' do
it 'contains the expected finders' do it 'contains the expected finders' do
expect(main_theme.finders.map { |f| f.class.to_s.demodulize }) expect(main_theme.finders.map { |f| f.class.to_s.demodulize })
.to eq %w[CssStyle WooFrameworkMetaGenerator UrlsInHomepage] .to eq %w[CssStyleInHomepage CssStyleIn404Page WooFrameworkMetaGenerator UrlsInHomepage UrlsIn404Page]
end end
end end
end end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
describe WPScan::Finders::Plugins::UrlsIn404Page do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url) }
let(:url) { 'https://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('plugins', 'urls_in_404_page') }
# This stuff is just a child class of URLsInHomepage (using the error_404_res rather than homepage_res)
# which already has a spec
end

View File

@@ -8,7 +8,8 @@ describe WPScan::Finders::Plugins::UrlsInHomepage do
before { target.scope << 'sub.lab' } before { target.scope << 'sub.lab' }
it_behaves_like 'App::Finders::WpItems::URLsInHomepage' do it_behaves_like 'App::Finders::WpItems::UrlsInPage' do
let(:page_url) { url }
let(:type) { 'plugins' } let(:type) { 'plugins' }
let(:uniq_links) { true } let(:uniq_links) { true }
let(:uniq_codes) { true } let(:uniq_codes) { true }

View File

@@ -8,7 +8,7 @@ describe WPScan::Finders::Plugins::Base do
describe '#finders' do describe '#finders' do
it 'contains the expected finders' do it 'contains the expected finders' do
expect(plugins.finders.map { |f| f.class.to_s.demodulize }) expect(plugins.finders.map { |f| f.class.to_s.demodulize })
.to eq %w[UrlsInHomepage HeaderPattern Comment Xpath BodyPattern JavascriptVar KnownLocations] .to eq %w[UrlsInHomepage UrlsIn404Page HeaderPattern Comment Xpath BodyPattern JavascriptVar KnownLocations]
end end
end end
end end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
describe WPScan::Finders::Themes::UrlsIn404Page do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url) }
let(:url) { 'http://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('themes', 'urls_in_404_page') }
# This stuff is just a child class of URLsInHomepage (using the error_404_res rather than homepage_res)
# which already has a spec
end

View File

@@ -8,7 +8,8 @@ describe WPScan::Finders::Themes::UrlsInHomepage do
# before { target.scope << 'sub.lab' } # before { target.scope << 'sub.lab' }
it_behaves_like 'App::Finders::WpItems::URLsInHomepage' do it_behaves_like 'App::Finders::WpItems::UrlsInPage' do
let(:page_url) { url }
let(:type) { 'themes' } let(:type) { 'themes' }
let(:uniq_links) { true } let(:uniq_links) { true }
let(:uniq_codes) { true } let(:uniq_codes) { true }

View File

@@ -8,7 +8,7 @@ describe WPScan::Finders::Themes::Base do
describe '#finders' do describe '#finders' do
it 'contains the expected finders' do it 'contains the expected finders' do
expect(themes.finders.map { |f| f.class.to_s.demodulize }) expect(themes.finders.map { |f| f.class.to_s.demodulize })
.to eq %w[UrlsInHomepage KnownLocations] .to eq %w[UrlsInHomepage UrlsIn404Page KnownLocations]
end end
end end
end end

View File

@@ -87,6 +87,8 @@ describe WPScan::Finders::Users::WpJsonApi do
describe '#api_url' do describe '#api_url' do
let(:fixtures) { super().join('api_url') } let(:fixtures) { super().join('api_url') }
before { allow(target).to receive(:sub_dir).and_return(false) }
context 'when url in the homepage' do context 'when url in the homepage' do
{ {
in_scope: 'https://wp.lab/wp-json/wp/v2/users/', in_scope: 'https://wp.lab/wp-json/wp/v2/users/',
@@ -100,7 +102,7 @@ describe WPScan::Finders::Users::WpJsonApi do
end end
context 'when subdir' do context 'when subdir' do
before { allow(target).to receive(:subdir).and_return('cms') } before { allow(target).to receive(:sub_dir).and_return('cms') }
{ {
in_scope_subdir: 'https://wp.lab/cms/wp-json/wp/v2/users/', in_scope_subdir: 'https://wp.lab/cms/wp-json/wp/v2/users/',

View File

@@ -38,13 +38,21 @@ WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |slug, confi
end end
end end
let(:stubbed_response) { { body: 'aa' } } before { allow(target).to receive(:content_dir).and_return('wp-content') }
describe '#passive', slow: true do describe '#passive', slow: true do
before do before do
stub_request(:get, target.url).to_return(stubbed_response) if defined?(stubbed_homepage_res)
stub_request(:get, target.url).to_return(stubbed_homepage_res)
else
stub_request(:get, target.url)
end
expect(target).to receive(:content_dir).at_least(1).and_return('wp-content') if defined?(stubbed_404_res)
stub_request(:get, ERROR_404_URL_PATTERN).to_return(stubbed_404_res)
else
stub_request(:get, ERROR_404_URL_PATTERN)
end
end end
if config['path'] if config['path']
@@ -56,31 +64,67 @@ WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |slug, confi
else else
context 'when no PATH' do context 'when no PATH' do
context 'when the version is detected' do context 'when the version is detected' do
let(:stubbed_response) do context 'from the homepage' do
let(:ie_url) { target.url }
let(:stubbed_homepage_res) do
df_stubbed_response( df_stubbed_response(
fixtures.join("#{finder_super_class.underscore}_passive_all.html"), fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
finder_super_class finder_super_class
) )
end end
it 'returns the expected version/s from the homepage' do it 'returns the expected version/s' do
found = [*finder.passive] found = [*finder.passive]
expect(found).to_not be_empty expect(found).to_not be_empty
found.each_with_index do |version, index| found.each_with_index do |version, index|
expected_version = expected.at(index) expected_version = expected.at(index)
expected_ie = expected_version['interesting_entries'].map do |ie|
ie.gsub(target.url + ',', ie_url + ',')
end
expect(version).to be_a WPScan::Model::Version expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by'] expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_version['interesting_entries'] expect(version.interesting_entries).to match_array expected_ie
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence'] expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end end
end end
end end
context 'from the 404' do
let(:ie_url) { target.error_404_url }
let(:stubbed_404_res) do
df_stubbed_response(
fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
finder_super_class
)
end
it 'returns the expected version/s' do
found = [*finder.passive]
expect(found).to_not be_empty
found.each_with_index do |version, index|
expected_version = expected.at(index)
expected_ie = expected_version['interesting_entries'].map do |ie|
ie.gsub(target.url + ',', ie_url + ',')
end
expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_ie
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end
end
end
end
context 'when the version is not detected' do context 'when the version is not detected' do
it 'returns nil or an empty array' do it 'returns nil or an empty array' do
expect(finder.passive).to eql finder_super_class == 'QueryParameter' ? [] : nil expect(finder.passive).to eql finder_super_class == 'QueryParameter' ? [] : nil
@@ -92,10 +136,9 @@ WPScan::DB::DynamicFinders::Plugin.versions_finders_configs.each do |slug, confi
describe '#aggressive' do describe '#aggressive' do
let(:fixtures) { super().join(slug, finder_class.underscore) } let(:fixtures) { super().join(slug, finder_class.underscore) }
let(:stubbed_response) { { body: 'aa' } }
before do before do
expect(target).to receive(:content_dir).at_least(1).and_return('wp-content')
stub_request(:get, plugin.url(config['path'])).to_return(stubbed_response) if config['path'] stub_request(:get, plugin.url(config['path'])).to_return(stubbed_response) if config['path']
end end

View File

@@ -38,8 +38,6 @@ WPScan::DB::DynamicFinders::Theme.versions_finders_configs.each do |slug, config
end end
end end
let(:stubbed_response) { { body: 'aa' } }
before do before do
allow(target).to receive(:content_dir).and_return('wp-content') allow(target).to receive(:content_dir).and_return('wp-content')
@@ -48,7 +46,19 @@ WPScan::DB::DynamicFinders::Theme.versions_finders_configs.each do |slug, config
end end
describe '#passive', slow: true do describe '#passive', slow: true do
before { stub_request(:get, target.url).to_return(stubbed_response) } before do
if defined?(stubbed_homepage_res)
stub_request(:get, target.url).to_return(stubbed_homepage_res)
else
stub_request(:get, target.url)
end
if defined?(stubbed_404_res)
stub_request(:get, ERROR_404_URL_PATTERN).to_return(stubbed_404_res)
else
stub_request(:get, ERROR_404_URL_PATTERN)
end
end
if config['path'] if config['path']
context 'when PATH' do context 'when PATH' do
@@ -59,31 +69,67 @@ WPScan::DB::DynamicFinders::Theme.versions_finders_configs.each do |slug, config
else else
context 'when no PATH' do context 'when no PATH' do
context 'when the version is detected' do context 'when the version is detected' do
let(:stubbed_response) do context 'from the homepage' do
let(:ie_url) { target.url }
let(:stubbed_homepage_res) do
df_stubbed_response( df_stubbed_response(
fixtures.join("#{finder_super_class.underscore}_passive_all.html"), fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
finder_super_class finder_super_class
) )
end end
it 'returns the expected version/s from the homepage' do it 'returns the expected version/s' do
found = [*finder.passive] found = [*finder.passive]
expect(found).to_not be_empty expect(found).to_not be_empty
found.each_with_index do |version, index| found.each_with_index do |version, index|
expected_version = expected.at(index) expected_version = expected.at(index)
expected_ie = expected_version['interesting_entries'].map do |ie|
ie.gsub(target.url + ',', ie_url + ',')
end
expect(version).to be_a WPScan::Model::Version expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by'] expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_version['interesting_entries'] expect(version.interesting_entries).to match_array expected_ie
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence'] expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end end
end end
end end
context 'from the 404' do
let(:ie_url) { target.error_404_url }
let(:stubbed_404_res) do
df_stubbed_response(
fixtures.join("#{finder_super_class.underscore}_passive_all.html"),
finder_super_class
)
end
it 'returns the expected version/s' do
found = [*finder.passive]
expect(found).to_not be_empty
found.each_with_index do |version, index|
expected_version = expected.at(index)
expected_ie = expected_version['interesting_entries'].map do |ie|
ie.gsub(target.url + ',', ie_url + ',')
end
expect(version).to be_a WPScan::Model::Version
expect(version.number).to eql expected_version['number'].to_s
expect(version.found_by).to eql expected_version['found_by']
expect(version.interesting_entries).to match_array expected_ie
expect(version.confidence).to eql expected_version['confidence'] if expected_version['confidence']
end
end
end
end
context 'when the version is not detected' do context 'when the version is not detected' do
it 'returns nil or an empty array' do it 'returns nil or an empty array' do
expect(finder.passive).to eql finder_super_class == 'QueryParameter' ? [] : nil expect(finder.passive).to eql finder_super_class == 'QueryParameter' ? [] : nil
@@ -95,6 +141,7 @@ WPScan::DB::DynamicFinders::Theme.versions_finders_configs.each do |slug, config
describe '#aggressive' do describe '#aggressive' do
let(:fixtures) { super().join(slug, finder_class.underscore) } let(:fixtures) { super().join(slug, finder_class.underscore) }
let(:stubbed_response) { { body: 'aa' } }
before do before do
stub_request(:get, theme.url(config['path'])).to_return(stubbed_response) if config['path'] stub_request(:get, theme.url(config['path'])).to_return(stubbed_response) if config['path']

View File

@@ -25,7 +25,10 @@ WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.each do |finder_c
let(:stubbed_response) { { body: '' } } let(:stubbed_response) { { body: '' } }
describe '#passive' do describe '#passive' do
before { stub_request(:get, target.url).to_return(stubbed_response) } before do
stub_request(:get, target.url).to_return(stubbed_response)
stub_request(:get, ERROR_404_URL_PATTERN)
end
if config['path'] if config['path']
context 'when PATH' do context 'when PATH' do
@@ -66,7 +69,7 @@ WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.each do |finder_c
let(:fixtures) { super().join(finder_class.underscore) } let(:fixtures) { super().join(finder_class.underscore) }
before do before do
allow(target).to receive(:sub_dir).and_return(nil) allow(target).to receive(:sub_dir).and_return(false)
stub_request(:get, target.url(config['path'])).to_return(stubbed_response) if config['path'] stub_request(:get, target.url(config['path'])).to_return(stubbed_response) if config['path']
end end

View File

@@ -5,6 +5,6 @@ require 'shared_examples/views/wp_version'
require 'shared_examples/views/main_theme' require 'shared_examples/views/main_theme'
require 'shared_examples/views/enumeration' require 'shared_examples/views/enumeration'
require 'shared_examples/target/platform/wordpress' require 'shared_examples/target/platform/wordpress'
require 'shared_examples/finders/wp_items/urls_in_homepage' require 'shared_examples/finders/wp_items/urls_in_page'
require 'shared_examples/references' require 'shared_examples/references'
require 'shared_examples/dynamic_finders/wp_items' require 'shared_examples/dynamic_finders/wp_items'

View File

@@ -16,13 +16,15 @@ shared_examples WPScan::Finders::DynamicFinder::WpItems::Finder do
describe '#passive' do describe '#passive' do
before do before do
stub_request(:get, target.url).to_return(body: body) stub_request(:get, target.url).to_return(body: homepage_body)
stub_request(:get, ERROR_404_URL_PATTERN).to_return(body: error_404_body)
allow(target).to receive(:content_dir).and_return('wp-content') allow(target).to receive(:content_dir).and_return('wp-content')
end end
context 'when no matches' do context 'when no matches' do
let(:body) { '' } let(:homepage_body) { '' }
let(:error_404_body) { '' }
it 'returns an empty array' do it 'returns an empty array' do
expect(finder.passive).to eql([]) expect(finder.passive).to eql([])
@@ -30,9 +32,7 @@ shared_examples WPScan::Finders::DynamicFinder::WpItems::Finder do
end end
context 'when matches' do context 'when matches' do
let(:body) { File.read(passive_fixture) } let(:expected_items) do
it 'contains the expected items' do
expected = [] expected = []
finder.passive_configs.each do |slug, configs| finder.passive_configs.each do |slug, configs|
@@ -48,7 +48,25 @@ shared_examples WPScan::Finders::DynamicFinder::WpItems::Finder do
end end
end end
expect(finder.passive).to match_array(expected.map { |item| eql(item) }) expected
end
context 'from the homepage' do
let(:homepage_body) { File.read(passive_fixture) }
let(:error_404_body) { '' }
it 'contains the expected items' do
expect(finder.passive).to match_array(expected_items.map { |item| eql(item) })
end
end
context 'from the 404' do
let(:homepage_body) { '' }
let(:error_404_body) { File.read(passive_fixture) }
it 'contains the expected items' do
expect(finder.passive).to match_array(expected_items.map { |item| eql(item) })
end
end end
end end
end end

View File

@@ -1,8 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
shared_examples 'App::Finders::WpItems::URLsInHomepage' do shared_examples 'App::Finders::WpItems::UrlsInPage' do
before do before do
stub_request(:get, finder.target.url).to_return(body: File.read(fixtures.join(file))) stub_request(:get, page_url).to_return(body: File.read(fixtures.join(file)))
end end
describe '#items_from_links' do describe '#items_from_links' do

View File

@@ -7,14 +7,17 @@ shared_examples WPScan::Target::Platform::WordPress do
let(:fixtures) { FIXTURES.join('target', 'platform', 'wordpress') } let(:fixtures) { FIXTURES.join('target', 'platform', 'wordpress') }
describe '#wordpress?' do describe '#wordpress?, wordpress_from_meta_comments_or_scripts?' do
let(:fixtures) { super().join('detection') } let(:fixtures) { super().join('detection') }
before do before do
stub_request(:get, target.url).to_return(body: File.read(fixtures.join("#{homepage}.html"))) stub_request(:get, target.url).to_return(body: File.read(fixtures.join("#{homepage}.html")))
stub_request(:get, ERROR_404_URL_PATTERN).to_return(body: File.read(fixtures.join("#{page_404}.html")))
end end
context 'when pattern/s in the homepage' do context 'when pattern/s in the homepage' do
let(:page_404) { 'not_wp' }
%w[default wp_includes only_scripts meta_generator comments mu_plugins wp_admin wp_json_oembed].each do |file| %w[default wp_includes only_scripts meta_generator comments mu_plugins wp_admin wp_json_oembed].each do |file|
context "when a wordpress page (#{file}.html)" do context "when a wordpress page (#{file}.html)" do
let(:homepage) { file } let(:homepage) { file }
@@ -29,6 +32,21 @@ shared_examples WPScan::Target::Platform::WordPress do
context 'when no clues in the homepage' do context 'when no clues in the homepage' do
let(:homepage) { 'not_wp' } let(:homepage) { 'not_wp' }
context 'when pattern/s in the 404 page' do
%w[default wp_includes only_scripts meta_generator comments mu_plugins wp_admin wp_json_oembed].each do |file|
context "when a wordpress page (#{file}.html)" do
let(:page_404) { file }
it 'returns true' do
expect(subject.wordpress?(:mixed)).to be true
end
end
end
end
context 'when no clues in the 404 page' do
let(:page_404) { 'not_wp' }
context 'when only passive detection mode' do context 'when only passive detection mode' do
it 'returns false' do it 'returns false' do
expect(subject.wordpress?(:passive)).to be false expect(subject.wordpress?(:passive)).to be false
@@ -67,6 +85,7 @@ shared_examples WPScan::Target::Platform::WordPress do
end end
end end
end end
end
describe '#maybe_add_cookies' do describe '#maybe_add_cookies' do
let(:fixtures) { super().join('maybe_add_cookies') } let(:fixtures) { super().join('maybe_add_cookies') }

View File

@@ -4,6 +4,9 @@ shared_examples 'WordPress::CustomDirectories' do
let(:fixtures) { super().join('custom_directories') } let(:fixtures) { super().join('custom_directories') }
describe '#content_dir' do describe '#content_dir' do
# Stub the error_404_res to make it easier to test
before { stub_request(:get, ERROR_404_URL_PATTERN) }
{ {
default: 'wp-content', https: 'wp-content', custom_w_spaces: 'custom content spaces', default: 'wp-content', https: 'wp-content', custom_w_spaces: 'custom content spaces',
relative_one: 'wp-content', relative_two: 'wp-content', cache: 'wp-content', relative_one: 'wp-content', relative_two: 'wp-content', cache: 'wp-content',
@@ -45,9 +48,9 @@ shared_examples 'WordPress::CustomDirectories' do
end end
end end
context 'when not found via the homepage' do context 'when not found via the homepage or 404' do
before do before do
stub_request(:get, target.url).to_return(body: '') stub_request(:get, target.url)
expect(target).to receive(:default_content_dir_exists?).and_return(dir_exist) expect(target).to receive(:default_content_dir_exists?).and_return(dir_exist)
end end
@@ -123,6 +126,9 @@ shared_examples 'WordPress::CustomDirectories' do
end end
describe '#sub_dir' do describe '#sub_dir' do
# Stub the error_404_res to make it easier to test
before { stub_request(:get, ERROR_404_URL_PATTERN) }
{ default: false, with_sub_dir: 'wp', relative_two_sub_dir: 'cms' }.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 it "returns #{expected} for #{file}.html" do
fixture = File.join(fixtures, "#{file}.html") fixture = File.join(fixtures, "#{file}.html")

View File

@@ -102,5 +102,6 @@ SPECS = Pathname.new(__FILE__).dirname
FIXTURES = SPECS.join('fixtures') FIXTURES = SPECS.join('fixtures')
FINDERS_FIXTURES = FIXTURES.join('finders') FINDERS_FIXTURES = FIXTURES.join('finders')
DYNAMIC_FINDERS_FIXTURES = FIXTURES.join('dynamic_finders') DYNAMIC_FINDERS_FIXTURES = FIXTURES.join('dynamic_finders')
ERROR_404_URL_PATTERN = %r{/[a-z\d]{7}\.html$}
redefine_constant(:DB_DIR, FIXTURES.join('db')) redefine_constant(:DB_DIR, FIXTURES.join('db'))