Compare commits

...

89 Commits

Author SHA1 Message Date
erwanlr
f3810a1504 Bumps version 2019-04-07 17:45:29 +01:00
erwanlr
4831760c11 Merge branch '3.5.1' 2019-04-07 17:42:51 +01:00
erwanlr
f375d8991e Update deps 2019-04-07 17:35:18 +01:00
erwanlr
8145a4a3a6 Fixes #1330 2019-04-07 17:06:19 +01:00
erwanlr
12c9b49d4c Adds DFs 2019-04-06 11:34:23 +01:00
erwanlr
c8eb81161e Uses https rather than git protocols for CMSScanner dep 2019-04-05 19:53:29 +01:00
erwanlr
8ab246a66c Uses CMSScanner git dep 2019-04-05 19:48:22 +01:00
erwanlr
8dfc4797fa Handles default user_agent_list via CLI option (in CMSScanner) 2019-04-05 19:30:53 +01:00
erwanlr
7888fe1176 Uses ParsedCli 2019-04-05 16:47:14 +01:00
Erwan
8a6f3056a3 Merge pull request #1329 from wpscanteam/dependabot/bundler/rubocop-tw-0.67.1
Update rubocop requirement from ~> 0.66.0 to ~> 0.67.1
2019-04-05 11:37:00 +02:00
dependabot[bot]
5fbdf9e013 Update rubocop requirement from ~> 0.66.0 to ~> 0.67.1
Updates the requirements on [rubocop](https://github.com/rubocop-hq/rubocop) to permit the latest version.
- [Release notes](https://github.com/rubocop-hq/rubocop/releases)
- [Changelog](https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop-hq/rubocop/compare/v0.66.0...v0.67.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-04-05 06:16:13 +00:00
erwanlr
1da2f5e823 Sets the Target#mu_plugind to true when detected passively 2019-04-04 17:25:58 +01:00
erwanlr
888779f81b Support of Ruby 2.3 removed as its life ended 2019-04-04 15:40:21 +01:00
erwanlr
352286e497 Adds a #maybe_add_cookies to handle website requiring a specific cookie 2019-04-03 19:08:52 +01:00
erwanlr
025ce37c05 Bumps version 2019-04-03 12:32:07 +01:00
erwanlr
d6c2c63679 Updates deps 2019-04-03 10:14:28 +01:00
erwanlr
49efbf25ea Adds detection of Plugin/Theme via errors 500 and custom 401/403 - Fixes #1090 2019-04-03 08:22:31 +01:00
erwanlr
02cdee2776 Retains db_data for Plugin, Theme and WpVersion models 2019-04-02 17:10:07 +01:00
erwanlr
7c9d4d5b05 Updates deps 2019-04-02 11:56:59 +01:00
erwanlr
609b7551f8 Forces GC to start only after Plugin/Theme DF generation 2019-04-02 09:47:16 +01:00
erwanlr
e8f215ae00 Forces the Garbage Collector to run after creating the DFs 2019-04-01 19:39:40 +01:00
erwanlr
2e00aea16e Mem tests 2019-04-01 12:19:40 +01:00
erwanlr
dd274d77f5 Updates deps 2019-04-01 11:15:02 +01:00
Erwan
58171a7b8c Fixes CodeClimate URL 2019-03-30 16:00:32 +01:00
erwanlr
8b05179401 Adds DFs 2019-03-30 14:17:09 +00:00
erwanlr
51d61a7e88 Adds DFs 2019-03-30 12:15:08 +00:00
erwanlr
d653ce4e0e Adds DFs 2019-03-30 11:11:27 +00:00
erwanlr
07b3826806 Adds DFs 2019-03-30 07:22:14 +00:00
erwanlr
1baa3e23b2 Fixes #1326 2019-03-29 08:27:18 +00:00
erwanlr
0aa1f20d47 Removes Changelog detection 2019-03-28 13:40:58 +00:00
erwanlr
1cf330b389 Merge branch 'master' into 3.5.0 2019-03-28 06:45:48 +00:00
erwanlr
1771c4b346 Updates Ruby version 2019-03-27 17:44:47 +00:00
erwanlr
4c053b4873 Updates dockerignore to ignore profiling executables as well 2019-03-27 14:25:58 +00:00
erwanlr
743ba0541b Updates finders to use new methods 2019-03-26 21:10:14 +00:00
erwanlr
cfab2a9cd7 Uses the new CMSScanner Enumerator module 2019-03-26 17:05:19 +00:00
erwanlr
32270efd65 Updates plugin version detection via Readme 2019-03-26 09:02:23 +00:00
erwanlr
7ea1acb7c1 Fixes non detection of plugin/theme readme and changelog files due to changes in CMSSCanner 2019-03-25 21:25:00 +00:00
erwanlr
bf91f60242 Uses the new Browser#forge_request method 2019-03-25 20:42:43 +00:00
Ryan Dewhurst
660885c0b1 Try to resolve weird char after readme conversion 2019-03-25 09:49:03 +01:00
erwanlr
15fd3b969f Uses head_and_get to check for Readme and Changelog locations 2019-03-24 22:01:19 +00:00
erwanlr
f1d15ca7f2 Updates spec for latest changes 2019-03-24 20:24:14 +00:00
erwanlr
6f4f4a5924 Typo 2019-03-24 20:15:43 +00:00
erwanlr
9af0520701 Delegates #head_and_get to #blog in WpItem models 2019-03-24 20:06:03 +00:00
erwanlr
2edeab558e Adds ruby frozen_string_literal comment to profiling bins 2019-03-24 19:57:59 +00:00
erwanlr
87bf59f50b Merge branch 'master' of github.com:wpscanteam/wpscan 2019-03-24 14:38:25 +00:00
erwanlr
eeb69e63f7 Adds DFs 2019-03-24 14:38:01 +00:00
erwanlr
f9435906e7 Merges with Master (and solves conflicts) 2019-03-24 13:01:29 +00:00
Ryan Dewhurst
6c8adbe50e Remove strange char when converted to html 2019-03-23 10:37:05 +01:00
Ryan Dewhurst
23bdb6c579 Open readme links in new tab 2019-03-23 10:14:51 +01:00
Ryan Dewhurst
264411bfb9 Update README.md 2019-03-23 10:00:50 +01:00
Ryan Dewhurst
2104237584 Update README.md 2019-03-23 09:57:50 +01:00
Ryan Dewhurst
0ae2525737 Update README.md 2019-03-23 09:57:33 +01:00
Ryan Dewhurst
b12973a837 Add projects links to the top of Readme 2019-03-23 09:41:14 +01:00
erwanlr
fa0582ce0b Uses head or get method to enumerate config backups 2019-03-22 20:35:22 +00:00
erwanlr
231f5157bf Fixes #1322 2019-03-22 20:20:07 +00:00
erwanlr
8b18204a69 Updates memory_profiler dep, revert changes to memory allocated commit (increased retained memory too much) 2019-03-22 06:56:10 +00:00
erwanlr
95eb6a732c Memprofiling - Increases the top to be displayed to 15 2019-03-21 20:50:57 +00:00
erwanlr
047a188b34 Uses the frozen_string_literal magic comment (will be the default in Ruby 3) 2019-03-21 17:41:29 +00:00
erwanlr
d407815c30 Adds comment about scale_bytes in memory_profiler 2019-03-21 16:54:06 +00:00
erwanlr
1f0f87633b Reduces memory allocation with creating DFs 2019-03-21 13:52:34 +00:00
erwanlr
c15ff4e32e Adds memprof binary - Ref #1321 2019-03-21 12:45:44 +00:00
erwanlr
72bddca314 Adds profiling binary for dev [WIP] - Ref #1321 2019-03-20 21:12:53 +00:00
erwanlr
496fc4ebee Typo 2019-03-20 20:12:18 +00:00
erwanlr
f414e6eeb7 Better code for WpVersion#all 2019-03-20 20:10:30 +00:00
erwanlr
f09606cfa3 Fixes #1319 2019-03-20 15:42:05 +00:00
erwanlr
6304fe4c19 Fixes #1318 2019-03-20 08:41:39 +00:00
erwanlr
5f2b8f8a2e Fixes #1317 2019-03-20 07:47:28 +00:00
erwanlr
898e8d4546 Moves Models into their own namespace - Ref #1315 2019-03-19 21:07:53 +00:00
erwanlr
f1657164d5 Errors moved into their own namespace - Ref #1315 2019-03-19 19:09:16 +00:00
erwanlr
357e13be2b Updates cms_scanner dep 2019-03-19 18:52:18 +00:00
erwanlr
9685568c75 Updates deps 2019-03-19 10:55:50 +00:00
erwanlr
b316940790 Merge branch 'enum-head' 2019-03-18 20:40:36 +00:00
erwanlr
2ced489e1e Updates deps 2019-03-18 20:37:24 +00:00
erwanlr
5969fe08d8 Revert changes related to the unexpected return - Ref #1314 2019-03-18 19:24:02 +00:00
erwanlr
4a427f1ff6 Adds a custom temporary Enumerator for Plugins,Themes and Timthumbs 2019-03-18 19:15:43 +00:00
erwanlr
9a3db275f3 Merge branch 'master' of github.com:wpscanteam/wpscan 2019-03-17 07:25:09 +00:00
erwanlr
475dd4d1ff Ref #1314 2019-03-17 07:24:49 +00:00
erwanlr
57c99c4a34 Fixes #1313 2019-03-17 06:59:44 +00:00
Christian Mehlmauer
966f5691a2 update image 2019-03-16 19:48:47 +01:00
erwanlr
5088ece8a1 Updates deps 2019-03-16 12:35:19 +00:00
erwanlr
943d87fe17 Updates deps 2019-03-16 09:31:01 +00:00
erwanlr
b5363b2689 Adds DFs 2019-03-16 08:38:07 +00:00
erwanlr
c15cb16ca8 Update deps 2019-03-15 14:09:31 +00:00
erwanlr
18b7f088fc Adds ruby versions to Travis 2019-03-15 12:47:06 +00:00
erwanlr
4f9822743c Improves Password Attack against wp-login.php to avoid FP 2019-03-14 19:21:39 +00:00
erwanlr
e7925de5bc Check the wp-login.php for potential redirection before using it 2019-03-14 18:06:32 +00:00
erwanlr
27fc6a7279 Updates cms_scanner dep 2019-03-14 11:55:20 +00:00
erwanlr
ab5f46e955 Adds detection of wp-content from raw JS 2019-03-14 09:14:55 +00:00
erwanlr
d30d212cc5 Updates WP DF (also check non minified file paths) - Ref #1311 2019-03-12 07:55:32 +00:00
998 changed files with 139182 additions and 927 deletions

View File

@@ -12,5 +12,5 @@ spec/
Dockerfile
**/*.orig
*.orig
bin/wpscan-docker*
bin/wpscan-*
.wpscan/

3
.gitignore vendored
View File

@@ -21,3 +21,6 @@ doc/
# Old files from v2
cache/
data/
# Profiling reports
bin/memprof*.report

View File

@@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 2.3
TargetRubyVersion: 2.4
Exclude:
- '*.gemspec'
- 'vendor/**/*'
@@ -22,7 +22,5 @@ Metrics/CyclomaticComplexity:
Max: 8
Style/Documentation:
Enabled: false
Style/FrozenStringLiteralComment:
Enabled: false
Style/FormatStringToken:
Enabled: false

View File

@@ -1 +1 @@
2.6.0
2.6.2

View File

@@ -2,25 +2,21 @@ language: ruby
sudo: false
cache: bundler
rvm:
- 2.3.0
- 2.3.1
- 2.3.2
- 2.3.3
- 2.3.4
- 2.3.5
- 2.3.6
- 2.3.7
- 2.3.8
- 2.4.1
- 2.4.2
- 2.4.3
- 2.4.4
- 2.4.5
- 2.4.6
- 2.5.0
- 2.5.1
- 2.5.2
- 2.5.3
- 2.5.4
- 2.5.5
- 2.6.0
- 2.6.1
- 2.6.2
- ruby-head
before_install:
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"

View File

@@ -1,4 +1,4 @@
FROM ruby:2.5.1-alpine AS builder
FROM ruby:2.6.2-alpine3.9 AS builder
LABEL maintainer="WPScan Team <team@wpscan.org>"
ARG BUNDLER_ARGS="--jobs=8 --without test development"
@@ -19,7 +19,7 @@ RUN rake install --trace
RUN chmod -R a+r /usr/local/bundle
FROM ruby:2.5-alpine
FROM ruby:2.6.2-alpine3.9
LABEL maintainer="WPScan Team <team@wpscan.org>"
RUN adduser -h /wpscan -g WPScan -D wpscan

View File

@@ -1,2 +1,6 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec
# gem 'cms_scanner', branch: 'xxx', git: 'https://github.com/wpscanteam/CMSScanner.git'

View File

@@ -1,9 +1,24 @@
![alt text](https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png "WPScan - WordPress Security Scanner")
<p align="center">
<a href="https://wpscan.org/">
<img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo">
</a>
</p>
[![Gem Version](https://badge.fury.io/rb/wpscan.svg)](https://badge.fury.io/rb/wpscan)
[![Build Status](https://travis-ci.org/wpscanteam/wpscan.svg?branch=master)](https://travis-ci.org/wpscanteam/wpscan)
[![Code Climate](https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg)](https://codeclimate.com/github/wpscanteam/wpscan)
[![Patreon Donate](https://img.shields.io/badge/patreon-donate-green.svg)](https://www.patreon.com/wpscan)
<h3 align="center">WPScan</h3>
<p align="center">
WordPress Vulnerability Scanner
<br>
<br>
<a href="https://wpscan.org/" title="homepage" target="_blank">Homepage</a> - <a href="https://wpscan.io/" title="wpscan.io" target="_blank">WPScan.io</a> - <a href="https://wpvulndb.com/" title="vulnerability database" target="_blank">Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress plugin" target="_blank">WordPress Plugin</a>
</p>
<p align="center">
<a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a>
<a href="https://travis-ci.org/wpscanteam/wpscan" target="_blank"><img src="https://travis-ci.org/wpscanteam/wpscan.svg?branch=master"></a>
<a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
<a href="https://www.patreon.com/wpscan" target="_blank"><img src="https://img.shields.io/badge/patreon-donate-green.svg"></a>
</p>
# INSTALL
@@ -110,14 +125,6 @@ wpscan --url https://target.tld/ --enumerate u1-100
** replace u1-100 with a range of your choice.
# PROJECT HOME
[https://wpscan.org](https://wpscan.org)
# VULNERABILITY DATABASE
[https://wpvulndb.com](https://wpvulndb.com)
# LICENSE
## WPScan Public Source License

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'models'
require_relative 'finders'
require_relative 'controllers'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'controllers/core'
require_relative 'controllers/custom_directories'
require_relative 'controllers/wp_version'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to add the aliases in the CLI

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Specific Core controller to include WordPress checks
@@ -25,53 +27,56 @@ module WPScan
# @return [ Boolean ]
def update_db_required?
if local_db.missing_files?
raise MissingDatabaseFile if parsed_options[:update] == false
raise Error::MissingDatabaseFile if ParsedCli.update == false
return true
end
return parsed_options[:update] unless parsed_options[:update].nil?
return ParsedCli.update unless ParsedCli.update.nil?
return false unless user_interaction? && local_db.outdated?
output('@notice', msg: 'It seems like you have not updated the database for some time.')
print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
Readline.readline =~ /^y/i ? true : false
/^y/i.match?(Readline.readline) ? true : false
end
def update_db
output('db_update_started')
output('db_update_finished', updated: local_db.update, verbose: parsed_options[:verbose])
output('db_update_finished', updated: local_db.update, verbose: ParsedCli.verbose)
exit(0) unless parsed_options[:url]
exit(0) unless ParsedCli.url
end
def before_scan
@last_update = local_db.last_update
maybe_output_banner_help_and_version # From CMS Scanner
maybe_output_banner_help_and_version # From CMSScanner
update_db if update_db_required?
setup_cache
check_target_availability
load_server_module
check_wordpress_state
rescue Error::NotWordPress => e
target.maybe_add_cookies
raise e unless target.wordpress?(ParsedCli.detection_mode)
end
# Raises errors if the target is hosted on wordpress.com or is not running WordPress
# Also check if the homepage_url is still the install url
def check_wordpress_state
raise WordPressHostedError if target.wordpress_hosted?
raise Error::WordPressHosted if target.wordpress_hosted?
if Addressable::URI.parse(target.homepage_url).path =~ %r{/wp-admin/install.php$}i
if %r{/wp-admin/install.php$}i.match?(Addressable::URI.parse(target.homepage_url).path)
output('not_fully_configured', url: target.homepage_url)
exit(WPScan::ExitCode::VULNERABLE)
end
raise NotWordPressError unless target.wordpress?(parsed_options[:detection_mode]) || parsed_options[:force]
raise Error::NotWordPress unless target.wordpress?(ParsedCli.detection_mode) || ParsedCli.force
end
# Loads the related server module in the target
@@ -83,7 +88,7 @@ module WPScan
server = target.server || :Apache # Tries to auto detect the server
# Force a specific server module to be loaded if supplied
case parsed_options[:server]
case ParsedCli.server
when :apache
server = :Apache
when :iis
@@ -95,7 +100,7 @@ module WPScan
mod = CMSScanner::Target::Server.const_get(server)
target.extend mod
WPScan::WpItem.include mod
Model::WpItem.include mod
server
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to ensure that the wp-content and wp-plugins
@@ -11,12 +13,12 @@ module WPScan
end
def before_scan
target.content_dir = parsed_options[:wp_content_dir] if parsed_options[:wp_content_dir]
target.plugins_dir = parsed_options[:wp_plugins_dir] if parsed_options[:wp_plugins_dir]
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
return if target.content_dir
raise 'Unable to identify the wp-content dir, please supply it with --wp-content-dir'
raise Error::WpContentDirNotDetected
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'enumeration/cli_options'
require_relative 'enumeration/enum_methods'
@@ -8,10 +10,14 @@ module WPScan
def before_scan
DB::DynamicFinders::Plugin.create_versions_finders
DB::DynamicFinders::Theme.create_versions_finders
# Force the Garbage Collector to run due to the above method being
# quite heavy in objects allocation
GC.start
end
def run
enum = parsed_options[:enumerate] || {}
enum = ParsedCli.enumerate || {}
enum_plugins if enum_plugins?(enum)
enum_themes if enum_themes?(enum)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Enumeration CLI Options

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Enumeration Methods
@@ -5,13 +7,13 @@ module WPScan
# @param [ String ] type (plugins or themes)
# @param [ Symbol ] detection_mode
#
# @return [ String ] The related enumration message depending on the parsed_options and type supplied
# @return [ String ] The related enumration message depending on the ParsedCli and type supplied
def enum_message(type, detection_mode)
return unless %w[plugins themes].include?(type)
details = if parsed_options[:enumerate][:"vulnerable_#{type}"]
details = if ParsedCli.enumerate[:"vulnerable_#{type}"]
'Vulnerable'
elsif parsed_options[:enumerate][:"all_#{type}"]
elsif ParsedCli.enumerate[:"all_#{type}"]
'All'
else
'Most Popular'
@@ -37,15 +39,15 @@ module WPScan
#
# @return [ Hash ]
def default_opts(type)
mode = parsed_options[:"#{type}_detection"] || parsed_options[:detection_mode]
mode = ParsedCli.options[:"#{type}_detection"] || ParsedCli.detection_mode
{
mode: mode,
exclude_content: parsed_options[:exclude_content_based],
exclude_content: ParsedCli.exclude_content_based,
show_progression: user_interaction?,
version_detection: {
mode: parsed_options[:"#{type}_version_detection"] || mode,
confidence_threshold: parsed_options[:"#{type}_version_all"] ? 0 : 100
mode: ParsedCli.options[:"#{type}_version_detection"] || mode,
confidence_threshold: ParsedCli.options[:"#{type}_version_all"] ? 0 : 100
}
}
end
@@ -59,7 +61,7 @@ module WPScan
def enum_plugins
opts = default_opts('plugins').merge(
list: plugins_list_from_opts(parsed_options),
list: plugins_list_from_opts(ParsedCli.options),
sort: true
)
@@ -75,7 +77,7 @@ module WPScan
plugins.each(&:version)
plugins.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_plugins]
plugins.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_plugins]
output('plugins', plugins: plugins)
end
@@ -105,7 +107,7 @@ module WPScan
def enum_themes
opts = default_opts('themes').merge(
list: themes_list_from_opts(parsed_options),
list: themes_list_from_opts(ParsedCli.options),
sort: true
)
@@ -121,7 +123,7 @@ module WPScan
themes.each(&:version)
themes.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_themes]
themes.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_themes]
output('themes', themes: themes)
end
@@ -143,28 +145,28 @@ module WPScan
end
def enum_timthumbs
opts = default_opts('timthumbs').merge(list: parsed_options[:timthumbs_list])
opts = default_opts('timthumbs').merge(list: ParsedCli.timthumbs_list)
output('@info', msg: "Enumerating Timthumbs #{enum_detection_message(opts[:mode])}") if user_interaction?
output('timthumbs', timthumbs: target.timthumbs(opts))
end
def enum_config_backups
opts = default_opts('config_backups').merge(list: parsed_options[:config_backups_list])
opts = default_opts('config_backups').merge(list: ParsedCli.config_backups_list)
output('@info', msg: "Enumerating Config Backups #{enum_detection_message(opts[:mode])}") if user_interaction?
output('config_backups', config_backups: target.config_backups(opts))
end
def enum_db_exports
opts = default_opts('db_exports').merge(list: parsed_options[:db_exports_list])
opts = default_opts('db_exports').merge(list: ParsedCli.db_exports_list)
output('@info', msg: "Enumerating DB Exports #{enum_detection_message(opts[:mode])}") if user_interaction?
output('db_exports', db_exports: target.db_exports(opts))
end
def enum_medias
opts = default_opts('medias').merge(range: parsed_options[:enumerate][:medias])
opts = default_opts('medias').merge(range: ParsedCli.enumerate[:medias])
if user_interaction?
output('@info',
@@ -179,13 +181,13 @@ module WPScan
#
# @return [ Boolean ] Wether or not to enumerate the users
def enum_users?(opts)
opts[:users] || (parsed_options[:passwords] && !parsed_options[:username] && !parsed_options[:usernames])
opts[:users] || (ParsedCli.passwords && !ParsedCli.username && !ParsedCli.usernames)
end
def enum_users
opts = default_opts('users').merge(
range: enum_users_range,
list: parsed_options[:users_list]
list: ParsedCli.users_list
)
output('@info', msg: "Enumerating Users #{enum_detection_message(opts[:mode])}") if user_interaction?
@@ -196,7 +198,7 @@ module WPScan
# If the --enumerate is used, the default value is handled by the Option
# However, when using --passwords alone, the default has to be set by the code below
def enum_users_range
parsed_options[:enumerate][:users] || cli_enum_choices[0].choices[:u].validate(nil)
ParsedCli.enumerate[:users] || cli_enum_choices[0].choices[:u].validate(nil)
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Main Theme Controller
@@ -16,9 +18,9 @@ module WPScan
output(
'theme',
theme: target.main_theme(
mode: parsed_options[:main_theme_detection] || parsed_options[:detection_mode]
mode: ParsedCli.main_theme_detection || ParsedCli.detection_mode
),
verbose: parsed_options[:verbose]
verbose: ParsedCli.verbose
)
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Password Attack Controller
@@ -22,7 +24,7 @@ module WPScan
end
def run
return unless parsed_options[:passwords]
return unless ParsedCli.passwords
if user_interaction?
output('@info',
@@ -31,13 +33,13 @@ module WPScan
attack_opts = {
show_progression: user_interaction?,
multicall_max_passwords: parsed_options[:multicall_max_passwords]
multicall_max_passwords: ParsedCli.multicall_max_passwords
}
begin
found = []
attacker.attack(users, passwords(parsed_options[:passwords]), attack_opts) do |user|
attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user|
found << user
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
@@ -52,24 +54,24 @@ module WPScan
@attacker ||= attacker_from_cli_options || attacker_from_automatic_detection
end
# @return [ WPScan::XMLRPC ]
# @return [ Model::XMLRPC ]
def xmlrpc
@xmlrpc ||= target.xmlrpc
end
# @return [ CMSScanner::Finders::Finder ]
def attacker_from_cli_options
return unless parsed_options[:password_attack]
return unless ParsedCli.password_attack
case parsed_options[:password_attack]
case ParsedCli.password_attack
when :wp_login
WPScan::Finders::Passwords::WpLogin.new(target)
when :xmlrpc
raise XMLRPCNotDetected unless xmlrpc
raise Error::XMLRPCNotDetected unless xmlrpc
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
when :xmlrpc_multicall
raise XMLRPCNotDetected unless xmlrpc
raise Error::XMLRPCNotDetected unless xmlrpc
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
end
@@ -92,10 +94,10 @@ module WPScan
# @return [ Array<Users> ] The users to brute force
def users
return target.users unless parsed_options[:usernames]
return target.users unless ParsedCli.usernames
parsed_options[:usernames].reduce([]) do |acc, elem|
acc << CMSScanner::User.new(elem.chomp)
ParsedCli.usernames.reduce([]) do |acc, elem|
acc << Model::User.new(elem.chomp)
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Controller
# Wp Version Controller
@@ -22,8 +24,8 @@ module WPScan
output(
'version',
version: target.wp_version(
mode: parsed_options[:wp_version_detection] || parsed_options[:detection_mode],
confidence_threshold: parsed_options[:wp_version_all] ? 0 : 100,
mode: ParsedCli.wp_version_detection || ParsedCli.detection_mode,
confidence_threshold: ParsedCli.wp_version_all ? 0 : 100,
show_progression: user_interaction?
)
)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'finders/interesting_findings'
require_relative 'finders/wp_items'
require_relative 'finders/wp_version'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'config_backups/known_filenames'
module WPScan

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module ConfigBackups
@@ -13,11 +15,10 @@ module WPScan
def aggressive(opts = {})
found = []
enumerate(potential_urls(opts), opts) do |res|
# Might need to improve that
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
next unless res.body =~ /define/i && res.body !~ /<\s?html/i
found << WPScan::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
found << Model::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'db_exports/known_locations'
module WPScan

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module DbExports
@@ -6,6 +8,8 @@ module WPScan
class KnownLocations < CMSScanner::Finders::Finder
include CMSScanner::Finders::Finder::Enumerator
SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE) TABLE|INSERT INTO/.freeze
# @param [ Hash ] opts
# @option opts [ String ] :list
# @option opts [ Boolean ] :show_progression
@@ -14,15 +18,23 @@ module WPScan
def aggressive(opts = {})
found = []
enumerate(potential_urls(opts), opts) do |res|
next unless res.code == 200 && res.body =~ /INSERT INTO/
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
if res.effective_url.end_with?('.zip')
next unless res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
else
next unless res.body =~ SQL_PATTERN
end
found << WPScan::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
end
found
end
def full_request_params
@full_request_params ||= { headers: { 'Range' => 'bytes=0-3000' } }
end
# @param [ Hash ] opts
# @option opts [ String ] :list Mandatory
#

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'interesting_findings/readme'
require_relative 'interesting_findings/wp_cron'
require_relative 'interesting_findings/multisite'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -6,13 +8,12 @@ module WPScan
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'wp-content/backup-db/'
url = target.url(path)
res = Browser.get(url)
res = target.head_and_get(path, [200, 403])
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
WPScan::BackupDB.new(
url,
Model::BackupDB.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path),

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -9,7 +11,7 @@ module WPScan
return unless target.debug_log?(path)
WPScan::DebugLog.new(
Model::DebugLog.new(
target.url(path),
confidence: 100, found_by: DIRECT_ACCESS,
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -5,13 +7,12 @@ module WPScan
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
url = target.url('installer-log.txt')
res = Browser.get(url)
path = 'installer-log.txt'
return unless res.body =~ /DUPLICATOR INSTALL-LOG/
return unless target.head_and_get(path).body =~ /DUPLICATOR INSTALL-LOG/
WPScan::DuplicatorInstallerLog.new(
url,
Model::DuplicatorInstallerLog.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS,
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -5,14 +7,14 @@ module WPScan
class EmergencyPwdResetScript < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
url = target.url('/emergency.php')
res = Browser.get(url)
path = 'emergency.php'
res = target.head_and_get(path)
return unless res.code == 200 && !target.homepage_or_404?(res)
WPScan::EmergencyPwdResetScript.new(
url,
confidence: res.body =~ /password/i ? 100 : 40,
Model::EmergencyPwdResetScript.new(
target.url(path),
confidence: /password/i.match?(res.body) ? 100 : 40,
found_by: DIRECT_ACCESS,
references: {
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -10,7 +12,7 @@ module WPScan
return if fpd_entries.empty?
WPScan::FullPathDisclosure.new(
Model::FullPathDisclosure.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -12,7 +14,9 @@ module WPScan
url = target.url('wp-content/mu-plugins/')
return WPScan::MuPlugins.new(
target.mu_plugins = true
return Model::MuPlugins.new(
url,
confidence: 70,
found_by: 'URLs In Homepage (Passive Detection)',
@@ -31,11 +35,9 @@ module WPScan
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::MuPlugins.new(
Model::MuPlugins.new(
url,
confidence: 80,
found_by: DIRECT_ACCESS,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -15,7 +17,7 @@ module WPScan
target.multisite = true
WPScan::Multisite.new(
Model::Multisite.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -5,14 +7,14 @@ module WPScan
class Readme < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
potential_files.each do |file|
url = target.url(file)
res = Browser.get(url)
potential_files.each do |path|
res = target.head_and_get(path)
if res.code == 200 && res.body =~ /wordpress/i
return WPScan::Readme.new(url, confidence: 100, found_by: DIRECT_ACCESS)
end
next unless res.code == 200 && res.body =~ /wordpress/i
return Model::Readme.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
end
nil
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -18,7 +20,7 @@ module WPScan
target.registration_enabled = true
WPScan::Registration.new(
Model::Registration.new(
res.effective_url,
confidence: 100,
found_by: DIRECT_ACCESS,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -7,11 +9,11 @@ module WPScan
def aggressive(_opts = {})
path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
url = target.url(path)
res = Browser.get(url)
res = browser.forge_request(url, target.head_or_get_request_params).run
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
WPScan::TmmDbMigrate.new(
Model::TmmDbMigrate.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -11,7 +13,7 @@ module WPScan
url = target.url(path)
WPScan::UploadDirectoryListing.new(
Model::UploadDirectoryListing.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,

View File

@@ -1,27 +1,25 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# UploadSQLDump finder
class UploadSQLDump < CMSScanner::Finders::Finder
SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/.freeze
SQL_PATTERN = /(?:DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO/.freeze
# @return [ InterestingFinding ]
def aggressive(_opts = {})
url = dump_url
res = Browser.get(url)
path = 'wp-content/uploads/dump.sql'
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
return unless res.code == 200 && res.body =~ SQL_PATTERN
return unless res.body =~ SQL_PATTERN
WPScan::UploadSQLDump.new(
url,
Model::UploadSQLDump.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS
)
end
def dump_url
target.url('wp-content/uploads/dump.sql')
end
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
@@ -9,7 +11,7 @@ module WPScan
return unless res.code == 200
WPScan::WPCron.new(
Model::WPCron.new(
wp_cron_url,
confidence: 60,
found_by: DIRECT_ACCESS,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'main_theme/css_style'
require_relative 'main_theme/woo_framework_meta_generator'
require_relative 'main_theme/urls_in_homepage'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
@@ -6,7 +8,7 @@ module WPScan
include Finders::WpItems::URLsInHomepage
def create_theme(slug, style_url, opts)
WPScan::Theme.new(
Model::Theme.new(
slug,
target,
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
@@ -14,7 +16,7 @@ module WPScan
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))
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
@@ -10,7 +12,7 @@ module WPScan
def passive(opts = {})
return unless target.homepage_res.body =~ PATTERN
WPScan::Theme.new(
Model::Theme.new(
Regexp.last_match[1],
target,
opts.merge(found_by: found_by, confidence: 80)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'medias/attachment_brute_forcing'
module WPScan

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Medias
@@ -15,7 +17,7 @@ module WPScan
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))
found << Model::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'passwords/wp_login'
require_relative 'passwords/xml_rpc'
require_relative 'passwords/xml_rpc_multicall'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Passwords
@@ -10,7 +12,8 @@ module WPScan
end
def valid_credentials?(response)
response.code == 302
response.code == 302 &&
response.headers['Set-Cookie']&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
end
def errored_response?(response)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Passwords

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Passwords
@@ -20,13 +22,13 @@ module WPScan
target.multi_call(methods).run
end
# @param [ Array<CMSScanner::User> ] users
# @param [ Array<Model::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
# @yield [ Model::User ] When a valid combination is found
#
# TODO: Make rubocop happy about metrics etc
#

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'plugin_version/readme'
module WPScan
@@ -7,7 +9,7 @@ module WPScan
class Base
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Plugin ] plugin
# @param [ Model::Plugin ] plugin
def initialize(plugin)
finders << PluginVersion::Readme.new(plugin)
@@ -16,7 +18,7 @@ module WPScan
# Load the finders associated with the plugin
#
# @param [ WPScan::Plugin ] plugin
# @param [ Model::Plugin ] plugin
def load_specific_finders(plugin)
module_name = plugin.classify

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module PluginVersion
@@ -7,21 +9,23 @@ module WPScan
def aggressive(_opts = {})
found_by_msg = 'Readme - %s (Aggressive Detection)'
WPScan::WpItem::READMES.each do |file|
url = target.url(file)
res = Browser.get(url)
# The target(plugin)#readme_url can't be used directly here
# as if the --detection-mode is passive, it will always return nil
Model::WpItem::READMES.each do |file|
res = target.head_and_get(file)
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
return numbers.reduce([]) do |a, e|
a << WPScan::Version.new(
a << Model::Version.new(
e[0],
found_by: format(found_by_msg, e[1]),
confidence: e[2],
interesting_entries: [url]
interesting_entries: [res.effective_url]
)
end
end
nil
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'plugins/urls_in_homepage'
require_relative 'plugins/known_locations'
# From the DynamicFinders

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -15,7 +17,7 @@ module WPScan
def process_response(opts, response, slug, klass, config)
return unless response.body =~ config['pattern']
Plugin.new(
Model::Plugin.new(
slug,
target,
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -18,7 +20,7 @@ module WPScan
next unless comment =~ config['pattern']
return Plugin.new(
return Model::Plugin.new(
slug,
target,
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -19,7 +21,7 @@ module WPScan
# when checking for plugins
#
Plugin.new(
Model::Plugin.new(
slug,
target,
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -18,7 +20,7 @@ module WPScan
configs.each do |klass, config|
next unless headers[config['header']] && headers[config['header']].to_s =~ config['pattern']
found << Plugin.new(
found << Model::Plugin.new(
slug,
target,
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -16,7 +18,7 @@ module WPScan
response.html.xpath(config['xpath'] || '//script[not(@src)]').each do |node|
next if config['pattern'] && !node.text.match(config['pattern'])
return Plugin.new(
return Model::Plugin.new(
slug,
target,
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -5,6 +7,11 @@ module WPScan
class KnownLocations < CMSScanner::Finders::Finder
include CMSScanner::Finders::Finder::Enumerator
# @return [ Array<Integer> ]
def valid_response_codes
@valid_response_codes ||= [200, 401, 403, 301, 500].freeze
end
# @param [ Hash ] opts
# @option opts [ String ] :list
#
@@ -12,12 +19,8 @@ module WPScan
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))
enumerate(target_urls(opts), opts.merge(check_full_response: [200, 401, 403, 500])) do |_res, slug|
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
end
found
@@ -30,10 +33,9 @@ module WPScan
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
urls[target.plugin_url(slug)] = slug
end
urls

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -14,7 +16,7 @@ module WPScan
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))
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
@@ -16,7 +18,7 @@ module WPScan
response.html.xpath(config['xpath']).each do |node|
next if config['pattern'] && !node.text.match(config['pattern'])
return Plugin.new(
return Model::Plugin.new(
slug,
target,
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'theme_version/style'
require_relative 'theme_version/woo_framework_meta_generator'
@@ -8,7 +10,7 @@ module WPScan
class Base
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Theme ] theme
# @param [ Model::Theme ] theme
def initialize(theme)
finders <<
ThemeVersion::Style.new(theme) <<
@@ -19,7 +21,7 @@ module WPScan
# Load the finders associated with the theme
#
# @param [ WPScan::Theme ] theme
# @param [ Model::Theme ] theme
def load_specific_finders(theme)
module_name = theme.classify

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module ThemeVersion
@@ -30,7 +32,7 @@ module WPScan
def style_version
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
WPScan::Version.new(
Model::Version.new(
Regexp.last_match[1],
found_by: found_by,
confidence: 80,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module ThemeVersion
@@ -11,7 +13,7 @@ module WPScan
return unless Regexp.last_match[1] == target.slug
WPScan::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
Model::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'themes/urls_in_homepage'
require_relative 'themes/known_locations'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Themes
@@ -5,6 +7,11 @@ module WPScan
class KnownLocations < CMSScanner::Finders::Finder
include CMSScanner::Finders::Finder::Enumerator
# @return [ Array<Integer> ]
def valid_response_codes
@valid_response_codes ||= [200, 401, 403, 301, 500].freeze
end
# @param [ Hash ] opts
# @option opts [ String ] :list
#
@@ -12,12 +19,8 @@ module WPScan
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))
enumerate(target_urls(opts), opts.merge(check_full_response: [200, 401, 403, 500])) do |_res, slug|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
end
found
@@ -28,12 +31,11 @@ module WPScan
#
# @return [ Hash ]
def target_urls(opts = {})
slugs = opts[:list] || DB::Themes.vulnerable_slugs
urls = {}
themes_url = target.url('wp-content/themes/')
slugs = opts[:list] || DB::Themes.vulnerable_slugs
urls = {}
slugs.each do |slug|
urls["#{themes_url}#{URI.encode(slug)}/"] = slug
urls[target.theme_url(slug)] = slug
end
urls

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Themes
@@ -12,7 +14,7 @@ module WPScan
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))
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'timthumb_version/bad_request'
module WPScan
@@ -7,7 +9,7 @@ module WPScan
class Base
include CMSScanner::Finders::UniqueFinder
# @param [ WPScan::Timthumb ] target
# @param [ Model::Timthumb ] target
def initialize(target)
finders << TimthumbVersion::BadRequest.new(target)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module TimthumbVersion
@@ -8,7 +10,7 @@ module WPScan
def aggressive(_opts = {})
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
WPScan::Version.new(
Model::Version.new(
Regexp.last_match[2],
found_by: 'Bad Request (Aggressive Detection)',
confidence: 90,

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'timthumbs/known_locations'
module WPScan

View File

@@ -1,10 +1,19 @@
# frozen_string_literal: true
module WPScan
module Finders
module Timthumbs
# Known Locations Timthumbs Finder
# Note: A vulnerable version, 2.8.13 can be found here:
# https://github.com/GabrielGil/TimThumb/blob/980c3d6a823477761570475e8b83d3e9fcd2d7ae/timthumb.php
class KnownLocations < CMSScanner::Finders::Finder
include CMSScanner::Finders::Finder::Enumerator
# @return [ Array<Integer> ]
def valid_response_codes
@valid_response_codes ||= [400]
end
# @param [ Hash ] opts
# @option opts [ String ] :list Mandatory
#
@@ -12,10 +21,10 @@ module WPScan
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts) do |res|
next unless res.code == 400 && res.body =~ /no image specified/i
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
next unless res.body =~ /no image specified/i
found << WPScan::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'users/author_posts'
require_relative 'users/wp_json_api'
require_relative 'users/oembed_api'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -5,6 +7,11 @@ module WPScan
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
include CMSScanner::Finders::Finder::Enumerator
# @return [ Array<Integer> ]
def valid_response_codes
@valid_response_codes ||= [200, 301, 302]
end
# @param [ Hash ] opts
# @option opts [ Range ] :range Mandatory
#
@@ -13,12 +20,12 @@ module WPScan
found = []
found_by_msg = 'Author Id Brute Forcing - %s (Aggressive Detection)'
enumerate(target_urls(opts), opts) do |res, id|
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, id|
username, found_by, confidence = potential_username(res)
next unless username
found << CMSScanner::User.new(
found << Model::User.new(
username,
id: id,
found_by: format(found_by_msg, found_by),
@@ -47,7 +54,7 @@ module WPScan
super(opts.merge(title: ' Brute Forcing Author IDs -'))
end
def request_params
def full_request_params
{ followlocation: true }
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -10,7 +12,7 @@ module WPScan
found_by_msg = 'Author Posts - %s (Passive Detection)'
usernames(opts).reduce([]) do |a, e|
a << CMSScanner::User.new(
a << Model::User.new(
e[0],
found_by: format(found_by_msg, e[1]),
confidence: e[2]
@@ -48,7 +50,7 @@ module WPScan
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
elsif uri.query =~ /author=[0-9]+/
elsif /author=[0-9]+/.match?(uri.query)
usernames << [node.text.to_s.strip, 'Display Name', 30]
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -24,7 +26,7 @@ module WPScan
next unless error =~ /The password you entered for the username|Incorrect Password/i
found << CMSScanner::User.new(username, found_by: found_by, confidence: 100)
found << Model::User.new(username, found_by: found_by, confidence: 100)
end
found

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -21,10 +23,10 @@ module WPScan
return [] unless details
[CMSScanner::User.new(details[0],
found_by: format(found_by_msg, details[1]),
confidence: details[2],
interesting_entries: [api_url])]
[Model::User.new(details[0],
found_by: format(found_by_msg, details[1]),
confidence: details[2],
interesting_entries: [api_url])]
rescue JSON::ParserError
[]
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -17,20 +19,20 @@ module WPScan
begin
res.xml.xpath('//item/dc:creator').each do |node|
potential_username = node.text.to_s
username = node.text.to_s
# Ignoring potential username longer than 60 characters and containing accents
# as they are considered invalid. See https://github.com/wpscanteam/wpscan/issues/1215
next if potential_username.length > 60 || potential_username =~ /[^\x00-\x7F]/
next if username.strip.empty? || username.length > 60 || username =~ /[^\x00-\x7F]/
potential_usernames << potential_username
potential_usernames << username
end
rescue Nokogiri::XML::XPath::SyntaxError
next
end
potential_usernames.uniq.each do |potential_username|
found << CMSScanner::User.new(potential_username, found_by: found_by, confidence: 50)
potential_usernames.uniq.each do |username|
found << Model::User.new(username, found_by: found_by, confidence: 50)
end
break

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -41,11 +43,11 @@ module WPScan
found = []
JSON.parse(response.body)&.each do |user|
found << CMSScanner::User.new(user['slug'],
id: user['id'],
found_by: found_by,
confidence: 100,
interesting_entries: [response.effective_url])
found << Model::User.new(user['slug'],
id: user['id'],
found_by: found_by,
confidence: 100,
interesting_entries: [response.effective_url])
end
found
@@ -53,7 +55,15 @@ module WPScan
# @return [ String ] The URL of the API listing the Users
def api_url
@api_url ||= target.url('wp-json/wp/v2/users/')
return @api_url if @api_url
target.in_scope_urls(target.homepage_res, "//link[@rel='https://api.w.org/']/@href").each do |url, _tag|
uri = Addressable::URI.parse(url.strip)
return @api_url = uri.join('wp/v2/users/').to_s if uri.path.include?('wp-json')
end
@api_url = target.url('wp-json/wp/v2/users/')
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
@@ -15,10 +17,10 @@ module WPScan
next unless username && !username.strip.empty?
found << CMSScanner::User.new(username,
found_by: found_by,
confidence: 100,
interesting_entries: [sitemap_url])
found << Model::User.new(username,
found_by: found_by,
confidence: 100,
interesting_entries: [sitemap_url])
end
found

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module WpItems

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'wp_version/rss_generator'
require_relative 'wp_version/atom_generator'
require_relative 'wp_version/rdf_generator'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module WpVersion

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module WpVersion

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module WpVersion
@@ -13,9 +15,9 @@ module WPScan
number = Regexp.last_match(1)
return unless WPScan::WpVersion.valid?(number)
return unless Model::WpVersion.valid?(number)
WPScan::WpVersion.new(
Model::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)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module WpVersion

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module WPScan
module Finders
module WpVersion
@@ -11,7 +13,7 @@ module WPScan
hydra.abort
progress_bar.finish
return WPScan::WpVersion.new(
return Model::WpVersion.new(
version_number,
found_by: 'Unique Fingerprinting (Aggressive Detection)',
confidence: 100,

View File

@@ -1,3 +1,11 @@
# frozen_string_literal: true
module WPScan
module Model
include CMSScanner::Model
end
end
require_relative 'models/interesting_finding'
require_relative 'models/wp_version'
require_relative 'models/xml_rpc'

View File

@@ -1,5 +1,9 @@
# frozen_string_literal: true
module WPScan
# Config Backup
class ConfigBackup < InterestingFinding
module Model
# Config Backup
class ConfigBackup < InterestingFinding
end
end
end

View File

@@ -1,5 +1,9 @@
# frozen_string_literal: true
module WPScan
# DB Export
class DbExport < InterestingFinding
module Model
# DB Export
class DbExport < InterestingFinding
end
end
end

View File

@@ -1,48 +1,52 @@
# frozen_string_literal: true
module WPScan
# Custom class to include the WPScan::References module
class InterestingFinding < CMSScanner::InterestingFinding
include References
end
module Model
# Custom class to include the WPScan::References module
class InterestingFinding < CMSScanner::Model::InterestingFinding
include References
end
#
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
#
class BackupDB < InterestingFinding
end
#
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
#
class BackupDB < InterestingFinding
end
class DebugLog < InterestingFinding
end
class DebugLog < InterestingFinding
end
class DuplicatorInstallerLog < InterestingFinding
end
class DuplicatorInstallerLog < InterestingFinding
end
class EmergencyPwdResetScript < InterestingFinding
end
class EmergencyPwdResetScript < InterestingFinding
end
class FullPathDisclosure < InterestingFinding
end
class FullPathDisclosure < InterestingFinding
end
class MuPlugins < InterestingFinding
end
class MuPlugins < InterestingFinding
end
class Multisite < InterestingFinding
end
class Multisite < InterestingFinding
end
class Readme < InterestingFinding
end
class Readme < InterestingFinding
end
class Registration < InterestingFinding
end
class Registration < InterestingFinding
end
class TmmDbMigrate < InterestingFinding
end
class TmmDbMigrate < InterestingFinding
end
class UploadDirectoryListing < InterestingFinding
end
class UploadDirectoryListing < InterestingFinding
end
class UploadSQLDump < InterestingFinding
end
class UploadSQLDump < InterestingFinding
end
class WPCron < InterestingFinding
class WPCron < InterestingFinding
end
end
end

View File

@@ -1,5 +1,9 @@
# frozen_string_literal: true
module WPScan
# Media
class Media < InterestingFinding
module Model
# Media
class Media < InterestingFinding
end
end
end

View File

@@ -1,25 +1,33 @@
# frozen_string_literal: true
module WPScan
# WordPress Plugin
class Plugin < WpItem
# See WpItem
def initialize(slug, blog, opts = {})
super(slug, blog, opts)
module Model
# WordPress Plugin
class Plugin < WpItem
# See WpItem
def initialize(slug, blog, opts = {})
super(slug, blog, opts)
@uri = Addressable::URI.parse(blog.url("wp-content/plugins/#{slug}/"))
end
# To be used by #head_and_get
# If custom wp-content, it will be replaced by blog#url
@path_from_blog = "wp-content/plugins/#{slug}/"
# @return [ JSON ]
def db_data
DB::Plugin.db_data(slug)
end
@uri = Addressable::URI.parse(blog.url(path_from_blog))
end
# @param [ Hash ] opts
#
# @return [ WPScan::Version, false ]
def version(opts = {})
@version = Finders::PluginVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
# @return [ JSON ]
def db_data
@db_data ||= DB::Plugin.db_data(slug)
end
@version
# @param [ Hash ] opts
#
# @return [ Model::Version, false ]
def version(opts = {})
@version = Finders::PluginVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
@version
end
end
end
end

View File

@@ -1,99 +1,107 @@
# frozen_string_literal: true
module WPScan
# WordPress Theme
class Theme < WpItem
attr_reader :style_url, :style_name, :style_uri, :author, :author_uri, :template, :description,
:license, :license_uri, :tags, :text_domain
module Model
# WordPress Theme
class Theme < WpItem
attr_reader :style_url, :style_name, :style_uri, :author, :author_uri, :template, :description,
:license, :license_uri, :tags, :text_domain
# See WpItem
def initialize(slug, blog, opts = {})
super(slug, blog, opts)
# See WpItem
def initialize(slug, blog, opts = {})
super(slug, blog, opts)
@uri = Addressable::URI.parse(blog.url("wp-content/themes/#{slug}/"))
@style_url = opts[:style_url] || url('style.css')
# To be used by #head_and_get
# If custom wp-content, it will be replaced by blog#url
@path_from_blog = "wp-content/themes/#{slug}/"
parse_style
end
@uri = Addressable::URI.parse(blog.url(path_from_blog))
@style_url = opts[:style_url] || url('style.css')
# @return [ JSON ]
def db_data
DB::Theme.db_data(slug)
end
# @param [ Hash ] opts
#
# @return [ WPScan::Version, false ]
def version(opts = {})
@version = Finders::ThemeVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
@version
end
# @return [ Theme ]
def parent_theme
return unless template
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
opts = detection_opts.merge(
style_url: url(Regexp.last_match[1]),
found_by: 'Parent Themes (Passive Detection)',
confidence: 100
).merge(version_detection: version_detection_opts)
self.class.new(template, blog, opts)
end
# @param [ Integer ] depth
#
# @retun [ Array<Theme> ]
def parent_themes(depth = 3)
theme = self
found = []
(1..depth).each do |_|
parent = theme.parent_theme
break unless parent
found << parent
theme = parent
parse_style
end
found
end
def style_body
@style_body ||= Browser.get(style_url).body
end
def parse_style
{
style_name: 'Theme Name',
style_uri: 'Theme URI',
author: 'Author',
author_uri: 'Author URI',
template: 'Template',
description: 'Description',
license: 'License',
license_uri: 'License URI',
tags: 'Tags',
text_domain: 'Text Domain'
}.each do |attribute, tag|
instance_variable_set(:"@#{attribute}", parse_style_tag(style_body, tag))
# @return [ JSON ]
def db_data
@db_data ||= DB::Theme.db_data(slug)
end
end
# @param [ String ] bofy
# @param [ String ] tag
#
# @return [ String ]
def parse_style_tag(body, tag)
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
# @param [ Hash ] opts
#
# @return [ Model::Version, false ]
def version(opts = {})
@version = Finders::ThemeVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
value && !value.strip.empty? ? value.strip : nil
end
@version
end
def ==(other)
super(other) && style_url == other.style_url
# @return [ Theme ]
def parent_theme
return unless template
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
opts = detection_opts.merge(
style_url: url(Regexp.last_match[1]),
found_by: 'Parent Themes (Passive Detection)',
confidence: 100
).merge(version_detection: version_detection_opts)
self.class.new(template, blog, opts)
end
# @param [ Integer ] depth
#
# @retun [ Array<Theme> ]
def parent_themes(depth = 3)
theme = self
found = []
(1..depth).each do |_|
parent = theme.parent_theme
break unless parent
found << parent
theme = parent
end
found
end
def style_body
@style_body ||= Browser.get(style_url).body
end
def parse_style
{
style_name: 'Theme Name',
style_uri: 'Theme URI',
author: 'Author',
author_uri: 'Author URI',
template: 'Template',
description: 'Description',
license: 'License',
license_uri: 'License URI',
tags: 'Tags',
text_domain: 'Text Domain'
}.each do |attribute, tag|
instance_variable_set(:"@#{attribute}", parse_style_tag(style_body, tag))
end
end
# @param [ String ] bofy
# @param [ String ] tag
#
# @return [ String ]
def parse_style_tag(body, tag)
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
value && !value.strip.empty? ? value.strip : nil
end
def ==(other)
super(other) && style_url == other.style_url
end
end
end
end

View File

@@ -1,71 +1,75 @@
# frozen_string_literal: true
module WPScan
# Timthumb
class Timthumb < InterestingFinding
include Vulnerable
module Model
# Timthumb
class Timthumb < InterestingFinding
include Vulnerable
attr_reader :version_detection_opts
attr_reader :version_detection_opts
# @param [ String ] url
# @param [ Hash ] opts
# @option opts [ Symbol ] :mode The mode to use to detect the version
def initialize(url, opts = {})
super(url, opts)
# @param [ String ] url
# @param [ Hash ] opts
# @option opts [ Symbol ] :mode The mode to use to detect the version
def initialize(url, opts = {})
super(url, opts)
@version_detection_opts = opts[:version_detection] || {}
end
@version_detection_opts = opts[:version_detection] || {}
end
# @param [ Hash ] opts
#
# @return [ WPScan::Version, false ]
def version(opts = {})
@version = Finders::TimthumbVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
# @param [ Hash ] opts
#
# @return [ Model::Version, false ]
def version(opts = {})
@version = Finders::TimthumbVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
@version
end
@version
end
# @return [ Array<Vulnerability> ]
def vulnerabilities
vulns = []
# @return [ Array<Vulnerability> ]
def vulnerabilities
vulns = []
vulns << rce_webshot_vuln if version == false || version > '1.35' && version < '2.8.14' && webshot_enabled?
vulns << rce_132_vuln if version == false || version < '1.33'
vulns << rce_webshot_vuln if version == false || version > '1.35' && version < '2.8.14' && webshot_enabled?
vulns << rce_132_vuln if version == false || version < '1.33'
vulns
end
vulns
end
# @return [ Vulnerability ] The RCE in the <= 1.32
def rce_132_vuln
Vulnerability.new(
'Timthumb <= 1.32 Remote Code Execution',
{ exploitdb: ['17602'] },
'RCE',
'1.33'
)
end
# @return [ Vulnerability ] The RCE in the <= 1.32
def rce_132_vuln
Vulnerability.new(
'Timthumb <= 1.32 Remote Code Execution',
{ exploitdb: ['17602'] },
'RCE',
'1.33'
)
end
# @return [ Vulnerability ] The RCE due to the WebShot in the > 1.35 (or >= 2.0) and <= 2.8.13
def rce_webshot_vuln
Vulnerability.new(
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
{
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
cve: '2014-4663'
},
'RCE',
'2.8.14'
)
end
# @return [ Vulnerability ] The RCE due to the WebShot in the > 1.35 (or >= 2.0) and <= 2.8.13
def rce_webshot_vuln
Vulnerability.new(
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
{
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
cve: '2014-4663'
},
'RCE',
'2.8.14'
)
end
# @return [ Boolean ]
def webshot_enabled?
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
# @return [ Boolean ]
def webshot_enabled?
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
res.body =~ /WEBSHOT_ENABLED == true/ ? false : true
end
/WEBSHOT_ENABLED == true/.match?(res.body) ? false : true
end
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
def default_allowed_domains
%w[flickr.com picasa.com img.youtube.com upload.wikimedia.org]
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
def default_allowed_domains
%w[flickr.com picasa.com img.youtube.com upload.wikimedia.org]
end
end
end
end

View File

@@ -1,158 +1,170 @@
# frozen_string_literal: true
module WPScan
# WpItem (superclass of Plugin & Theme)
class WpItem
include Vulnerable
include Finders::Finding
include CMSScanner::Target::Platform::PHP
include CMSScanner::Target::Server::Generic
module Model
# WpItem (superclass of Plugin & Theme)
class WpItem
include Vulnerable
include Finders::Finding
include CMSScanner::Target::Platform::PHP
include CMSScanner::Target::Server::Generic
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
CHANGELOGS = %w[changelog.txt CHANGELOG.md changelog.md].freeze
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :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_urls, to: :blog
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_urls, :head_or_get_params, to: :blog
# @param [ String ] slug The plugin/theme slug
# @param [ Target ] blog The targeted blog
# @param [ Hash ] opts
# @option opts [ Symbol ] :mode The detection mode to use
# @option opts [ Hash ] :version_detection The options to use when looking for the version
# @option opts [ String ] :url The URL of the item
def initialize(slug, blog, opts = {})
@slug = URI.decode(slug)
@blog = blog
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
# @param [ String ] slug The plugin/theme slug
# @param [ Target ] blog The targeted blog
# @param [ Hash ] opts
# @option opts [ Symbol ] :mode The detection mode to use
# @option opts [ Hash ] :version_detection The options to use when looking for the version
# @option opts [ String ] :url The URL of the item
def initialize(slug, blog, opts = {})
@slug = URI.decode(slug)
@blog = blog
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
@detection_opts = { mode: opts[:mode] }
@version_detection_opts = opts[:version_detection] || {}
@detection_opts = { mode: opts[:mode] }
@version_detection_opts = opts[:version_detection] || {}
parse_finding_options(opts)
end
# @return [ Array<Vulnerabily> ]
def vulnerabilities
return @vulnerabilities if @vulnerabilities
@vulnerabilities = []
[*db_data['vulnerabilities']].each do |json_vuln|
vulnerability = Vulnerability.load_from_json(json_vuln)
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
parse_finding_options(opts)
end
@vulnerabilities
end
# @return [ Array<Vulnerabily> ]
def vulnerabilities
return @vulnerabilities if @vulnerabilities
# Checks if the wp_item is vulnerable to a specific vulnerability
#
# @param [ Vulnerability ] vuln Vulnerability to check the item against
#
# @return [ Boolean ]
def vulnerable_to?(vuln)
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
@vulnerabilities = []
version < vuln.fixed_in
end
[*db_data['vulnerabilities']].each do |json_vuln|
vulnerability = Vulnerability.load_from_json(json_vuln)
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
end
# @return [ String ]
def latest_version
@latest_version ||= db_data['latest_version'] ? WPScan::Version.new(db_data['latest_version']) : nil
end
@vulnerabilities
end
# Not used anywhere ATM
# @return [ Boolean ]
def popular?
@popular ||= db_data['popular']
end
# Checks if the wp_item is vulnerable to a specific vulnerability
#
# @param [ Vulnerability ] vuln Vulnerability to check the item against
#
# @return [ Boolean ]
def vulnerable_to?(vuln)
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
# @return [ String ]
def last_updated
@last_updated ||= db_data['last_updated']
end
version < vuln.fixed_in
end
# @return [ Boolean ]
def outdated?
@outdated ||= if version && latest_version
version < latest_version
else
false
end
end
# @return [ String ]
def latest_version
@latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
end
# URI.encode is preferered over Addressable::URI.encode as it will encode
# leading # character:
# URI.encode('#t#') => %23t%23
# Addressable::URI.encode('#t#') => #t%23
#
# @param [ String ] path Optional path to merge with the uri
#
# @return [ String ]
def url(path = nil)
return unless @uri
return @uri.to_s unless path
# Not used anywhere ATM
# @return [ Boolean ]
def popular?
@popular ||= db_data['popular']
end
@uri.join(URI.encode(path)).to_s
end
# @return [ String ]
def last_updated
@last_updated ||= db_data['last_updated']
end
# @return [ Boolean ]
def ==(other)
self.class == other.class && slug == other.slug
end
# @return [ Boolean ]
def outdated?
@outdated ||= if version && latest_version
version < latest_version
else
false
end
end
def to_s
slug
end
# URI.encode is preferered over Addressable::URI.encode as it will encode
# leading # character:
# URI.encode('#t#') => %23t%23
# Addressable::URI.encode('#t#') => #t%23
#
# @param [ String ] path Optional path to merge with the uri
#
# @return [ String ]
def url(path = nil)
return unless @uri
return @uri.to_s unless path
# @return [ Symbol ] The Class symbol associated to the item
def classify
@classify ||= classify_slug(slug)
end
@uri.join(URI.encode(path)).to_s
end
# @return [ String ] The readme url if found
def readme_url
return if detection_opts[:mode] == :passive
# @return [ Boolean ]
def ==(other)
self.class == other.class && slug == other.slug
end
def to_s
slug
end
# @return [ Symbol ] The Class symbol associated to the item
def classify
@classify ||= classify_slug(slug)
end
# @return [ String, False ] The readme url if found, false otherwise
def readme_url
return if detection_opts[:mode] == :passive
return @readme_url unless @readme_url.nil?
if @readme_url.nil?
READMES.each do |path|
return @readme_url = url(path) if Browser.get(url(path)).code == 200
t_url = url(path)
return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200
end
@readme_url = false
end
@readme_url
end
# @param [ String ] path
# @param [ Hash ] params The request params
#
# @return [ Boolean ]
def directory_listing?(path = nil, params = {})
return if detection_opts[:mode] == :passive
# @return [ String, false ] The changelog urr if found
def changelog_url
return if detection_opts[:mode] == :passive
if @changelog_url.nil?
CHANGELOGS.each do |path|
return @changelog_url = url(path) if Browser.get(url(path)).code == 200
end
super(path, params)
end
@changelog_url
end
# @param [ String ] path
# @param [ Hash ] params The request params
#
# @return [ Boolean ]
def error_log?(path = 'error_log', params = {})
return if detection_opts[:mode] == :passive
# @param [ String ] path
# @param [ Hash ] params The request params
#
# @return [ Boolean ]
def directory_listing?(path = nil, params = {})
return if detection_opts[:mode] == :passive
super(path, params)
end
super(path, params)
end
# See CMSScanner::Target#head_and_get
#
# This is used by the error_log? above in the super()
# to have the correct path (ie readme.txt checked from the plugin/theme location
# and not from the blog root). Could also be used in finders
#
# @param [ String ] path
# @param [ Array<String> ] codes
# @param [ Hash ] params The requests params
# @option params [ Hash ] :head Request params for the HEAD
# @option params [ hash ] :get Request params for the GET
#
# @return [ Typhoeus::Response ]
def head_and_get(path, codes = [200], params = {})
final_path = +@path_from_blog
final_path << URI.encode(path) unless path.nil?
# @param [ String ] path
# @param [ Hash ] params The request params
#
# @return [ Boolean ]
def error_log?(path = 'error_log', params = {})
return if detection_opts[:mode] == :passive
super(path, params)
blog.head_and_get(final_path, codes, params)
end
end
end
end

View File

@@ -1,64 +1,67 @@
# frozen_string_literal: true
module WPScan
# WP Version
class WpVersion < CMSScanner::Version
include Vulnerable
module Model
# WP Version
class WpVersion < CMSScanner::Model::Version
include Vulnerable
def initialize(number, opts = {})
raise InvalidWordPressVersion unless WpVersion.valid?(number.to_s)
def initialize(number, opts = {})
raise Error::InvalidWordPressVersion unless WpVersion.valid?(number.to_s)
super(number, opts)
end
super(number, opts)
end
# @param [ String ] number
#
# @return [ Boolean ] true if the number is a valid WP version, false otherwise
def self.valid?(number)
all.include?(number)
end
# @param [ String ] number
#
# @return [ Boolean ] true if the number is a valid WP version, false otherwise
def self.valid?(number)
all.include?(number)
end
# @return [ Array<String> ] All the version numbers
def self.all
return @all_numbers if @all_numbers
# @return [ Array<String> ] All the version numbers
def self.all
return @all_numbers if @all_numbers
@all_numbers = []
@all_numbers = []
DB::Fingerprints.wp_fingerprints.each_value do |fp|
fp.each_value do |versions|
versions.each do |version|
@all_numbers << version unless @all_numbers.include?(version)
end
DB::Fingerprints.wp_fingerprints.each_value do |fp|
@all_numbers << fp.values
end
# @all_numbers.flatten.uniq.sort! {} doesn't produce the same result here.
@all_numbers.flatten!
@all_numbers.uniq!
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end
# @return [ JSON ]
def db_data
DB::Version.db_data(number)
end
# @return [ Array<Vulnerability> ]
def vulnerabilities
return @vulnerabilities if @vulnerabilities
@vulnerabilities = []
[*db_data['vulnerabilities']].each do |json_vuln|
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
# @return [ JSON ]
def db_data
@db_data ||= DB::Version.db_data(number)
end
@vulnerabilities
end
# @return [ Array<Vulnerability> ]
def vulnerabilities
return @vulnerabilities if @vulnerabilities
# @return [ String ]
def release_date
@release_date ||= db_data['release_date'] || 'Unknown'
end
@vulnerabilities = []
# @return [ String ]
def status
@status ||= db_data['status'] || 'Unknown'
[*db_data['vulnerabilities']].each do |json_vuln|
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
end
@vulnerabilities
end
# @return [ String ]
def release_date
@release_date ||= db_data['release_date'] || 'Unknown'
end
# @return [ String ]
def status
@status ||= db_data['status'] || 'Unknown'
end
end
end
end

View File

@@ -1,19 +1,23 @@
module WPScan
# Override of the CMSScanner::XMLRPC to include the references
class XMLRPC < CMSScanner::XMLRPC
include References # To be able to use the :wpvulndb reference if needed
# frozen_string_literal: true
# @return [ Hash ]
def references
{
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
metasploit: [
'auxiliary/scanner/http/wordpress_ghost_scanner',
'auxiliary/dos/http/wordpress_xmlrpc_dos',
'auxiliary/scanner/http/wordpress_xmlrpc_login',
'auxiliary/scanner/http/wordpress_pingback_access'
]
}
module WPScan
module Model
# Override of the CMSScanner::XMLRPC to include the references
class XMLRPC < CMSScanner::Model::XMLRPC
include References # To be able to use the :wpvulndb reference if needed
# @return [ Hash ]
def references
{
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
metasploit: [
'auxiliary/scanner/http/wordpress_ghost_scanner',
'auxiliary/dos/http/wordpress_xmlrpc_dos',
'auxiliary/scanner/http/wordpress_xmlrpc_login',
'auxiliary/scanner/http/wordpress_pingback_access'
]
}
end
end
end
end

View File

@@ -8,9 +8,6 @@
<% if @wp_item.readme_url -%>
| Readme: <%= @wp_item.readme_url %>
<% end -%>
<% if @wp_item.changelog_url -%>
| Changelog: <%= @wp_item.changelog_url %>
<% end -%>
<% if @wp_item.latest_version && @wp_item.outdated? -%>
| <%= warning_icon %> The version is out of date, the latest version is <%= @wp_item.latest_version %>
<% end -%>

View File

@@ -4,6 +4,5 @@
"last_updated": <%= @wp_item.last_updated.to_json %>,
"outdated": <%= @wp_item.outdated?.to_json %>,
"readme_url": <%= @wp_item.readme_url.to_json %>,
"changelog_url": <%= @wp_item.changelog_url.to_json %>,
"directory_listing": <%= @wp_item.directory_listing?.to_json %>,
"error_log_url": <% if @wp_item.error_log? %><%= @wp_item.url('error_log').to_json %><% else %>null<% end %>

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'wpscan'

Some files were not shown because too many files have changed in this diff Show More