HELLO v3!!!
This commit is contained in:
17
app/finders/config_backups.rb
Normal file
17
app/finders/config_backups.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'config_backups/known_filenames'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ConfigBackups
|
||||
# Config Backup Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << ConfigBackups::KnownFilenames.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
46
app/finders/config_backups/known_filenames.rb
Normal file
46
app/finders/config_backups/known_filenames.rb
Normal file
@@ -0,0 +1,46 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module ConfigBackups
|
||||
# Config Backup finder
|
||||
class KnownFilenames < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
#
|
||||
# @return [ Array<ConfigBackup> ]
|
||||
def aggressive(opts = {})
|
||||
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
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def potential_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
File.open(opts[:list]).each_with_index do |file, index|
|
||||
urls[target.url(file.chomp)] = index
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Config Backups -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/db_exports.rb
Normal file
17
app/finders/db_exports.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'db_exports/known_locations'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module DbExports
|
||||
# DB Exports Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << DbExports::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/finders/db_exports/known_locations.rb
Normal file
49
app/finders/db_exports/known_locations.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module DbExports
|
||||
# DB Exports finder
|
||||
# See https://github.com/wpscanteam/wpscan-v3/issues/62
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
#
|
||||
# @return [ Array<DBExport> ]
|
||||
def aggressive(opts = {})
|
||||
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
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def potential_urls(opts = {})
|
||||
urls = {}
|
||||
domain_name = target.uri.host[/(^[\w|-]+)/, 1]
|
||||
|
||||
File.open(opts[:list]).each_with_index do |path, index|
|
||||
path.gsub!('{domain_name}', domain_name)
|
||||
|
||||
urls[target.url(path.chomp)] = index
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking DB Exports -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
app/finders/interesting_findings.rb
Normal file
34
app/finders/interesting_findings.rb
Normal file
@@ -0,0 +1,34 @@
|
||||
require_relative 'interesting_findings/readme'
|
||||
require_relative 'interesting_findings/multisite'
|
||||
require_relative 'interesting_findings/debug_log'
|
||||
require_relative 'interesting_findings/backup_db'
|
||||
require_relative 'interesting_findings/mu_plugins'
|
||||
require_relative 'interesting_findings/registration'
|
||||
require_relative 'interesting_findings/tmm_db_migrate'
|
||||
require_relative 'interesting_findings/upload_sql_dump'
|
||||
require_relative 'interesting_findings/full_path_disclosure'
|
||||
require_relative 'interesting_findings/duplicator_installer_log'
|
||||
require_relative 'interesting_findings/upload_directory_listing'
|
||||
require_relative 'interesting_findings/emergency_pwd_reset_script'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Interesting Files Finder
|
||||
class Base < CMSScanner::Finders::InterestingFindings::Base
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
super(target)
|
||||
|
||||
%w[
|
||||
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
|
||||
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
|
||||
UploadSQLDump EmergencyPwdResetScript
|
||||
].each do |f|
|
||||
finders << InterestingFindings.const_get(f).new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/interesting_findings/backup_db.rb
Normal file
25
app/finders/interesting_findings/backup_db.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# BackupDB finder
|
||||
class BackupDB < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/backup-db/'
|
||||
url = target.url(path)
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 70,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: target.directory_listing_entries(path),
|
||||
references: { url: 'https://github.com/wpscanteam/wpscan/issues/422' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
app/finders/interesting_findings/debug_log.rb
Normal file
20
app/finders/interesting_findings/debug_log.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# debug.log finder
|
||||
class DebugLog < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/debug.log'
|
||||
|
||||
return unless target.debug_log?(path)
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
target.url(path),
|
||||
confidence: 100, found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/finders/interesting_findings/duplicator_installer_log.rb
Normal file
23
app/finders/interesting_findings/duplicator_installer_log.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# DuplicatorInstallerLog finder
|
||||
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('installer-log.txt')
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.body =~ /DUPLICATOR INSTALL-LOG/
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Emergency Password Reset Script finder
|
||||
class EmergencyPwdResetScript < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('/emergency.php')
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.code == 200 && !target.homepage_or_404?(res)
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: res.body =~ /password/i ? 100 : 40,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: {
|
||||
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/finders/interesting_findings/full_path_disclosure.rb
Normal file
23
app/finders/interesting_findings/full_path_disclosure.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Full Path Disclosure finder
|
||||
class FullPathDisclosure < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-includes/rss-functions.php'
|
||||
fpd_entries = target.full_path_disclosure_entries(path)
|
||||
|
||||
return if fpd_entries.empty?
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: fpd_entries
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/finders/interesting_findings/mu_plugins.rb
Normal file
49
app/finders/interesting_findings/mu_plugins.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Must Use Plugins Directory checker
|
||||
class MuPlugins < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def passive(_opts = {})
|
||||
pattern = %r{#{target.content_dir}/mu\-plugins/}i
|
||||
|
||||
target.in_scope_urls(target.homepage_res) do |url|
|
||||
next unless Addressable::URI.parse(url).path =~ pattern
|
||||
|
||||
url = target.url('wp-content/mu-plugins/')
|
||||
|
||||
return WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 70,
|
||||
found_by: 'URLs In Homepage (Passive Detection)',
|
||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
||||
)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('wp-content/mu-plugins/')
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
return unless [200, 401, 403].include?(res.code)
|
||||
return if target.homepage_or_404?(res)
|
||||
|
||||
# TODO: add the check for --exclude-content once implemented ?
|
||||
|
||||
target.mu_plugins = true
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 80,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/interesting_findings/multisite.rb
Normal file
29
app/finders/interesting_findings/multisite.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Multisite checker
|
||||
class Multisite < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('wp-signup.php')
|
||||
res = Browser.get(url)
|
||||
location = res.headers_hash['location']
|
||||
|
||||
return unless [200, 302].include?(res.code)
|
||||
return if res.code == 302 && location =~ /wp-login\.php\?action=register/
|
||||
return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
|
||||
|
||||
target.multisite = true
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: 'This site seems to be a multisite',
|
||||
references: { url: 'http://codex.wordpress.org/Glossary#Multisite' }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
app/finders/interesting_findings/readme.rb
Normal file
26
app/finders/interesting_findings/readme.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Readme.html finder
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
potential_files.each do |file|
|
||||
url = target.url(file)
|
||||
res = Browser.get(url)
|
||||
|
||||
if res.code == 200 && res.body =~ /wordpress/i
|
||||
return WPScan::InterestingFinding.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @retun [ Array<String> ] The list of potential readme files
|
||||
def potential_files
|
||||
%w[readme.html olvasdel.html lisenssi.html liesmich.html]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/finders/interesting_findings/registration.rb
Normal file
31
app/finders/interesting_findings/registration.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Registration Enabled checker
|
||||
class Registration < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def passive(_opts = {})
|
||||
# Maybe check in the homepage if there is the registration url ?
|
||||
end
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
res = Browser.get_and_follow_location(target.registration_url)
|
||||
|
||||
return unless res.code == 200
|
||||
return if res.html.css('form#setupform').empty? &&
|
||||
res.html.css('form#registerform').empty?
|
||||
|
||||
target.registration_enabled = true
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
res.effective_url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "Registration is enabled: #{res.effective_url}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/finders/interesting_findings/tmm_db_migrate.rb
Normal file
24
app/finders/interesting_findings/tmm_db_migrate.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# Tmm DB Migrate finder
|
||||
class TmmDbMigrate < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
|
||||
url = target.url(path)
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { packetstorm: 131_957 }
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/finders/interesting_findings/upload_directory_listing.rb
Normal file
24
app/finders/interesting_findings/upload_directory_listing.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# UploadDirectoryListing finder
|
||||
class UploadDirectoryListing < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/uploads/'
|
||||
|
||||
return unless target.directory_listing?(path)
|
||||
|
||||
url = target.url(path)
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "Upload directory has listing enabled: #{url}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/finders/interesting_findings/upload_sql_dump.rb
Normal file
28
app/finders/interesting_findings/upload_sql_dump.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# UploadSQLDump finder
|
||||
class UploadSQLDump < CMSScanner::Finders::Finder
|
||||
SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = dump_url
|
||||
res = Browser.get(url)
|
||||
|
||||
return unless res.code == 200 && res.body =~ SQL_PATTERN
|
||||
|
||||
WPScan::InterestingFinding.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
|
||||
def dump_url
|
||||
target.url('wp-content/uploads/dump.sql')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/finders/main_theme.rb
Normal file
22
app/finders/main_theme.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require_relative 'main_theme/css_style'
|
||||
require_relative 'main_theme/woo_framework_meta_generator'
|
||||
require_relative 'main_theme/urls_in_homepage'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# Main Theme Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
MainTheme::CssStyle.new(target) <<
|
||||
MainTheme::WooFrameworkMetaGenerator.new(target) <<
|
||||
MainTheme::UrlsInHomepage.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/finders/main_theme/css_style.rb
Normal file
43
app/finders/main_theme/css_style.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# From the css style
|
||||
class CssStyle < CMSScanner::Finders::Finder
|
||||
include Finders::WpItems::URLsInHomepage
|
||||
|
||||
def create_theme(slug, style_url, opts)
|
||||
WPScan::Theme.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)
|
||||
)
|
||||
end
|
||||
|
||||
def passive(opts = {})
|
||||
passive_from_css_href(target.homepage_res, opts) || passive_from_style_code(target.homepage_res, opts)
|
||||
end
|
||||
|
||||
def passive_from_css_href(res, opts)
|
||||
target.in_scope_urls(res, '//style/@src|//link/@href') do |url|
|
||||
next unless Addressable::URI.parse(url).path =~ %r{/themes/([^\/]+)/style.css\z}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], url, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def passive_from_style_code(res, opts)
|
||||
res.html.css('style').each do |tag|
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/main_theme/urls_in_homepage.rb
Normal file
25
app/finders/main_theme/urls_in_homepage.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# URLs In Homepage Finder
|
||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||
include WpItems::URLsInHomepage
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
slugs = items_from_links('themes', false) + items_from_codes('themes', false)
|
||||
|
||||
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/finders/main_theme/woo_framework_meta_generator.rb
Normal file
22
app/finders/main_theme/woo_framework_meta_generator.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
# From the WooFramework meta generators
|
||||
class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
|
||||
THEME_PATTERN = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?"\s+/?>}
|
||||
FRAMEWORK_PATTERN = %r{<meta name="generator" content="WooFramework\s?([^"]+)?"\s+/?>}
|
||||
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i
|
||||
|
||||
def passive(opts = {})
|
||||
return unless target.homepage_res.body =~ PATTERN
|
||||
|
||||
WPScan::Theme.new(
|
||||
Regexp.last_match[1],
|
||||
target,
|
||||
opts.merge(found_by: found_by, confidence: 80)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/medias.rb
Normal file
17
app/finders/medias.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'medias/attachment_brute_forcing'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Medias
|
||||
# Medias Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << Medias::AttachmentBruteForcing.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
44
app/finders/medias/attachment_brute_forcing.rb
Normal file
44
app/finders/medias/attachment_brute_forcing.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Medias
|
||||
# Medias Finder
|
||||
class AttachmentBruteForcing < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range Mandatory
|
||||
#
|
||||
# @return [ Array<Media> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res|
|
||||
next unless res.code == 200
|
||||
|
||||
found << WPScan::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range Mandatory
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
opts[:range].each do |id|
|
||||
urls[target.uri.join("?attachment_id=#{id}").to_s] = id
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Brute Forcing Attachment IDs -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
3
app/finders/passwords.rb
Normal file
3
app/finders/passwords.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
require_relative 'passwords/wp_login'
|
||||
require_relative 'passwords/xml_rpc'
|
||||
require_relative 'passwords/xml_rpc_multicall'
|
||||
22
app/finders/passwords/wp_login.rb
Normal file
22
app/finders/passwords/wp_login.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
# Password attack against the wp-login.php
|
||||
class WpLogin < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||
|
||||
def login_request(username, password)
|
||||
target.login_request(username, password)
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 302
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
response.code != 200 && response.body !~ /login_error/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
22
app/finders/passwords/xml_rpc.rb
Normal file
22
app/finders/passwords/xml_rpc.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
# Password attack against the XMLRPC interface
|
||||
class XMLRPC < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||
|
||||
def login_request(username, password)
|
||||
target.method_call('wp.getUsersBlogs', [username, password])
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 200 && response.body =~ /blogName/
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
response.code != 200 && response.body !~ /login_error/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
102
app/finders/passwords/xml_rpc_multicall.rb
Normal file
102
app/finders/passwords/xml_rpc_multicall.rb
Normal file
@@ -0,0 +1,102 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
# Password attack against the XMLRPC interface with the multicall method
|
||||
# WP < 4.4 is vulnerable to such attack
|
||||
class XMLRPCMulticall < CMSScanner::Finders::Finder
|
||||
# @param [ Array<User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def do_multi_call(users, passwords)
|
||||
methods = []
|
||||
|
||||
users.each do |user|
|
||||
passwords.each do |password|
|
||||
methods << ['wp.getUsersBlogs', user.username, password]
|
||||
end
|
||||
end
|
||||
|
||||
target.multi_call(methods).run
|
||||
end
|
||||
|
||||
# @param [ Array<CMSScanner::User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
# @option opts [ Integer ] :multicall_max_passwords
|
||||
#
|
||||
# @yield [ CMSScanner::User ] When a valid combination is found
|
||||
#
|
||||
# TODO: Make rubocop happy about metrics etc
|
||||
#
|
||||
# rubocop:disable all
|
||||
def attack(users, passwords, opts = {})
|
||||
wordlist_index = 0
|
||||
max_passwords = opts[:multicall_max_passwords]
|
||||
current_passwords_size = passwords_size(max_passwords, users.size)
|
||||
|
||||
create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
|
||||
show_progression: opts[:show_progression])
|
||||
|
||||
loop do
|
||||
current_users = users.select { |user| user.password.nil? }
|
||||
current_passwords = passwords[wordlist_index, current_passwords_size]
|
||||
wordlist_index += current_passwords_size
|
||||
|
||||
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
||||
|
||||
res = do_multi_call(current_users, current_passwords)
|
||||
|
||||
progress_bar.increment
|
||||
|
||||
check_and_output_errors(res)
|
||||
|
||||
# Avoid to parse the response and iterate over all the structs in the document
|
||||
# if there isn't any tag matching a valid combination
|
||||
next unless res.body =~ /isAdmin/ # maybe a better one ?
|
||||
|
||||
Nokogiri::XML(res.body).xpath('//struct').each_with_index do |struct, index|
|
||||
next if struct.text =~ /faultCode/
|
||||
|
||||
user = current_users[index / current_passwords.size]
|
||||
user.password = current_passwords[index % current_passwords.size]
|
||||
|
||||
yield user
|
||||
|
||||
# Updates the current_passwords_size and progress_bar#total
|
||||
# given that less requests will be done due to a valid combination found.
|
||||
current_passwords_size = passwords_size(max_passwords, current_users.size - 1)
|
||||
|
||||
if current_passwords_size == 0
|
||||
progress_bar.log('All Found') # remove ?
|
||||
progress_bar.stop
|
||||
break
|
||||
end
|
||||
|
||||
progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
|
||||
end
|
||||
end
|
||||
# Maybe a progress_bar.stop ?
|
||||
end
|
||||
# rubocop:disable all
|
||||
|
||||
def passwords_size(max_passwords, users_size)
|
||||
return 1 if max_passwords < users_size
|
||||
return 0 if users_size == 0
|
||||
|
||||
max_passwords / users_size
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
def check_and_output_errors(res)
|
||||
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
||||
|
||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
|
||||
|
||||
progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/finders/plugin_version.rb
Normal file
38
app/finders/plugin_version.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
require_relative 'plugin_version/readme'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module PluginVersion
|
||||
# Plugin Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Plugin ] plugin
|
||||
def initialize(plugin)
|
||||
finders << PluginVersion::Readme.new(plugin)
|
||||
|
||||
load_specific_finders(plugin)
|
||||
end
|
||||
|
||||
# Load the finders associated with the plugin
|
||||
#
|
||||
# @param [ WPScan::Plugin ] plugin
|
||||
def load_specific_finders(plugin)
|
||||
module_name = plugin.classify
|
||||
|
||||
return unless Finders::PluginVersion.constants.include?(module_name)
|
||||
|
||||
mod = Finders::PluginVersion.const_get(module_name)
|
||||
|
||||
mod.constants.each do |constant|
|
||||
c = mod.const_get(constant)
|
||||
|
||||
next unless c.is_a?(Class)
|
||||
|
||||
finders << c.new(plugin)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
79
app/finders/plugin_version/readme.rb
Normal file
79
app/finders/plugin_version/readme.rb
Normal file
@@ -0,0 +1,79 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module PluginVersion
|
||||
# Plugin Version Finder from the readme.txt file
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ Version ]
|
||||
def aggressive(_opts = {})
|
||||
found_by_msg = 'Readme - %s (Aggressive Detection)'
|
||||
|
||||
WPScan::WpItem::READMES.each do |file|
|
||||
url = target.url(file)
|
||||
res = Browser.get(url)
|
||||
|
||||
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
|
||||
|
||||
return numbers.reduce([]) do |a, e|
|
||||
a << WPScan::Version.new(
|
||||
e[0],
|
||||
found_by: format(found_by_msg, e[1]),
|
||||
confidence: e[2],
|
||||
interesting_entries: [url]
|
||||
)
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Array<String, String, Integer> ] number, found_by, confidence
|
||||
def version_numbers(body)
|
||||
numbers = []
|
||||
|
||||
if (number = from_stable_tag(body))
|
||||
numbers << [number, 'Stable Tag', 80]
|
||||
end
|
||||
|
||||
if (number = from_changelog_section(body))
|
||||
numbers << [number, 'ChangeLog Section', 50]
|
||||
end
|
||||
|
||||
numbers
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String, nil ] The version number detected from the stable tag
|
||||
def from_stable_tag(body)
|
||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
|
||||
|
||||
number = Regexp.last_match[1]
|
||||
|
||||
number if number =~ /[0-9]+/
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String, nil ] The best version number detected from the changelog section
|
||||
def from_changelog_section(body)
|
||||
extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
|
||||
|
||||
return if extracted_versions.nil? || extracted_versions.empty?
|
||||
|
||||
extracted_versions.flatten!
|
||||
# must contain at least one number
|
||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||
|
||||
sorted = extracted_versions.sort do |x, y|
|
||||
begin
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
sorted.last
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
33
app/finders/plugins.rb
Normal file
33
app/finders/plugins.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
require_relative 'plugins/urls_in_homepage'
|
||||
require_relative 'plugins/known_locations'
|
||||
# From the DynamicFinders
|
||||
require_relative 'plugins/comment'
|
||||
require_relative 'plugins/xpath'
|
||||
require_relative 'plugins/header_pattern'
|
||||
require_relative 'plugins/body_pattern'
|
||||
require_relative 'plugins/javascript_var'
|
||||
require_relative 'plugins/query_parameter'
|
||||
require_relative 'plugins/config_parser' # Not loaded below as not implemented
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
Plugins::UrlsInHomepage.new(target) <<
|
||||
Plugins::HeaderPattern.new(target) <<
|
||||
Plugins::Comment.new(target) <<
|
||||
Plugins::Xpath.new(target) <<
|
||||
Plugins::BodyPattern.new(target) <<
|
||||
Plugins::JavascriptVar.new(target) <<
|
||||
Plugins::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
27
app/finders/plugins/body_pattern.rb
Normal file
27
app/finders/plugins/body_pattern.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'BodyPattern'
|
||||
class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
return unless response.body =~ config['pattern']
|
||||
|
||||
Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/finders/plugins/comment.rb
Normal file
31
app/finders/plugins/comment.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'Comment'
|
||||
class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
||||
comment = node.text.to_s.strip
|
||||
|
||||
next unless comment =~ config['pattern']
|
||||
|
||||
return Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/finders/plugins/config_parser.rb
Normal file
31
app/finders/plugins/config_parser.rb
Normal file
@@ -0,0 +1,31 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'ConfigParser'
|
||||
class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 40
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def _process_response(_opts, _response, slug, klass, config)
|
||||
#
|
||||
# TODO. Currently not implemented, and not even loaded by the Finders, as this
|
||||
# finder only has an aggressive method, which has been disabled (globally)
|
||||
# when checking for plugins
|
||||
#
|
||||
|
||||
Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/finders/plugins/header_pattern.rb
Normal file
41
app/finders/plugins/header_pattern.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'HeaderPattern'
|
||||
class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
headers = target.homepage_res.headers
|
||||
|
||||
return found if headers.empty?
|
||||
|
||||
DB::DynamicFinders::Plugin.passive_header_pattern_finder_configs.each do |slug, configs|
|
||||
configs.each do |klass, config|
|
||||
next unless headers[config['header']] && headers[config['header']].to_s =~ config['pattern']
|
||||
|
||||
found << Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ nil ]
|
||||
def aggressive(_opts = {})
|
||||
# None
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/plugins/javascript_var.rb
Normal file
29
app/finders/plugins/javascript_var.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'JavascriptVar'
|
||||
class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 60
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
response.html.xpath(config['xpath'] || '//script[not(@src)]').each do |node|
|
||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/finders/plugins/known_locations.rb
Normal file
48
app/finders/plugins/known_locations.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Known Locations Plugins Finder
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, slug|
|
||||
# TODO: follow the location (from enumerate()) and remove the 301 here ?
|
||||
# As a result, it might remove false positive due to redirection to the homepage
|
||||
next unless [200, 401, 403, 301].include?(res.code)
|
||||
|
||||
found << WPScan::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
slugs = opts[:list] || DB::Plugins.vulnerable_slugs
|
||||
urls = {}
|
||||
plugins_url = target.plugins_url
|
||||
|
||||
slugs.each do |slug|
|
||||
urls["#{plugins_url}#{URI.encode(slug)}/"] = slug
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Known Locations -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/plugins/query_parameter.rb
Normal file
25
app/finders/plugins/query_parameter.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'QueryParameter'
|
||||
class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 10
|
||||
|
||||
def passive(_opts = {})
|
||||
# Handled by UrlsInHomePage, so no need to check this twice
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
# TODO: when a real case will be found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/finders/plugins/urls_in_homepage.rb
Normal file
25
app/finders/plugins/urls_in_homepage.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# URLs In Homepage Finder
|
||||
# Typically, the items detected from URLs like
|
||||
# /wp-content/plugins/<slug>/
|
||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||
include WpItems::URLsInHomepage
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Plugin> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
(items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |slug|
|
||||
found << Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/plugins/xpath.rb
Normal file
29
app/finders/plugins/xpath.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'Xpath'
|
||||
class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 40
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] slug
|
||||
# @param [ String ] klass
|
||||
# @param [ Hash ] config The related dynamic finder config hash
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
response.html.xpath(config['xpath']).each do |node|
|
||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
app/finders/theme_version.rb
Normal file
41
app/finders/theme_version.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
require_relative 'theme_version/style'
|
||||
require_relative 'theme_version/woo_framework_meta_generator'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
# Theme Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Theme ] theme
|
||||
def initialize(theme)
|
||||
finders <<
|
||||
ThemeVersion::Style.new(theme) <<
|
||||
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
|
||||
|
||||
load_specific_finders(theme)
|
||||
end
|
||||
|
||||
# Load the finders associated with the theme
|
||||
#
|
||||
# @param [ WPScan::Theme ] theme
|
||||
def load_specific_finders(theme)
|
||||
module_name = theme.classify
|
||||
|
||||
return unless Finders::ThemeVersion.constants.include?(module_name)
|
||||
|
||||
mod = Finders::ThemeVersion.const_get(module_name)
|
||||
|
||||
mod.constants.each do |constant|
|
||||
c = mod.const_get(constant)
|
||||
|
||||
next unless c.is_a?(Class)
|
||||
|
||||
finders << c.new(theme)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/finders/theme_version/style.rb
Normal file
43
app/finders/theme_version/style.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
# Theme Version Finder from the style.css file
|
||||
class Style < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Version ]
|
||||
def passive(_opts = {})
|
||||
return unless cached_style?
|
||||
|
||||
style_version
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Version ]
|
||||
def aggressive(_opts = {})
|
||||
return if cached_style?
|
||||
|
||||
style_version
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def cached_style?
|
||||
Typhoeus::Config.cache.get(browser.forge_request(target.style_url)) ? true : false
|
||||
end
|
||||
|
||||
# @return [ Version ]
|
||||
def style_version
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
|
||||
|
||||
WPScan::Version.new(
|
||||
Regexp.last_match[1],
|
||||
found_by: found_by,
|
||||
confidence: 80,
|
||||
interesting_entries: ["#{target.style_url}, Match: '#{Regexp.last_match}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
19
app/finders/theme_version/woo_framework_meta_generator.rb
Normal file
19
app/finders/theme_version/woo_framework_meta_generator.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
# Theme Version Finder from the WooFramework generators
|
||||
class WooFrameworkMetaGenerator < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Version ]
|
||||
def passive(_opts = {})
|
||||
return unless target.blog.homepage_res.body =~ Finders::MainTheme::WooFrameworkMetaGenerator::PATTERN
|
||||
|
||||
return unless Regexp.last_match[1] == target.slug
|
||||
|
||||
WPScan::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
20
app/finders/themes.rb
Normal file
20
app/finders/themes.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
require_relative 'themes/urls_in_homepage'
|
||||
require_relative 'themes/known_locations'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
# themes Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
Themes::UrlsInHomepage.new(target) <<
|
||||
Themes::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
48
app/finders/themes/known_locations.rb
Normal file
48
app/finders/themes/known_locations.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
# Known Locations Themes Finder
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, slug|
|
||||
# TODO: follow the location (from enumerate()) and remove the 301 here ?
|
||||
# As a result, it might remove false positive due to redirection to the homepage
|
||||
next unless [200, 401, 403, 301].include?(res.code)
|
||||
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
slugs = opts[:list] || DB::Themes.vulnerable_slugs
|
||||
urls = {}
|
||||
themes_url = target.url('wp-content/themes/')
|
||||
|
||||
slugs.each do |slug|
|
||||
urls["#{themes_url}#{URI.encode(slug)}/"] = slug
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Known Locations -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/finders/themes/urls_in_homepage.rb
Normal file
23
app/finders/themes/urls_in_homepage.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
# URLs In Homepage Finder
|
||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||
include WpItems::URLsInHomepage
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Theme> ]
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
(items_from_links('themes') + items_from_codes('themes')).uniq.sort.each do |slug|
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/timthumb_version.rb
Normal file
17
app/finders/timthumb_version.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'timthumb_version/bad_request'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module TimthumbVersion
|
||||
# Timthumb Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Timthumb ] target
|
||||
def initialize(target)
|
||||
finders << TimthumbVersion::BadRequest.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
21
app/finders/timthumb_version/bad_request.rb
Normal file
21
app/finders/timthumb_version/bad_request.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module TimthumbVersion
|
||||
# Timthumb Version Finder from the body of a bad request
|
||||
# See https://code.google.com/p/timthumb/source/browse/trunk/timthumb.php#435
|
||||
class BadRequest < CMSScanner::Finders::Finder
|
||||
# @return [ Version ]
|
||||
def aggressive(_opts = {})
|
||||
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
|
||||
|
||||
WPScan::Version.new(
|
||||
Regexp.last_match[2],
|
||||
found_by: 'Bad Request (Aggressive Detection)',
|
||||
confidence: 90,
|
||||
interesting_entries: ["#{target.url}, Match: '#{Regexp.last_match[1]}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
17
app/finders/timthumbs.rb
Normal file
17
app/finders/timthumbs.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require_relative 'timthumbs/known_locations'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Timthumbs
|
||||
# Timthumbs Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders << Timthumbs::KnownLocations.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
56
app/finders/timthumbs/known_locations.rb
Normal file
56
app/finders/timthumbs/known_locations.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Timthumbs
|
||||
# Known Locations Timthumbs Finder
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Array<Timthumb> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res|
|
||||
next unless res.code == 400 && res.body =~ /no image specified/i
|
||||
|
||||
found << WPScan::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
File.open(opts[:list]).each_with_index do |path, index|
|
||||
urls[target.url(path.chomp)] = index
|
||||
end
|
||||
|
||||
# Add potential timthumbs located in the main theme
|
||||
if target.main_theme
|
||||
main_theme_timthumbs_paths.each do |path|
|
||||
urls[target.main_theme.url(path)] = 1 # index not important there
|
||||
end
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def main_theme_timthumbs_paths
|
||||
%w[timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
|
||||
scripts/timthumb.php tools/timthumb.php functions/timthumb.php]
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking Known Locations -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
28
app/finders/users.rb
Normal file
28
app/finders/users.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
require_relative 'users/author_posts'
|
||||
require_relative 'users/wp_json_api'
|
||||
require_relative 'users/oembed_api'
|
||||
require_relative 'users/rss_generator'
|
||||
require_relative 'users/author_id_brute_forcing'
|
||||
require_relative 'users/login_error_messages'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Users Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::SameTypeFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
finders <<
|
||||
Users::AuthorPosts.new(target) <<
|
||||
Users::WpJsonApi.new(target) <<
|
||||
Users::OembedApi.new(target) <<
|
||||
Users::RSSGenerator.new(target) <<
|
||||
Users::AuthorIdBruteForcing.new(target) <<
|
||||
Users::LoginErrorMessages.new(target)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
111
app/finders/users/author_id_brute_forcing.rb
Normal file
111
app/finders/users/author_id_brute_forcing.rb
Normal file
@@ -0,0 +1,111 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Author Id Brute Forcing
|
||||
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range Mandatory
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
found_by_msg = 'Author Id Brute Forcing - %s (Aggressive Detection)'
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, id|
|
||||
username, found_by, confidence = potential_username(res)
|
||||
|
||||
next unless username
|
||||
|
||||
found << CMSScanner::User.new(
|
||||
username,
|
||||
id: id,
|
||||
found_by: format(found_by_msg, found_by),
|
||||
confidence: confidence
|
||||
)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
urls = {}
|
||||
|
||||
opts[:range].each do |id|
|
||||
urls[target.uri.join("?author=#{id}").to_s] = id
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Brute Forcing Author IDs -'))
|
||||
end
|
||||
|
||||
def request_params
|
||||
{ followlocation: true }
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
#
|
||||
# @return [ Array<String, String, Integer>, nil ] username, found_by, confidence
|
||||
def potential_username(res)
|
||||
username = username_from_author_url(res.effective_url) || username_from_response(res)
|
||||
|
||||
return username, 'Author Pattern', 100 if username
|
||||
|
||||
username = display_name_from_body(res.body)
|
||||
|
||||
return username, 'Display Name', 50 if username
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
#
|
||||
# @return [ String, nil ]
|
||||
def username_from_author_url(url)
|
||||
url[%r{/author/([^/\b]+)/?}i, 1]
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
#
|
||||
# @return [ String, nil ] The username found
|
||||
def username_from_response(res)
|
||||
# Permalink enabled
|
||||
target.in_scope_urls(res, '//link/@href|//a/@href') do |url|
|
||||
username = username_from_author_url(url)
|
||||
return username if username
|
||||
end
|
||||
|
||||
# No permalink
|
||||
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
#
|
||||
# @return [ String, nil ]
|
||||
def display_name_from_body(body)
|
||||
page = Nokogiri::HTML.parse(body)
|
||||
# WP >= 3.0
|
||||
page.css('h1.page-title span').each do |node|
|
||||
return node.text.to_s
|
||||
end
|
||||
|
||||
# WP < 3.0
|
||||
page.xpath('//link[@rel="alternate" and @type="application/rss+xml"]').each do |node|
|
||||
title = node['title']
|
||||
|
||||
next unless title =~ /Posts by (.*) Feed\z/i
|
||||
|
||||
return Regexp.last_match[1] unless Regexp.last_match[1].empty?
|
||||
end
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
app/finders/users/author_posts.rb
Normal file
61
app/finders/users/author_posts.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Author Posts
|
||||
class AuthorPosts < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def passive(opts = {})
|
||||
found_by_msg = 'Author Posts - %s (Passive Detection)'
|
||||
|
||||
usernames(opts).reduce([]) do |a, e|
|
||||
a << CMSScanner::User.new(
|
||||
e[0],
|
||||
found_by: format(found_by_msg, e[1]),
|
||||
confidence: e[2]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<Array>> ]
|
||||
def usernames(_opts = {})
|
||||
found = potential_usernames(target.homepage_res)
|
||||
|
||||
return found unless found.empty?
|
||||
|
||||
target.homepage_res.html.css('header.entry-header a').each do |post_url_node|
|
||||
url = post_url_node['href']
|
||||
|
||||
next if url.nil? || url.empty?
|
||||
|
||||
found += potential_usernames(Browser.get(url))
|
||||
end
|
||||
|
||||
found.compact.uniq
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
#
|
||||
# @return [ Array<Array> ]
|
||||
def potential_usernames(res)
|
||||
usernames = []
|
||||
|
||||
target.in_scope_urls(res, '//a/@href') do |url, node|
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
||||
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
||||
elsif uri.query =~ /author=[0-9]+/
|
||||
usernames << [node.text.to_s.strip, 'Display Name', 30]
|
||||
end
|
||||
end
|
||||
|
||||
usernames.uniq
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
45
app/finders/users/login_error_messages.rb
Normal file
45
app/finders/users/login_error_messages.rb
Normal file
@@ -0,0 +1,45 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Login Error Messages
|
||||
#
|
||||
# Existing username:
|
||||
# WP < 3.1 - Incorrect password.
|
||||
# WP >= 3.1 - The password you entered for the username admin is incorrect.
|
||||
# Non existent username: Invalid username.
|
||||
#
|
||||
class LoginErrorMessages < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
usernames(opts).each do |username|
|
||||
res = target.do_login(username, SecureRandom.hex[0, 8])
|
||||
error = res.html.css('div#login_error').text.strip
|
||||
|
||||
return found if error.empty? # Protection plugin / error disabled
|
||||
|
||||
next unless error =~ /The password you entered for the username|Incorrect Password/i
|
||||
|
||||
found << CMSScanner::User.new(username, found_by: found_by, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] List of usernames to check
|
||||
def usernames(opts = {})
|
||||
# usernames from the potential Users found
|
||||
unames = opts[:found].map(&:username)
|
||||
|
||||
[*opts[:list]].each { |uname| unames << uname.chomp }
|
||||
|
||||
unames.uniq
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
49
app/finders/users/oembed_api.rb
Normal file
49
app/finders/users/oembed_api.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Since WP 4.4, the oembed API can disclose a user
|
||||
# https://github.com/wpscanteam/wpscan/issues/1049
|
||||
class OembedApi < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def passive(_opts = {})
|
||||
# TODO: get the api_url from the Homepage and query it if present,
|
||||
# then discard the aggressive check if same/similar URL
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# TODO: make this code pretty :x
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
found_by_msg = 'Oembed API - %s (Aggressive Detection)'
|
||||
|
||||
oembed_data = JSON.parse(Browser.get(api_url).body)
|
||||
|
||||
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
|
||||
details = [Regexp.last_match[1], 'Author URL', 90]
|
||||
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
||||
details = [oembed_data['author_name'].delete(' '), 'Author Name', 70]
|
||||
end
|
||||
|
||||
return unless details
|
||||
|
||||
found << CMSScanner::User.new(details[0],
|
||||
found_by: format(found_by_msg, details[1]),
|
||||
confidence: details[2],
|
||||
interesting_entries: [api_url])
|
||||
rescue JSON::ParserError
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ] The URL of the API listing the Users
|
||||
def api_url
|
||||
@api_url ||= target.url("wp-json/oembed/1.0/embed?url=#{target.url}&format=json")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/finders/users/rss_generator.rb
Normal file
38
app/finders/users/rss_generator.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Users disclosed from the dc:creator field in the RSS
|
||||
# The names disclosed are display names, however depending on the configuration of the blog,
|
||||
# they can be the same than usernames
|
||||
class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
|
||||
def process_urls(urls, _opts = {})
|
||||
found = []
|
||||
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
next unless res.code == 200 && res.body =~ /<dc\:creator>/i
|
||||
|
||||
potential_usernames = []
|
||||
|
||||
begin
|
||||
res.xml.xpath('//item/dc:creator').each do |node|
|
||||
potential_usernames << node.text.to_s unless node.text.to_s.length > 40
|
||||
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)
|
||||
end
|
||||
|
||||
break
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/finders/users/wp_json_api.rb
Normal file
35
app/finders/users/wp_json_api.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# WP JSON API
|
||||
#
|
||||
# Since 4.7 - Need more investigation as it seems WP 4.7.1 reduces the exposure, see https://github.com/wpscanteam/wpscan/issues/1038)
|
||||
#
|
||||
class WpJsonApi < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
|
||||
JSON.parse(Browser.get(api_url).body)&.each do |user|
|
||||
found << CMSScanner::User.new(user['slug'],
|
||||
id: user['id'],
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [api_url])
|
||||
end
|
||||
|
||||
found
|
||||
rescue JSON::ParserError, TypeError
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ] The URL of the API listing the Users
|
||||
def api_url
|
||||
@api_url ||= target.url('wp-json/wp/v2/users/')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
1
app/finders/wp_items.rb
Normal file
1
app/finders/wp_items.rb
Normal file
@@ -0,0 +1 @@
|
||||
require_relative 'wp_items/urls_in_homepage'
|
||||
68
app/finders/wp_items/urls_in_homepage.rb
Normal file
68
app/finders/wp_items/urls_in_homepage.rb
Normal file
@@ -0,0 +1,68 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpItems
|
||||
# URLs In Homepage Module to use in plugins & themes finders
|
||||
module URLsInHomepage
|
||||
# @param [ String ] type plugins / themes
|
||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||
#
|
||||
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
|
||||
def items_from_links(type, uniq = true)
|
||||
found = []
|
||||
|
||||
target.in_scope_urls(target.homepage_res) do |url|
|
||||
next unless url =~ item_attribute_pattern(type)
|
||||
|
||||
found << Regexp.last_match[1]
|
||||
end
|
||||
|
||||
uniq ? found.uniq.sort : found.sort
|
||||
end
|
||||
|
||||
# @param [ String ] type plugins / themes
|
||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||
#
|
||||
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
|
||||
def items_from_codes(type, uniq = true)
|
||||
found = []
|
||||
|
||||
target.homepage_res.html.css('script,style').each do |tag|
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
code.scan(item_code_pattern(type)).flatten.uniq.each { |slug| found << slug }
|
||||
end
|
||||
|
||||
uniq ? found.uniq.sort : found.sort
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_attribute_pattern(type)
|
||||
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_code_pattern(type)
|
||||
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_url_pattern(type)
|
||||
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||
|
||||
url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
42
app/finders/wp_version.rb
Normal file
42
app/finders/wp_version.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
require_relative 'wp_version/rss_generator'
|
||||
require_relative 'wp_version/atom_generator'
|
||||
require_relative 'wp_version/rdf_generator'
|
||||
require_relative 'wp_version/readme'
|
||||
require_relative 'wp_version/unique_fingerprinting'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
# Specific Finders container to filter the version detected
|
||||
# and remove the one with low confidence to avoid false
|
||||
# positive when there is not enought information to accurately
|
||||
# determine it.
|
||||
class WpVersionFinders < UniqueFinders
|
||||
def filter_findings
|
||||
best_finding = super
|
||||
|
||||
best_finding && best_finding.confidence >= 40 ? best_finding : false
|
||||
end
|
||||
end
|
||||
|
||||
module WpVersion
|
||||
# Wp Version Finder
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
(%w[RSSGenerator AtomGenerator RDFGenerator] +
|
||||
WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
|
||||
%w[Readme UniqueFingerprinting]
|
||||
).each do |finder_name|
|
||||
finders << WpVersion.const_get(finder_name.to_sym).new(target)
|
||||
end
|
||||
end
|
||||
|
||||
def finders
|
||||
@finders ||= Finders::WpVersionFinders.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
40
app/finders/wp_version/atom_generator.rb
Normal file
40
app/finders/wp_version/atom_generator.rb
Normal file
@@ -0,0 +1,40 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# Atom Generator Version Finder
|
||||
class AtomGenerator < CMSScanner::Finders::Finder
|
||||
include Finder::WpVersion::SmartURLChecker
|
||||
|
||||
def process_urls(urls, _opts = {})
|
||||
found = Findings.new
|
||||
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
res.html.css('generator').each do |node|
|
||||
next unless node.text.to_s.strip.casecmp('wordpress').zero?
|
||||
|
||||
found << create_version(
|
||||
node['version'],
|
||||
found_by: found_by,
|
||||
entries: ["#{res.effective_url}, #{node.to_s.strip}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//link[@rel="alternate" and @type="application/atom+xml"]/@href'
|
||||
end
|
||||
|
||||
def aggressive_urls(_opts = {})
|
||||
%w[feed/atom/ ?feed=atom].reduce([]) do |a, uri|
|
||||
a << target.url(uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
38
app/finders/wp_version/rdf_generator.rb
Normal file
38
app/finders/wp_version/rdf_generator.rb
Normal file
@@ -0,0 +1,38 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# RDF Generator Version Finder
|
||||
class RDFGenerator < CMSScanner::Finders::Finder
|
||||
include Finder::WpVersion::SmartURLChecker
|
||||
|
||||
def process_urls(urls, _opts = {})
|
||||
found = Findings.new
|
||||
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
res.html.xpath('//generatoragent').each do |node|
|
||||
next unless node['rdf:resource'] =~ %r{\Ahttps?://wordpress\.(?:[a-z.]+)/\?v=(.*)\z}i
|
||||
|
||||
found << create_version(
|
||||
Regexp.last_match[1],
|
||||
found_by: found_by,
|
||||
entries: ["#{res.effective_url}, #{node.to_s.strip}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//a[contains(@href, "rdf")]/@href'
|
||||
end
|
||||
|
||||
def aggressive_urls(_opts = {})
|
||||
[target.url('feed/rdf/')]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
29
app/finders/wp_version/readme.rb
Normal file
29
app/finders/wp_version/readme.rb
Normal file
@@ -0,0 +1,29 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# Readme Version Finder
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ WpVersion ]
|
||||
def aggressive(_opts = {})
|
||||
readme_url = target.url('readme.html') # Maybe move this into the Target ?
|
||||
|
||||
node = Browser.get(readme_url).html.css('h1#logo').last
|
||||
|
||||
return unless node&.text.to_s.strip =~ /\AVersion (.*)\z/i
|
||||
|
||||
number = Regexp.last_match(1)
|
||||
|
||||
return unless WPScan::WpVersion.valid?(number)
|
||||
|
||||
WPScan::WpVersion.new(
|
||||
number,
|
||||
found_by: 'Readme (Aggressive Detection)',
|
||||
# Since WP 4.7, the Readme only contains the major version (ie 4.7, 4.8 etc)
|
||||
confidence: number >= '4.7' ? 10 : 90,
|
||||
interesting_entries: ["#{readme_url}, Match: '#{node.text.to_s.strip}'"]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
app/finders/wp_version/rss_generator.rb
Normal file
43
app/finders/wp_version/rss_generator.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# RSS Generator Version Finder
|
||||
class RSSGenerator < CMSScanner::Finders::Finder
|
||||
include Finder::WpVersion::SmartURLChecker
|
||||
|
||||
def process_urls(urls, _opts = {})
|
||||
found = Findings.new
|
||||
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
res.html.xpath('//comment()[contains(., "wordpress")] | //generator').each do |node|
|
||||
node_text = node.text.to_s.strip
|
||||
|
||||
next unless node_text =~ %r{\Ahttps?://wordpress\.(?:[a-z]+)/\?v=(.*)\z}i ||
|
||||
node_text =~ %r{\Agenerator="wordpress/([^"]+)"\z}i
|
||||
|
||||
found << create_version(
|
||||
Regexp.last_match[1],
|
||||
found_by: found_by,
|
||||
entries: ["#{res.effective_url}, #{node.to_s.strip}"]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//link[@rel="alternate" and @type="application/rss+xml"]/@href'
|
||||
end
|
||||
|
||||
def aggressive_urls(_opts = {})
|
||||
%w[feed/ comments/feed/ feed/rss/ feed/rss2/].reduce([]) do |a, uri|
|
||||
a << target.url(uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
30
app/finders/wp_version/unique_fingerprinting.rb
Normal file
30
app/finders/wp_version/unique_fingerprinting.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
# Unique Fingerprinting Version Finder
|
||||
class UniqueFingerprinting < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Fingerprinter
|
||||
|
||||
# @return [ WpVersion ]
|
||||
def aggressive(opts = {})
|
||||
fingerprint(DB::Fingerprints.wp_unique_fingerprints, opts) do |version_number, url, md5sum|
|
||||
hydra.abort
|
||||
progress_bar.finish
|
||||
|
||||
return WPScan::WpVersion.new(
|
||||
version_number,
|
||||
found_by: 'Unique Fingerprinting (Aggressive Detection)',
|
||||
confidence: 100,
|
||||
interesting_entries: ["#{url} md5sum is #{md5sum}"]
|
||||
)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: 'Fingerprinting the version -'))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user