Compare commits

..

3 Commits

Author SHA1 Message Date
erwanlr
82db02a688 Updates spec for #1342 2019-05-03 14:25:17 +01:00
erwanlr
2c07de8c6b Updates class comment 2019-05-03 14:23:04 +01:00
erwanlr
4b0b8fa624 Fixes #1342 2019-05-03 14:04:50 +01:00
743 changed files with 1224 additions and 248350 deletions

View File

@@ -14,4 +14,3 @@ Dockerfile
*.orig
bin/wpscan-*
.wpscan/
.github/

View File

@@ -1,49 +0,0 @@
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: [2.4, 2.5, 2.6, 2.7]
steps:
- name: Checkout code
uses: actions/checkout@v1
- name: Set up Ruby ${{ matrix.ruby }}
uses: actions/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: Restore GEM cache
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-${{ matrix.ruby }}-gem-${{ hashFiles('**/wpscan.gemspec') }}
restore-keys: |
${{ runner.os }}-${{ matrix.ruby }}-gem-
- name: Install GEMs
run: |
gem install bundler
bundle config force_ruby_platform true
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: rubocop
run: |
bundle exec rubocop
- name: rspec
run: |
bundle exec rspec
- name: Coveralls
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,40 +0,0 @@
name: Ruby Gem
on:
release:
types: [published]
jobs:
build:
name: Build + Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Set up Ruby 2.6
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.x
#- name: Publish to GPR
# run: |
# mkdir -p $HOME/.gem
# touch $HOME/.gem/credentials
# chmod 0600 $HOME/.gem/credentials
# printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
# gem build *.gemspec
# gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
# env:
# GEM_HOST_API_KEY: ${{secrets.GITHUB_TOKEN}}
# OWNER: wpscanteam
- name: Publish to RubyGems
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec
gem push *.gem
env:
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}

5
.rspec
View File

@@ -1,2 +1,3 @@
--require spec_helper
--color
--color
--fail-fast
--require spec_helper

View File

@@ -4,8 +4,12 @@ AllCops:
Exclude:
- '*.gemspec'
- 'vendor/**/*'
Layout/LineLength:
ClassVars:
Enabled: false
LineLength:
Max: 120
MethodLength:
Max: 20
Lint/UriEscapeUnescape:
Enabled: false
Metrics/AbcSize:
@@ -15,20 +19,9 @@ Metrics/BlockLength:
- 'spec/**/*'
Metrics/ClassLength:
Max: 150
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Metrics/CyclomaticComplexity:
Max: 8
Metrics/MethodLength:
Max: 20
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Style/ClassVars:
Enabled: false
Style/Documentation:
Enabled: false
Style/FormatStringToken:
Enabled: false
Style/NumericPredicate:
Exclude:
- 'app/controllers/vuln_api.rb'

View File

@@ -1,18 +1,4 @@
if ENV['GITHUB_ACTION']
require 'simplecov-lcov'
SimpleCov::Formatter::LcovFormatter.config do |c|
c.single_report_path = 'coverage/lcov.info'
c.report_with_single_file = true
end
SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
end
SimpleCov.start do
enable_coverage :branch # Only supported for Ruby >= 2.5
add_filter '/spec/'
add_filter 'helper'
end

33
.travis.yml Normal file
View File

@@ -0,0 +1,33 @@
language: ruby
sudo: false
cache: bundler
rvm:
- 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
- 2.6.3
- ruby-head
before_install:
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
- gem update --system
matrix:
allow_failures:
- rvm: ruby-head
script:
- bundle exec rubocop
- bundle exec rspec
notifications:
email:
- team@wpscan.org

View File

@@ -1,4 +1,4 @@
FROM ruby:2.6.3-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.6.3-alpine
FROM ruby:2.6.2-alpine3.9
LABEL maintainer="WPScan Team <team@wpscan.org>"
RUN adduser -h /wpscan -g WPScan -D wpscan
@@ -38,3 +38,4 @@ USER wpscan
RUN /usr/local/bundle/bin/wpscan --update --verbose
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
CMD ["--help"]

View File

@@ -29,6 +29,8 @@ Example cases which do not require a commercial license, and thus fall under the
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
Free-use Terms and Conditions;
3. Redistribution

View File

@@ -15,8 +15,9 @@
<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://github.com/wpscanteam/wpscan/actions?query=workflow%3ABuild" target="_blank"><img src="https://github.com/wpscanteam/wpscan/workflows/Build/badge.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
@@ -29,7 +30,6 @@
- Curl >= 7.21 - Recommended: latest
- The 7.29 has a segfault
- RubyGems - Recommended: latest
- Nokogiri might require packages to be installed via your package manager depending on your OS, see https://nokogiri.org/tutorials/installing_nokogiri.html
### From RubyGems (Recommended)
@@ -77,65 +77,41 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
# Usage
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings.
If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings. If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'.
For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo)
The DB is located at ~/.wpscan/db
## Vulnerability Database
The WPScan CLI tool uses the [WPVulnDB API](https://wpvulndb.com/api) to retrieve WordPress vulnerability data in real time. For WPScan to retrieve the vulnerability data an API token must be supplied via the `--api-token` option, or via a configuration file, as discussed below. An API token can be obtained by registering an account on [WPVulnDB](https://wpvulndb.com/users/sign_up). Up to 50 API requests per day are given free of charge to registered users. Once the 50 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data. Users can upgrade to paid API usage to increase their API limits within their user profile on [WPVulnDB](https://wpvulndb.com/).
## Load CLI options from file/s
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
- ~/.wpscan/scan.json
- ~/.wpscan/scan.yml
- pwd/.wpscan/scan.json
- pwd/.wpscan/scan.yml
- ~/.wpscan/cli_options.json
- ~/.wpscan/cli_options.yml
- pwd/.wpscan/cli_options.json
- pwd/.wpscan/cli_options.yml
If those files exist, options from the `cli_options` key will be loaded and overridden if found twice.
If those files exist, options from them will be loaded and overridden if found twice.
e.g:
~/.wpscan/scan.yml:
~/.wpscan/cli_options.yml:
```yml
cli_options:
proxy: 'http://127.0.0.1:8080'
verbose: true
proxy: 'http://127.0.0.1:8080'
verbose: true
```
pwd/.wpscan/scan.yml:
pwd/.wpscan/cli_options.yml:
```yml
cli_options:
proxy: 'socks5://127.0.0.1:9090'
url: 'http://target.tld'
proxy: 'socks5://127.0.0.1:9090'
url: 'http://target.tld'
```
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
## Save API Token in a file
The feature mentioned above is useful to keep the API Token in a config file and not have to supply it via the CLI each time. To do so, create the ~/.wpscan/scan.yml file containing the below:
```yml
cli_options:
api_token: YOUR_API_TOKEN
```
## Load APi Token From ENV
The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
## Enumerating usernames
Enumerating usernames
```shell
wpscan --url https://target.tld/ --enumerate u

View File

@@ -6,18 +6,14 @@ exec = []
begin
require 'rubocop/rake_task'
RuboCop::RakeTask.new
exec << :rubocop
rescue LoadError
end
begin
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) { |t| t.rspec_opts = %w{--tag ~slow} }
RSpec::Core::RakeTask.new(:spec)
exec << :spec
rescue LoadError
end

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true
require_relative 'controllers/core'
require_relative 'controllers/vuln_api'
require_relative 'controllers/custom_directories'
require_relative 'controllers/wp_version'
require_relative 'controllers/main_theme'

View File

@@ -18,7 +18,9 @@ module WPScan
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
raise Error::WpContentDirNotDetected unless target.content_dir
return if target.content_dir(ParsedCli.detection_mode)
raise Error::WpContentDirNotDetected
end
end
end

View File

@@ -7,6 +7,15 @@ module WPScan
module Controller
# Enumeration Controller
class Enumeration < CMSScanner::Controller::Base
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 = ParsedCli.enumerate || {}

View File

@@ -11,6 +11,7 @@ module WPScan
end
# @return [ Array<OptParseValidator::OptBase> ]
# rubocop:disable Metrics/MethodLength
def cli_enum_choices
[
OptMultiChoices.new(
@@ -18,10 +19,10 @@ module WPScan
choices: {
vp: OptBoolean.new(['--vulnerable-plugins']),
ap: OptBoolean.new(['--all-plugins']),
p: OptBoolean.new(['--popular-plugins']),
p: OptBoolean.new(['--plugins']),
vt: OptBoolean.new(['--vulnerable-themes']),
at: OptBoolean.new(['--all-themes']),
t: OptBoolean.new(['--popular-themes']),
t: OptBoolean.new(['--themes']),
tt: OptBoolean.new(['--timthumbs']),
cb: OptBoolean.new(['--config-backups']),
dbe: OptBoolean.new(['--db-exports']),
@@ -44,6 +45,7 @@ module WPScan
)
]
end
# rubocop:enable Metrics/MethodLength
# @return [ Array<OptParseValidator::OptBase> ]
def cli_plugins_opts
@@ -65,11 +67,6 @@ module WPScan
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
'or --plugins-detection modes.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
),
OptInteger.new(
['--plugins-threshold THRESHOLD',
'Raise an error when the number of detected plugins via known locations reaches the threshold. ' \
'Set to 0 to ignore the threshold.'], default: 100, advanced: true
)
]
end
@@ -94,11 +91,6 @@ module WPScan
'Use the supplied mode to check themes versions instead of the --detection-mode ' \
'or --themes-detection modes.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
),
OptInteger.new(
['--themes-threshold THRESHOLD',
'Raise an error when the number of detected themes via known locations reaches the threshold. ' \
'Set to 0 to ignore the threshold.'], default: 20, advanced: true
)
]
end

View File

@@ -56,13 +56,12 @@ module WPScan
#
# @return [ Boolean ] Wether or not to enumerate the plugins
def enum_plugins?(opts)
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
end
def enum_plugins
opts = default_opts('plugins').merge(
list: plugins_list_from_opts(ParsedCli.options),
threshold: ParsedCli.plugins_threshold,
sort: true
)
@@ -92,7 +91,7 @@ module WPScan
if opts[:enumerate][:all_plugins]
DB::Plugins.all_slugs
elsif opts[:enumerate][:popular_plugins]
elsif opts[:enumerate][:plugins]
DB::Plugins.popular_slugs
else
DB::Plugins.vulnerable_slugs
@@ -103,13 +102,12 @@ module WPScan
#
# @return [ Boolean ] Wether or not to enumerate the themes
def enum_themes?(opts)
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
end
def enum_themes
opts = default_opts('themes').merge(
list: themes_list_from_opts(ParsedCli.options),
threshold: ParsedCli.themes_threshold,
sort: true
)
@@ -139,7 +137,7 @@ module WPScan
if opts[:enumerate][:all_themes]
DB::Themes.all_slugs
elsif opts[:enumerate][:popular_themes]
elsif opts[:enumerate][:themes]
DB::Themes.popular_slugs
else
DB::Themes.vulnerable_slugs

View File

@@ -65,43 +65,30 @@ module WPScan
case ParsedCli.password_attack
when :wp_login
Finders::Passwords::WpLogin.new(target)
WPScan::Finders::Passwords::WpLogin.new(target)
when :xmlrpc
raise Error::XMLRPCNotDetected unless xmlrpc
Finders::Passwords::XMLRPC.new(xmlrpc)
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
when :xmlrpc_multicall
raise Error::XMLRPCNotDetected unless xmlrpc
Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
end
end
# @return [ Boolean ]
def xmlrpc_get_users_blogs_enabled?
if xmlrpc&.enabled? &&
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
.run.body !~ /XML\-RPC services are disabled/
true
else
false
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
end
end
# @return [ CMSScanner::Finders::Finder ]
def attacker_from_automatic_detection
if xmlrpc_get_users_blogs_enabled?
if xmlrpc&.enabled? && xmlrpc.available_methods.include?('wp.getUsersBlogs')
wp_version = target.wp_version
if wp_version && wp_version < '4.4'
Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
else
Finders::Passwords::XMLRPC.new(xmlrpc)
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
end
else
Finders::Passwords::WpLogin.new(target)
WPScan::Finders::Passwords::WpLogin.new(target)
end
end

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to handle the API token
class VulnApi < CMSScanner::Controller::Base
ENV_KEY = 'WPSCAN_API_TOKEN'
def cli_options
[
OptString.new(['--api-token TOKEN', 'The WPVulnDB API Token to display vulnerability data'])
]
end
def before_scan
return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
api_status = DB::VulnApi.status
raise Error::InvalidApiToken if api_status['error']
raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
raise api_status['http_error'] if api_status['http_error']
end
def after_scan
output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
end
end
end
end

View File

@@ -17,7 +17,7 @@ module WPScan
end
def before_scan
DB::DynamicFinders::Wordpress.create_versions_finders
WPScan::DB::DynamicFinders::Wordpress.create_versions_finders
end
def run

View File

@@ -4,6 +4,7 @@ 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
@@ -19,9 +20,9 @@ module WPScan
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
if res.effective_url.end_with?('.zip')
next unless %r{\Aapplication/zip}i.match?(res.headers['Content-Type'])
next unless res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
else
next unless SQL_PATTERN.match?(res.body)
next unless res.body =~ SQL_PATTERN
end
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
@@ -40,7 +41,7 @@ module WPScan
# @return [ Hash ]
def potential_urls(opts = {})
urls = {}
domain_name = PublicSuffix.domain(target.uri.host)[/(^[\w|-]+)/, 1]
domain_name = target.uri.host[/(^[\w|-]+)/, 1]
File.open(opts[:list]).each_with_index do |path, index|
path.gsub!('{domain_name}', domain_name)

View File

@@ -4,7 +4,7 @@ require_relative 'interesting_findings/readme'
require_relative 'interesting_findings/wp_cron'
require_relative 'interesting_findings/multisite'
require_relative 'interesting_findings/debug_log'
require_relative 'interesting_findings/backup_db'
require_relative 'interesting_findings/plugin_backup_folders'
require_relative 'interesting_findings/mu_plugins'
require_relative 'interesting_findings/registration'
require_relative 'interesting_findings/tmm_db_migrate'
@@ -24,7 +24,7 @@ module WPScan
super(target)
%w[
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
Readme DebugLog FullPathDisclosure PluginBackupFolders DuplicatorInstallerLog
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
UploadSQLDump EmergencyPwdResetScript WPCron
].each do |f|

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# BackupDB finder
class BackupDB < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'wp-content/backup-db/'
res = target.head_and_get(path, [200, 403])
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
Model::BackupDB.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path)
)
end
end
end
end
end

View File

@@ -11,7 +11,11 @@ module WPScan
return unless target.debug_log?(path)
Model::DebugLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
Model::DebugLog.new(
target.url(path),
confidence: 100, found_by: DIRECT_ACCESS,
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
)
end
end
end

View File

@@ -9,9 +9,14 @@ module WPScan
def aggressive(_opts = {})
path = 'installer-log.txt'
return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body)
return unless target.head_and_get(path).body =~ /DUPLICATOR INSTALL-LOG/
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
Model::DuplicatorInstallerLog.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS,
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
)
end
end
end

View File

@@ -15,7 +15,10 @@ module WPScan
Model::EmergencyPwdResetScript.new(
target.url(path),
confidence: /password/i.match?(res.body) ? 100 : 40,
found_by: DIRECT_ACCESS
found_by: DIRECT_ACCESS,
references: {
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
}
)
end
end

View File

@@ -16,7 +16,8 @@ module WPScan
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS,
interesting_entries: fpd_entries
interesting_entries: fpd_entries,
references: { url: 'https://www.owasp.org/index.php/Full_Path_Disclosure' }
)
end
end

View File

@@ -9,14 +9,20 @@ module WPScan
def passive(_opts = {})
pattern = %r{#{target.content_dir}/mu\-plugins/}i
target.in_scope_uris(target.homepage_res, '(//@href|//@src)[contains(., "mu-plugins")]') do |uri|
next unless uri.path&.match?(pattern)
target.in_scope_uris(target.homepage_res) do |uri|
next unless uri.path =~ pattern
url = target.url('wp-content/mu-plugins/')
target.mu_plugins = true
return Model::MuPlugins.new(url, confidence: 70, found_by: 'URLs In Homepage (Passive Detection)')
return Model::MuPlugins.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
@@ -31,7 +37,13 @@ module WPScan
target.mu_plugins = true
Model::MuPlugins.new(url, confidence: 80, found_by: DIRECT_ACCESS)
Model::MuPlugins.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

View File

@@ -17,7 +17,13 @@ module WPScan
target.multisite = true
Model::Multisite.new(url, confidence: 100, found_by: DIRECT_ACCESS)
Model::Multisite.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

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# Known Backup Folders from Plugin finder
class PluginBackupFolders < CMSScanner::Finders::Finder
PATHS = %w[wp-content/backup-db/ wp-content/backups-dup-pro/ wp-content/updraft/].freeze
# @return [ InterestingFinding ]
def aggressive(_opts = {})
found = []
PATHS.each do |path|
res = target.head_and_get(path, [200, 403])
next unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
found << Model::PluginBackupFolder.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path),
references: { url: ['https://github.com/wpscanteam/wpscan/issues/422',
'https://github.com/wpscanteam/wpscan/issues/1342'] }
)
end
found
end
end
end
end
end

View File

@@ -20,7 +20,12 @@ module WPScan
target.registration_enabled = true
Model::Registration.new(res.effective_url, confidence: 100, found_by: DIRECT_ACCESS)
Model::Registration.new(
res.effective_url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: "Registration is enabled: #{res.effective_url}"
)
end
end
end

View File

@@ -13,7 +13,12 @@ module WPScan
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
Model::TmmDbMigrate.new(url, confidence: 100, found_by: DIRECT_ACCESS)
Model::TmmDbMigrate.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
references: { packetstorm: 131_957 }
)
end
end
end

View File

@@ -13,7 +13,12 @@ module WPScan
url = target.url(path)
Model::UploadDirectoryListing.new(url, confidence: 100, found_by: DIRECT_ACCESS)
Model::UploadDirectoryListing.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: "Upload directory has listing enabled: #{url}"
)
end
end
end

View File

@@ -12,9 +12,13 @@ module WPScan
path = 'wp-content/uploads/dump.sql'
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
return unless SQL_PATTERN.match?(res.body)
return unless res.body =~ SQL_PATTERN
Model::UploadSQLDump.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
Model::UploadSQLDump.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS
)
end
end
end

View File

@@ -11,7 +11,17 @@ module WPScan
return unless res.code == 200
Model::WPCron.new(wp_cron_url, confidence: 60, found_by: DIRECT_ACCESS)
Model::WPCron.new(
wp_cron_url,
confidence: 60,
found_by: DIRECT_ACCESS,
references: {
url: [
'https://www.iplocation.net/defend-wordpress-from-ddos',
'https://github.com/wpscanteam/wpscan/issues/1299'
]
}
)
end
def wp_cron_url

View File

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

View File

@@ -3,9 +3,9 @@
module WPScan
module Finders
module MainTheme
# From the CSS style in the homepage
class CssStyleInHomepage < CMSScanner::Finders::Finder
include Finders::WpItems::UrlsInPage # To have the item_code_pattern method available here
# From the css style
class CssStyle < CMSScanner::Finders::Finder
include Finders::WpItems::URLsInHomepage
def create_theme(slug, style_url, opts)
Model::Theme.new(
@@ -20,7 +20,7 @@ module WPScan
end
def passive_from_css_href(res, opts)
target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri|
target.in_scope_uris(res, '//style/@src|//link/@href') do |uri|
next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
return create_theme(Regexp.last_match[1], uri.to_s, opts)

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ module WPScan
def valid_credentials?(response)
response.code == 302 &&
[*response.headers['Set-Cookie']]&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
response.headers['Set-Cookie']&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
end
def errored_response?(response)

View File

@@ -8,7 +8,7 @@ module WPScan
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
def login_request(username, password)
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0)
target.method_call('wp.getUsersBlogs', [username, password])
end
def valid_credentials?(response)

View File

@@ -19,7 +19,7 @@ module WPScan
end
end
target.multi_call(methods, cache_ttl: 0).run
target.multi_call(methods).run
end
# @param [ Array<Model::User> ] users

View File

@@ -13,15 +13,25 @@ module WPScan
def initialize(plugin)
finders << PluginVersion::Readme.new(plugin)
create_and_load_dynamic_versions_finders(plugin)
load_specific_finders(plugin)
end
# Create the dynamic version finders related to the plugin and register them
# Load the finders associated with the plugin
#
# @param [ Model::Plugin ] plugin
def create_and_load_dynamic_versions_finders(plugin)
DB::DynamicFinders::Plugin.create_versions_finders(plugin.slug).each do |finder|
finders << finder.new(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

View File

@@ -11,7 +11,7 @@ module WPScan
# The target(plugin)#readme_url can't be used directly here
# as if the --detection-mode is passive, it will always return nil
target.potential_readme_filenames.each do |file|
Model::WpItem::READMES.each do |file|
res = target.head_and_get(file)
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
@@ -52,7 +52,7 @@ module WPScan
number = Regexp.last_match[1]
number if /[0-9]+/.match?(number)
number if number =~ /[0-9]+/
end
# @param [ String ] body

View File

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

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from Dynamic Finder 'BodyPattern'
class BodyPattern < Finders::DynamicFinder::WpItems::Finder
class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts The options from the #passive, #aggressive methods
@@ -15,7 +15,7 @@ module WPScan
#
# @return [ Plugin ] The detected plugin in the response, related to the config
def process_response(opts, response, slug, klass, config)
return unless response.body&.match?(config['pattern'])
return unless response.body =~ config['pattern']
Model::Plugin.new(
slug,

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from the Dynamic Finder 'Comment'
class Comment < Finders::DynamicFinder::WpItems::Finder
class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts The options from the #passive, #aggressive methods
@@ -18,7 +18,7 @@ module WPScan
response.html.xpath(config['xpath'] || '//comment()').each do |node|
comment = node.text.to_s.strip
next unless comment&.match?(config['pattern'])
next unless comment =~ config['pattern']
return Model::Plugin.new(
slug,

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from Dynamic Finder 'ConfigParser'
class ConfigParser < Finders::DynamicFinder::WpItems::Finder
class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 40
# @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from Dynamic Finder 'HeaderPattern'
class HeaderPattern < Finders::DynamicFinder::WpItems::Finder
class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from the Dynamic Finder 'JavascriptVar'
class JavascriptVar < Finders::DynamicFinder::WpItems::Finder
class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 60
# @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -19,14 +19,8 @@ module WPScan
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
finding_opts = opts.merge(found_by: found_by,
confidence: 80,
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
found << Model::Plugin.new(slug, target, finding_opts)
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
end
found

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from Dynamic Finder 'QueryParameter'
class QueryParameter < Finders::DynamicFinder::WpItems::Finder
class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 10
def passive(_opts = {})

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module Plugins
# Plugins finder from the Dynamic Finder 'Xpath'
class Xpath < Finders::DynamicFinder::WpItems::Finder
class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 40
# @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -16,15 +16,25 @@ module WPScan
ThemeVersion::Style.new(theme) <<
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
create_and_load_dynamic_versions_finders(theme)
load_specific_finders(theme)
end
# Create the dynamic version finders related to the theme and register them
# Load the finders associated with the theme
#
# @param [ Model::Theme ] theme
def create_and_load_dynamic_versions_finders(theme)
DB::DynamicFinders::Theme.create_versions_finders(theme.slug).each do |finder|
finders << finder.new(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

View File

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

View File

@@ -19,14 +19,8 @@ module WPScan
def aggressive(opts = {})
found = []
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
finding_opts = opts.merge(found_by: found_by,
confidence: 80,
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
found << Model::Theme.new(slug, target, finding_opts)
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
end
found

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ module WPScan
found = []
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
next unless /no image specified/i.match?(res.body)
next unless res.body =~ /no image specified/i
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
end

View File

@@ -71,13 +71,11 @@ module WPScan
return username, 'Display Name', 50 if username
end
# @param [ String, Addressable::URI ] uri
# @param [ String ] url
#
# @return [ String, nil ]
def username_from_author_url(uri)
uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
uri.path[%r{/author/([^/\b]+)/?}i, 1]
def username_from_author_url(url)
url[%r{/author/([^/\b]+)/?}i, 1]
end
# @param [ Typhoeus::Response ] res
@@ -85,12 +83,12 @@ module WPScan
# @return [ String, nil ] The username found
def username_from_response(res)
# Permalink enabled
target.in_scope_uris(res, '//@href[contains(., "author/")]') do |uri|
username = username_from_author_url(uri)
target.in_scope_uris(res, '//link/@href|//a/@href') do |uri|
username = username_from_author_url(uri.to_s)
return username if username
end
# No permalink, TODO Maybe use xpath to extract the classes ?
# No permalink
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
end
@@ -99,12 +97,9 @@ module WPScan
# @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|
text = node.text.to_s.strip
return text unless text.empty?
return node.text.to_s
end
# WP < 3.0

View File

@@ -45,7 +45,7 @@ module WPScan
def potential_usernames(res)
usernames = []
target.in_scope_uris(res, '//a/@href[contains(., "author")]') do |uri, node|
target.in_scope_uris(res, '//a/@href') do |uri, node|
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
elsif /author=[0-9]+/.match?(uri.query)

View File

@@ -24,7 +24,7 @@ module WPScan
return found if error.empty? # Protection plugin / error disabled
next unless /The password you entered for the username|Incorrect Password/i.match?(error)
next unless error =~ /The password you entered for the username|Incorrect Password/i
found << Model::User.new(username, found_by: found_by, confidence: 100)
end

View File

@@ -34,8 +34,6 @@ module WPScan
def user_details_from_oembed_data(oembed_data)
return unless oembed_data
oembed_data = oembed_data.first if oembed_data.is_a?(Array)
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?

View File

@@ -6,7 +6,7 @@ module WPScan
# 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 < Finders::WpVersion::RSSGenerator
class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
def process_urls(urls, _opts = {})
found = []

View File

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

View File

@@ -4,24 +4,18 @@ module WPScan
module Finders
module WpItems
# URLs In Homepage Module to use in plugins & themes finders
module UrlsInPage
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 page
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
def items_from_links(type, uniq = true)
found = []
xpath = format(
'(//@href|//@src|//@data-src)[contains(., "%s")]',
type == 'plugins' ? target.plugins_dir : target.content_dir
)
target.in_scope_uris(page_res, xpath) do |uri|
target.in_scope_uris(target.homepage_res) do |uri|
next unless uri.to_s =~ item_attribute_pattern(type)
slug = Regexp.last_match[1]&.strip
found << slug unless slug&.empty?
found << Regexp.last_match[1]
end
uniq ? found.uniq.sort : found.sort
@@ -34,7 +28,7 @@ module WPScan
def items_from_codes(type, uniq = true)
found = []
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
target.homepage_res.html.css('script,style').each do |tag|
code = tag.text.to_s
next if code.empty?
@@ -48,7 +42,7 @@ module WPScan
#
# @return [ Regexp ]
def item_attribute_pattern(type)
@item_attribute_pattern ||= %r{#{item_url_pattern(type)}([^/]+)/}i
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
end
# @param [ String ] type
@@ -65,7 +59,7 @@ module WPScan
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(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
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

View File

@@ -28,7 +28,7 @@ module WPScan
# @param [ WPScan::Target ] target
def initialize(target)
(%w[RSSGenerator AtomGenerator RDFGenerator] +
DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
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)

View File

@@ -28,7 +28,7 @@ module WPScan
end
def passive_urls_xpath
'//a[contains(@href, "/rdf")]/@href'
'//a[contains(@href, "rdf")]/@href'
end
def aggressive_urls(_opts = {})

View File

@@ -8,110 +8,45 @@ module WPScan
end
#
# Some classes are empty for the #type to be correctly displayed (as taken from the self.class from the parent)
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
#
class BackupDB < InterestingFinding
# @return [ Hash ]
def references
@references ||= { url: ['https://github.com/wpscanteam/wpscan/issues/422'] }
end
class PluginBackupFolder < InterestingFinding
end
class DebugLog < InterestingFinding
# @ return [ Hash ]
def references
@references ||= { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
end
end
class DuplicatorInstallerLog < InterestingFinding
# @return [ Hash ]
def references
@references ||= { url: ['https://www.exploit-db.com/ghdb/3981/'] }
end
end
class EmergencyPwdResetScript < InterestingFinding
def references
@references ||= {
url: ['https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script']
}
end
end
class FullPathDisclosure < InterestingFinding
# @return [ Hash ]
def references
@references ||= { url: ['https://www.owasp.org/index.php/Full_Path_Disclosure'] }
end
end
class MuPlugins < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "This site has 'Must Use Plugins': #{url}"
end
# @return [ Hash ]
def references
@references ||= { url: ['http://codex.wordpress.org/Must_Use_Plugins'] }
end
end
class Multisite < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= 'This site seems to be a multisite'
end
# @return [ Hash ]
def references
@references ||= { url: ['http://codex.wordpress.org/Glossary#Multisite'] }
end
end
class Readme < InterestingFinding
end
class Registration < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "Registration is enabled: #{url}"
end
end
class TmmDbMigrate < InterestingFinding
# @return [ Hash ]
def references
@references ||= { packetstorm: [131_957] }
end
end
class UploadDirectoryListing < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "Upload directory has listing enabled: #{url}"
end
end
class UploadSQLDump < InterestingFinding
end
class WPCron < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "The external WP-Cron seems to be enabled: #{url}"
end
# @return [ Hash ]
def references
@references ||= {
url: [
'https://www.iplocation.net/defend-wordpress-from-ddos',
'https://github.com/wpscanteam/wpscan/issues/1299'
]
}
end
end
end
end

View File

@@ -15,16 +15,9 @@ module WPScan
@uri = Addressable::URI.parse(blog.url(path_from_blog))
end
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= db_data.empty? ? DB::Plugin.metadata_at(slug) : db_data
end
# @return [ Hash ]
# @return [ JSON ]
def db_data
@db_data ||= DB::VulnApi.plugin_data(slug)
@db_data ||= DB::Plugin.db_data(slug)
end
# @param [ Hash ] opts
@@ -35,11 +28,6 @@ module WPScan
@version
end
# @return [ Array<String> ]
def potential_readme_filenames
@potential_readme_filenames ||= [*(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)]
end
end
end
end

View File

@@ -21,16 +21,9 @@ module WPScan
parse_style
end
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ JSON ]
def metadata
@metadata ||= db_data.empty? ? DB::Theme.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data
@db_data ||= DB::VulnApi.theme_data(slug)
@db_data ||= DB::Theme.db_data(slug)
end
# @param [ Hash ] opts
@@ -101,7 +94,7 @@ module WPScan
#
# @return [ String ]
def parse_style_tag(body, tag)
value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n\*]+)/i, 1]
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
value && !value.strip.empty? ? value.strip : nil
end

View File

@@ -9,12 +9,11 @@ module WPScan
include CMSScanner::Target::Platform::PHP
include CMSScanner::Target::Server::Generic
# Most common readme filenames, based on checking all public plugins and themes.
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
delegate :homepage_res, :error_404_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog
# @param [ String ] slug The plugin/theme slug
# @param [ Target ] blog The targeted blog
@@ -23,7 +22,7 @@ module WPScan
# @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 = Addressable::URI.unencode(slug)
@slug = URI.decode(slug)
@blog = blog
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
@@ -60,18 +59,18 @@ module WPScan
# @return [ String ]
def latest_version
@latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil
@latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
end
# Not used anywhere ATM
# @return [ Boolean ]
def popular?
@popular ||= metadata['popular'] ? true : false
@popular ||= db_data['popular']
end
# @return [ String ]
def last_updated
@last_updated ||= metadata['last_updated']
@last_updated ||= db_data['last_updated']
end
# @return [ Boolean ]
@@ -83,6 +82,11 @@ module WPScan
end
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 ]
@@ -90,7 +94,7 @@ module WPScan
return unless @uri
return @uri.to_s unless path
@uri.join(Addressable::URI.encode(path)).to_s
@uri.join(URI.encode(path)).to_s
end
# @return [ Boolean ]
@@ -113,7 +117,7 @@ module WPScan
return @readme_url unless @readme_url.nil?
potential_readme_filenames.each do |path|
READMES.each do |path|
t_url = url(path)
return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200
@@ -122,10 +126,6 @@ module WPScan
@readme_url = false
end
def potential_readme_filenames
@potential_readme_filenames ||= READMES
end
# @param [ String ] path
# @param [ Hash ] params The request params
#
@@ -161,7 +161,7 @@ module WPScan
# @return [ Typhoeus::Response ]
def head_and_get(path, codes = [200], params = {})
final_path = +@path_from_blog
final_path << path unless path.nil?
final_path << URI.encode(path) unless path.nil?
blog.head_and_get(final_path, codes, params)
end

View File

@@ -35,16 +35,9 @@ module WPScan
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= db_data.empty? ? DB::Version.metadata_at(number) : db_data
end
# @return [ Hash ]
# @return [ JSON ]
def db_data
@db_data ||= DB::VulnApi.wordpress_data(number)
@db_data ||= DB::Version.db_data(number)
end
# @return [ Array<Vulnerability> ]
@@ -62,12 +55,12 @@ module WPScan
# @return [ String ]
def release_date
@release_date ||= metadata['release_date'] || 'Unknown'
@release_date ||= db_data['release_date'] || 'Unknown'
end
# @return [ String ]
def status
@status ||= metadata['status'] || 'Unknown'
@status ||= db_data['status'] || 'Unknown'
end
end
end

View File

@@ -8,7 +8,7 @@ module WPScan
# @return [ Hash ]
def references
@references ||= {
{
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
metasploit: [
'auxiliary/scanner/http/wordpress_ghost_scanner',

View File

@@ -1,14 +1,14 @@
_______________________________________________________________
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
WordPress Security Scanner by the WPScan Team
Version <%= WPScan::VERSION %>
<%= ' ' * ((63 - WPScan::DB::Sponsor.text.length)/2) + WPScan::DB::Sponsor.text %>
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
WordPress Security Scanner by the WPScan Team
Version <%= WPScan::VERSION %>
Sponsored by Sucuri - https://sucuri.net
@_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
_______________________________________________________________

View File

@@ -5,7 +5,7 @@
<%= notice_icon %> Config Backup(s) Identified:
<% @config_backups.each do |config_backup| -%>
<%= critical_icon %> <%= config_backup %>
<%= info_icon %> <%= config_backup %>
<%= render('@finding', item: config_backup) -%>
<% end -%>
<% end %>

View File

@@ -5,7 +5,7 @@
<%= notice_icon %> Db Export(s) Identified:
<% @db_exports.each do |db_export| -%>
<%= critical_icon %> <%= db_export %>
<%= info_icon %> <%= db_export %>
<%= render('@finding', item: db_export) -%>
<% end -%>
<% end %>

View File

@@ -1,4 +1,4 @@
| Found By: <%= @item.found_by %>
| Detected By: <%= @item.found_by %>
<% @item.interesting_entries.each do |entry| -%>
| - <%= entry %>
<% end -%>

View File

@@ -1,13 +0,0 @@
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
<%= critical_icon %> WPVulnDB API, <%= @status['http_error'].to_s %>
<% else -%>
<%= info_icon %> WPVulnDB API OK
| Plan: <%= @status['plan'] %>
| Requests Done (during the scan): <%= @api_requests %>
| Requests Remaining: <%= @status['requests_remaining'] %>
<% end -%>
<% else -%>
<%= warning_icon %> No WPVulnDB API Token given, as a result vulnerability data has not been output.
<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up
<% end -%>

View File

@@ -5,7 +5,7 @@
"@_WPScan_",
"@ethicalhack3r",
"@erwan_lr",
"@firefart"
"@_FireFart_"
],
"sponsor": <%= WPScan::DB::Sponsor.text.to_json %>
"sponsored_by": "Sucuri - https://sucuri.net"
},

View File

@@ -11,10 +11,9 @@
}<% unless index == last_index %>,<% end -%>
<% end -%>
<% end -%>
}
<% if @item.respond_to?(:vulnerabilities) -%>
,"vulnerabilities": [
<% unless (vulns = @item.vulnerabilities).empty? -%>
},
"vulnerabilities": [
<% if @item.respond_to?(:vulnerabilities) && !(vulns = @item.vulnerabilities).empty? -%>
<% last_index = vulns.size - 1 -%>
<% vulns.each_with_index do |v, index| -%>
{
@@ -24,5 +23,4 @@
}<% unless index == last_index -%>,<% end -%>
<% end -%>
<% end -%>
]
<% end -%>
]

View File

@@ -1,13 +0,0 @@
"vuln_api": {
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
"http_error": <%= @status['http_error'].to_s.to_json %>
<% else -%>
"plan": <%= @status['plan'].to_json %>,
"requests_done_during_scan": <%= @api_requests.to_json %>,
"requests_remaining": <%= @status['requests_remaining'].to_json %>
<% end -%>
<% else -%>
"error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up"
<% end -%>
},

View File

@@ -5,7 +5,6 @@ require 'wpscan'
WPScan::Scan.new do |s|
s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new <<

View File

@@ -7,7 +7,6 @@ require 'wpscan'
report = MemoryProfiler.report(top: 15) do
WPScan::Scan.new do |s|
s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new <<

View File

@@ -12,7 +12,6 @@ StackProf.run(mode: :cpu, out: '/tmp/stackprof-cpu.dump', interval: 500) do
# require_relative 'wpscan' doesn't work
WPScan::Scan.new do |s|
s.controllers <<
WPScan::Controller::VulnApi.new <<
WPScan::Controller::CustomDirectories.new <<
WPScan::Controller::InterestingFindings.new <<
WPScan::Controller::WpVersion.new <<

View File

@@ -13,8 +13,7 @@ require 'uri'
require 'time'
require 'readline'
require 'securerandom'
# Monkey Patches/Fixes/Override
require 'wpscan/typhoeus/response' # Adds a from_vuln_api? method
# Custom Libs
require 'wpscan/helper'
require 'wpscan/db'
@@ -39,28 +38,12 @@ module WPScan
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db')
Typhoeus.on_complete do |response|
next if response.cached? || !response.from_vuln_api?
self.api_requests += 1
end
# Override, otherwise it would be returned as 'wp_scan'
#
# @return [ String ]
def self.app_name
'wpscan'
end
# @return [ Integer ]
def self.api_requests
@@api_requests ||= 0
end
# @param [ Integer ] value
def self.api_requests=(value)
@@api_requests = value
end
end
require "#{WPScan::APP_DIR}/app"

View File

@@ -7,7 +7,7 @@ module WPScan
# @return [ String ]
def default_user_agent
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.org/)"
"WPScan v#{VERSION} (https://wpscan.org/)"
end
end
end

View File

@@ -7,12 +7,9 @@ require_relative 'db/plugins'
require_relative 'db/themes'
require_relative 'db/plugin'
require_relative 'db/theme'
require_relative 'db/sponsor'
require_relative 'db/wp_version'
require_relative 'db/fingerprints'
require_relative 'db/vuln_api'
require_relative 'db/dynamic_finders/base'
require_relative 'db/dynamic_finders/plugin'
require_relative 'db/dynamic_finders/theme'

View File

@@ -5,19 +5,18 @@ module WPScan
module DynamicFinders
class Base
# @return [ String ]
def self.df_file
@df_file ||= DB_DIR.join('dynamic_finders.yml').to_s
def self.db_file
@db_file ||= DB_DIR.join('dynamic_finders.yml').to_s
end
# @return [ Hash ]
def self.all_df_data
@all_df_data ||= YAML.safe_load(File.read(df_file), [Regexp])
def self.db_data
# true allows aliases to be loaded
@db_data ||= YAML.safe_load(File.read(db_file), [Regexp], [], true)
end
# @return [ Array<Symbol> ]
def self.allowed_classes
# The Readme is not put in there as it's not a Real DF, but rather using the DF system
# to get the list of potential filenames for a given slug
@allowed_classes ||= %i[Comment Xpath HeaderPattern BodyPattern JavascriptVar QueryParameter ConfigParser]
end

View File

@@ -5,8 +5,8 @@ module WPScan
module DynamicFinders
class Plugin < Base
# @return [ Hash ]
def self.df_data
@df_data ||= all_df_data['plugins'] || {}
def self.db_data
@db_data ||= super['plugins'] || {}
end
def self.version_finder_module
@@ -21,7 +21,7 @@ module WPScan
return configs unless allowed_classes.include?(finder_class)
df_data.each do |slug, finders|
db_data.each do |slug, finders|
# Quite sure better can be done with some kind of logic statement in the select
fs = if aggressive
finders.reject { |_f, c| c['path'].nil? }
@@ -48,7 +48,7 @@ module WPScan
@versions_finders_configs = {}
df_data.each do |slug, finders|
db_data.each do |slug, finders|
finders.each do |finder_name, config|
next unless config.key?('version')
@@ -73,33 +73,23 @@ module WPScan
version_finder_module.const_get(constant_name)
end
# Create the dynamic finders related to the given slug, and return the created classes
#
# @param [ String ] slug
#
# @return [ Array<Class> ] The created classes
def self.create_versions_finders(slug)
created = []
mod = maybe_create_module(slug)
def self.create_versions_finders
versions_finders_configs.each do |slug, finders|
mod = maybe_create_module(slug)
versions_finders_configs[slug]&.each do |finder_class, config|
klass = config['class'] || finder_class
finders.each do |finder_class, config|
klass = config['class'] || finder_class
# Instead of raising exceptions, skip unallowed/already defined finders
# So that, when new DF configs are put in the .yml
# users with old version of WPScan will still be able to scan blogs
# when updating the DB but not the tool
# Instead of raising exceptions, skip unallowed/already defined finders
# So that, when new DF configs are put in the .yml
# users with old version of WPScan will still be able to scan blogs
# when updating the DB but not the tool
next if mod.constants.include?(finder_class.to_sym) ||
!allowed_classes.include?(klass.to_sym)
next unless allowed_classes.include?(klass.to_sym)
created << if mod.constants.include?(finder_class.to_sym)
mod.const_get(finder_class.to_sym)
else
version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
end
version_finder_super_class(klass).create_child_class(mod, finder_class.to_sym, config)
end
end
created
end
# The idea here would be to check if the class exist in

View File

@@ -5,8 +5,8 @@ module WPScan
module DynamicFinders
class Theme < Plugin
# @return [ Hash ]
def self.df_data
@df_data ||= all_df_data['themes'] || {}
def self.db_data
@db_data ||= super['themes'] || {}
end
def self.version_finder_module

View File

@@ -5,8 +5,8 @@ module WPScan
module DynamicFinders
class Wordpress < Base
# @return [ Hash ]
def self.df_data
@df_data ||= all_df_data['wordpress'] || {}
def self.db_data
@db_data ||= super['wordpress'] || {}
end
# @return [ Constant ]
@@ -30,9 +30,9 @@ module WPScan
return configs unless allowed_classes.include?(finder_class)
finders = if aggressive
df_data.reject { |_f, c| c['path'].nil? }
db_data.reject { |_f, c| c['path'].nil? }
else
df_data.select { |_f, c| c['path'].nil? }
db_data.select { |_f, c| c['path'].nil? }
end
finders.each do |finder_name, config|
@@ -48,7 +48,7 @@ module WPScan
# @return [ Hash ]
def self.versions_finders_configs
@versions_finders_configs ||= df_data.select { |_finder_name, config| config.key?('version') }
@versions_finders_configs ||= db_data.select { |_finder_name, config| config.key?('version') }
end
def self.create_versions_finders

View File

@@ -4,9 +4,9 @@ module WPScan
module DB
# Plugin DB
class Plugin < WpItem
# @return [ Hash ]
def self.metadata
@metadata ||= super['plugins'] || {}
# @return [ String ]
def self.db_file
@db_file ||= DB_DIR.join('plugins.json').to_s
end
end
end

View File

@@ -5,8 +5,8 @@ module WPScan
# WP Plugins
class Plugins < WpItems
# @return [ JSON ]
def self.metadata
Plugin.metadata
def self.db
Plugin.db
end
end
end

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
module WPScan
module DB
class Sponsor
# @return [ Hash ]
def self.text
@text ||= file_path.exist? ? File.read(file_path).chomp : ''
end
def self.file_path
@file_path ||= DB_DIR.join('sponsor.txt')
end
end
end
end

View File

@@ -4,9 +4,9 @@ module WPScan
module DB
# Theme DB
class Theme < WpItem
# @return [ Hash ]
def self.metadata
@metadata ||= super['themes'] || {}
# @return [ String ]
def self.db_file
@db_file ||= DB_DIR.join('themes.json').to_s
end
end
end

View File

@@ -5,8 +5,8 @@ module WPScan
# WP Themes
class Themes < WpItems
# @return [ JSON ]
def self.metadata
Theme.metadata
def self.db
Theme.db
end
end
end

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