Compare commits

...

89 Commits

Author SHA1 Message Date
erwanlr
4be3f17ae4 Bumps version 2019-11-05 19:19:18 +00:00
erwanlr
f24e7be264 Updates dep 2019-11-05 18:47:41 +00:00
erwanlr
9adc26445d Adds DFs 2019-11-02 11:47:11 +00:00
erwanlr
353e7dcbb9 Removes ruby-head from Travis 2019-11-01 17:47:05 +00:00
erwanlr
430e65c12e Updates cms_scanner dep 2019-11-01 16:43:13 +00:00
erwanlr
1aa242a9d8 Avoids duplicates in finding the same thing diffrent pages 2019-11-01 15:20:46 +00:00
erwanlr
7173cd85fe Changes "Detected By" to "Found By" in cli output for consistency with JSON output 2019-11-01 10:52:31 +00:00
erwanlr
b95a4f55e3 Adds DFs 2019-11-01 10:32:02 +00:00
erwanlr
6b5e016770 Improves detection of WP Version, Plugins etc by checking 404 2019-11-01 09:47:44 +00:00
erwanlr
85aa9f61cd Center the banner text 2019-10-31 16:42:40 +00:00
erwanlr
5c187002d6 Updates cms_scanner dep 2019-10-31 16:27:41 +00:00
Erwan
9bc373308b Merge pull request #1420 from wpscanteam/dependabot/bundler/rubocop-tw-0.76.0
Update rubocop requirement from ~> 0.75.0 to ~> 0.76.0
2019-10-29 07:52:15 +01:00
dependabot-preview[bot]
cdeb0fc144 Update rubocop requirement from ~> 0.75.0 to ~> 0.76.0
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.75.0...v0.76.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-29 05:26:34 +00:00
ethicalhack3r
f1acdd9389 Remove 'no monetary cost' from LICENSE file 2019-10-28 16:42:15 +01:00
erwanlr
d6fac6a210 Adds DFs 2019-10-26 12:29:41 +01:00
Erwan
007cfb0801 Updates Ruby versions for Travis (now only latest in stable) 2019-10-26 10:58:56 +02:00
erwanlr
1f9829b7c0 Adds DFs 2019-10-19 22:56:38 +01:00
Erwan
e039d22565 Fix #1413 2019-10-17 20:31:28 +02:00
erwanlr
b0775b1610 Adds DFs 2019-10-15 17:18:40 +01:00
erwanlr
0e429700c6 Fixes #1412 2019-10-12 11:48:14 +01:00
erwanlr
af7804ca23 Adds DF 2019-10-12 10:54:21 +01:00
erwanlr
9da326967b Bumps version 2019-10-11 15:07:50 +01:00
erwanlr
62600b3a66 Fixes #1411 2019-10-11 14:32:00 +01:00
erwanlr
b236138fb5 Adds additional specs related to previous commit 2019-10-10 22:57:26 +01:00
erwanlr
40c2e9a54b Fixes non detection of plugins/themes from homepage in some cases 2019-10-10 22:33:19 +01:00
erwanlr
a9062db57f Improves detection speed of plugins/themes from homepage 2019-10-10 21:15:59 +01:00
erwanlr
2621404c5f Removes useless spec comment 2019-10-10 21:03:54 +01:00
erwanlr
c47211ca79 Fixes empty username returned in some cases 2019-10-10 20:34:39 +01:00
erwanlr
e39a192e8d Checks default wp-content dir regardless of detection mode if not found passively 2019-10-10 19:59:09 +01:00
Erwan
d85035d5ef Merge pull request #1407 from wpscanteam/dependabot/bundler/rspec-tw-3.9.0
Update rspec requirement from ~> 3.8.0 to ~> 3.9.0
2019-10-09 12:50:59 +02:00
dependabot-preview[bot]
de09a97343 Update rspec requirement from ~> 3.8.0 to ~> 3.9.0
Updates the requirements on [rspec](https://github.com/rspec/rspec) to permit the latest version.
- [Release notes](https://github.com/rspec/rspec/releases)
- [Commits](https://github.com/rspec/rspec/compare/v3.8.0...v3.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-09 05:48:37 +00:00
erwanlr
a6855345d7 Fixes #1406 2019-10-07 07:03:06 +01:00
erwanlr
a53f88b626 Improves WP detection 2019-10-06 16:51:35 +01:00
erwanlr
7048c82124 Adds DFs 2019-10-06 10:46:12 +01:00
erwanlr
6aa7cda478 Fixes #1404 2019-10-05 20:25:35 +01:00
erwanlr
ff339b9a8c Updates rubocop config 2019-10-03 13:09:14 +01:00
Erwan
8898cc20fe Merge pull request #1403 from wpscanteam/dependabot/bundler/rubocop-performance-tw-1.5.0
Update rubocop-performance requirement from ~> 1.4.0 to ~> 1.5.0
2019-10-02 18:35:11 +02:00
dependabot-preview[bot]
770d1da280 Update rubocop-performance requirement from ~> 1.4.0 to ~> 1.5.0
Updates the requirements on [rubocop-performance](https://github.com/rubocop-hq/rubocop-performance) to permit the latest version.
- [Release notes](https://github.com/rubocop-hq/rubocop-performance/releases)
- [Changelog](https://github.com/rubocop-hq/rubocop-performance/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop-hq/rubocop-performance/compare/v1.4.0...v1.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-02 06:46:49 +00:00
Erwan
6ba4e8a29b Merge pull request #1402 from wpscanteam/dependabot/bundler/rubocop-tw-0.75.0
Update rubocop requirement from ~> 0.74.0 to ~> 0.75.0
2019-10-02 08:45:36 +02:00
dependabot-preview[bot]
953ca68495 Update rubocop requirement from ~> 0.74.0 to ~> 0.75.0
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.74.0...v0.75.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-01 06:21:21 +00:00
Erwan
4289dfb37d Merge pull request #1401 from wpscanteam/dependabot/bundler/rake-tw-13.0
Update rake requirement from ~> 12.3 to ~> 13.0
2019-09-30 12:37:15 +02:00
dependabot-preview[bot]
4f6f2f436a Update rake requirement from ~> 12.3 to ~> 13.0
Updates the requirements on [rake](https://github.com/ruby/rake) to permit the latest version.
- [Release notes](https://github.com/ruby/rake/releases)
- [Changelog](https://github.com/ruby/rake/blob/master/History.rdoc)
- [Commits](https://github.com/ruby/rake/compare/v12.3.0...v13.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 05:44:30 +00:00
erwanlr
237979a479 Adds DFs 2019-09-28 17:29:35 +01:00
erwanlr
2e48968fd3 Bumps version 2019-09-25 16:00:24 +01:00
erwanlr
9a0c4a5c8f Fixes #1399 2019-09-25 10:02:15 +01:00
Erwan
9a011f0007 Merge pull request #1397 from noplanman/fix_registration_link
Fix broken registration / signup link.
2019-09-25 10:52:06 +02:00
Armando Lüscher
3f907a706f Fix broken registration / signup link. 2019-09-24 23:19:47 +02:00
erwanlr
9446141716 Adds DFs 2019-09-21 10:20:59 +01:00
erwanlr
1994826af8 Bumps version 2019-09-16 13:14:27 +01:00
erwanlr
ab950d6ffc Do not cache login requests - Fixes #1395 2019-09-16 10:37:43 +01:00
erwanlr
b77e611a90 Adds DFs 2019-09-14 10:35:22 +01:00
erwanlr
86f0284894 Updates help to reflect enumeration of popular plugins and themes 2019-09-13 18:10:33 +01:00
erwanlr
9bbe014dfe Merge branch 'master' of github.com:wpscanteam/wpscan 2019-09-13 17:23:19 +01:00
erwanlr
ad92c95500 Fixes crash when API returns HTML data rather than JSON in edge cases 2019-09-13 17:22:26 +01:00
Erwan
d360190382 Adds section for username enumeration in the Readme 2019-09-13 11:31:32 +02:00
ethicalhack3r
1737c8a7f6 Update readme 2019-09-13 11:02:12 +02:00
ethicalhack3r
cde262fd66 Add wpvulndb api info to readme 2019-09-13 10:49:05 +02:00
erwanlr
bd74689079 Bumps version 2019-09-13 08:34:19 +01:00
Erwan
248942bdea Updates Readme (adds link, fixes typo) 2019-09-11 11:29:45 +02:00
erwanlr
d9f203300b Updates deps 2019-09-11 10:19:48 +01:00
erwanlr
aceabc969f Merge branch 'master' into 3.7.0 2019-09-11 10:18:28 +01:00
erwanlr
dedc24d3a7 Adds DFs 2019-09-07 11:55:53 +01:00
erwanlr
6e583e78e8 Gets Sponsor text from db file 2019-09-05 21:47:26 +01:00
erwanlr
c012e83355 Merge branch 'master' into 3.7.0-merged-master 2019-09-05 19:46:16 +01:00
erwanlr
264355d185 Ignores 404 with BodyPattern DF - Ref #1386 2019-09-05 19:41:24 +01:00
erwanlr
fdbfd1ec60 Ref #1386 2019-09-03 12:03:12 +01:00
erwanlr
7a8b27a255 Fixes #1386 2019-09-02 22:09:28 +01:00
erwanlr
ec4bfac98b Adds DFs 2019-09-01 11:38:57 +01:00
erwanlr
c63ffe37c9 Updates deps and Readme 2019-08-30 18:22:25 +01:00
erwanlr
d2f3ce82c9 Fixes specs 2019-08-30 09:28:47 +01:00
erwanlr
3e24a0b0a4 Merge with master 2019-08-30 09:03:31 +01:00
Erwan
1a07e29ff4 Merge pull request #1383 from wpscanteam/dependabot/bundler/webmock-tw-3.7.0
Update webmock requirement from ~> 3.6.0 to ~> 3.7.0
2019-08-28 14:29:22 +02:00
dependabot-preview[bot]
1aa46a8928 Update webmock requirement from ~> 3.6.0 to ~> 3.7.0
Updates the requirements on [webmock](https://github.com/bblimke/webmock) to permit the latest version.
- [Release notes](https://github.com/bblimke/webmock/releases)
- [Changelog](https://github.com/bblimke/webmock/blob/master/CHANGELOG.md)
- [Commits](https://github.com/bblimke/webmock/compare/v3.6.0...v3.7.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 09:01:28 +00:00
erwanlr
d9083f8b5f Fixes spec related to latest changes 2019-08-24 15:17:18 +01:00
erwanlr
23d558a6d7 Updates to CMSScanner 0.5.8 2019-08-24 14:49:56 +01:00
erwanlr
665a5b7b12 Adds DFs 2019-08-24 14:06:52 +01:00
erwanlr
1d73418969 Adds DFs 2019-08-17 10:36:18 +01:00
erwanlr
f67b5e4cc4 Updates deps again 2019-08-13 11:55:57 +01:00
erwanlr
ae2515444f Updates deps 2019-08-13 11:03:34 +01:00
erwanlr
463e77f0a5 VulnAPI Implementation 2019-08-13 10:03:01 +01:00
erwanlr
d7b796b1a7 Adds DFs 2019-08-10 12:22:38 +01:00
erwanlr
9b07d53077 Bumps version 2019-08-06 16:10:21 +01:00
erwanlr
8ee9b2bc31 Fixes #1378 2019-08-06 13:01:22 +01:00
erwanlr
c5989477a4 Adds DFs 2019-08-03 10:56:22 +01:00
Erwan
96d8a4e4f8 Merge pull request #1377 from wpscanteam/dependabot/bundler/rubocop-tw-0.74.0
Update rubocop requirement from ~> 0.73.0 to ~> 0.74.0
2019-08-03 10:19:49 +02:00
dependabot-preview[bot]
e865e11731 Update rubocop requirement from ~> 0.73.0 to ~> 0.74.0
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.73.0...v0.74.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-01 05:31:24 +00:00
erwanlr
84422b10c8 Changes db_data to metadata 2019-07-18 18:52:52 +01:00
erwanlr
d05ad0f8f4 Adds an Api Token controller 2019-07-18 17:40:27 +01:00
erwanlr
3f70ddaffa Switching to dev 2019-07-18 17:31:02 +01:00
277 changed files with 44584 additions and 637 deletions

View File

@@ -4,14 +4,6 @@ AllCops:
Exclude:
- '*.gemspec'
- 'vendor/**/*'
ClassVars:
Enabled: false
LineLength:
Max: 120
MethodLength:
Max: 20
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Lint/UriEscapeUnescape:
Enabled: false
Metrics/AbcSize:
@@ -25,7 +17,18 @@ Metrics/ClassLength:
- 'app/controllers/enumeration/cli_options.rb'
Metrics/CyclomaticComplexity:
Max: 8
Metrics/LineLength:
Max: 120
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

@@ -2,29 +2,12 @@ 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
- 2.4.9
- 2.5.7
- 2.6.5
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

View File

@@ -38,4 +38,3 @@ USER wpscan
RUN /usr/local/bundle/bin/wpscan --update --verbose
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
CMD ["--help"]

View File

@@ -29,8 +29,6 @@ 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

@@ -77,41 +77,60 @@ 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/cli_options.json
- ~/.wpscan/cli_options.yml
- pwd/.wpscan/cli_options.json
- pwd/.wpscan/cli_options.yml
- ~/.wpscan/scan.json
- ~/.wpscan/scan.yml
- pwd/.wpscan/scan.json
- pwd/.wpscan/scan.yml
If those files exist, options from them will be loaded and overridden if found twice.
If those files exist, options from the `cli_options` key will be loaded and overridden if found twice.
e.g:
~/.wpscan/cli_options.yml:
~/.wpscan/scan.yml:
```yml
proxy: 'http://127.0.0.1:8080'
verbose: true
cli_options:
proxy: 'http://127.0.0.1:8080'
verbose: true
```
pwd/.wpscan/cli_options.yml:
pwd/.wpscan/scan.yml:
```yml
proxy: 'socks5://127.0.0.1:9090'
url: 'http://target.tld'
cli_options:
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```
Enumerating usernames
## 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
```
## Enumerating usernames
```shell
wpscan --url https://target.tld/ --enumerate u

View File

@@ -1,6 +1,7 @@
# 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,7 @@ 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
return if target.content_dir(ParsedCli.detection_mode)
return if target.content_dir
raise Error::WpContentDirNotDetected
end

View File

@@ -18,10 +18,10 @@ module WPScan
choices: {
vp: OptBoolean.new(['--vulnerable-plugins']),
ap: OptBoolean.new(['--all-plugins']),
p: OptBoolean.new(['--plugins']),
p: OptBoolean.new(['--popular-plugins']),
vt: OptBoolean.new(['--vulnerable-themes']),
at: OptBoolean.new(['--all-themes']),
t: OptBoolean.new(['--themes']),
t: OptBoolean.new(['--popular-themes']),
tt: OptBoolean.new(['--timthumbs']),
cb: OptBoolean.new(['--config-backups']),
dbe: OptBoolean.new(['--db-exports']),
@@ -69,7 +69,7 @@ module WPScan
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
'Set to 0 to ignore the threshold.'], default: 100, advanced: true
)
]
end
@@ -98,7 +98,7 @@ module WPScan
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
'Set to 0 to ignore the threshold.'], default: 20, advanced: true
)
]
end

View File

@@ -56,7 +56,7 @@ module WPScan
#
# @return [ Boolean ] Wether or not to enumerate the plugins
def enum_plugins?(opts)
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
end
def enum_plugins
@@ -92,7 +92,7 @@ module WPScan
if opts[:enumerate][:all_plugins]
DB::Plugins.all_slugs
elsif opts[:enumerate][:plugins]
elsif opts[:enumerate][:popular_plugins]
DB::Plugins.popular_slugs
else
DB::Plugins.vulnerable_slugs
@@ -103,7 +103,7 @@ module WPScan
#
# @return [ Boolean ] Wether or not to enumerate the themes
def enum_themes?(opts)
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
end
def enum_themes
@@ -139,7 +139,7 @@ module WPScan
if opts[:enumerate][:all_themes]
DB::Themes.all_slugs
elsif opts[:enumerate][:themes]
elsif opts[:enumerate][:popular_themes]
DB::Themes.popular_slugs
else
DB::Themes.vulnerable_slugs

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to handle the API token
class VulnApi < CMSScanner::Controller::Base
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
DB::VulnApi.token = ParsedCli.api_token
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

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

View File

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

View File

@@ -3,9 +3,9 @@
module WPScan
module Finders
module MainTheme
# From the css style
class CssStyle < CMSScanner::Finders::Finder
include Finders::WpItems::URLsInHomepage
# 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
def create_theme(slug, style_url, opts)
Model::Theme.new(

View File

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

View File

@@ -5,7 +5,7 @@ module WPScan
module MainTheme
# URLs In Homepage Finder
class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::URLsInHomepage
include WpItems::UrlsInPage
# @param [ Hash ] opts
#
@@ -21,6 +21,11 @@ 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
return unless target.homepage_res.body =~ PATTERN || target.error_404_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])
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0)
end
def valid_credentials?(response)

View File

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

View File

@@ -1,6 +1,7 @@
# 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'
@@ -22,6 +23,7 @@ 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

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

View File

@@ -4,10 +4,9 @@ module WPScan
module Finders
module 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::URLsInHomepage
include WpItems::UrlsInPage
# @param [ Hash ] opts
#
@@ -21,6 +20,11 @@ module WPScan
found
end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end
end
end

View File

@@ -1,12 +1,13 @@
# 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
@@ -14,6 +15,7 @@ module WPScan
def initialize(target)
finders <<
Themes::UrlsInHomepage.new(target) <<
Themes::UrlsIn404Page.new(target) <<
Themes::KnownLocations.new(target)
end
end

View File

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

View File

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

View File

@@ -97,9 +97,12 @@ 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|
return node.text.to_s
text = node.text.to_s.strip
return text unless text.empty?
end
# WP < 3.0

View File

@@ -34,6 +34,8 @@ 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

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

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module WpItems
# URLs In Homepage Module to use in plugins & themes finders
module URLsInHomepage
module UrlsInPage
# @param [ String ] type plugins / themes
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
#
@@ -12,10 +12,12 @@ module WPScan
def items_from_links(type, uniq = true)
found = []
target.in_scope_uris(target.homepage_res) do |uri|
target.in_scope_uris(page_res) do |uri|
next unless uri.to_s =~ item_attribute_pattern(type)
found << Regexp.last_match[1]
slug = Regexp.last_match[1]&.strip
found << slug unless slug&.empty?
end
uniq ? found.uniq.sort : found.sort
@@ -28,7 +30,7 @@ module WPScan
def items_from_codes(type, uniq = true)
found = []
target.homepage_res.html.css('script,style').each do |tag|
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
code = tag.text.to_s
next if code.empty?
@@ -42,7 +44,7 @@ module WPScan
#
# @return [ Regexp ]
def item_attribute_pattern(type)
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
@item_attribute_pattern ||= %r{#{item_url_pattern(type)}([^/]+)/}i
end
# @param [ String ] type
@@ -59,7 +61,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(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i

View File

@@ -15,9 +15,16 @@ module WPScan
@uri = Addressable::URI.parse(blog.url(path_from_blog))
end
# @return [ JSON ]
# 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 ]
def db_data
@db_data ||= DB::Plugin.db_data(slug)
@db_data ||= DB::VulnApi.plugin_data(slug)
end
# @param [ Hash ] opts

View File

@@ -21,9 +21,16 @@ 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::Theme.db_data(slug)
@db_data ||= DB::VulnApi.theme_data(slug)
end
# @param [ Hash ] opts
@@ -94,7 +101,7 @@ module WPScan
#
# @return [ String ]
def parse_style_tag(body, tag)
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n\*]+)/i, 1]
value && !value.strip.empty? ? value.strip : nil
end

View File

@@ -14,7 +14,7 @@ module WPScan
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog
delegate :homepage_res, :error_404_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog
# @param [ String ] slug The plugin/theme slug
# @param [ Target ] blog The targeted blog
@@ -60,18 +60,18 @@ module WPScan
# @return [ String ]
def latest_version
@latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
@latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil
end
# Not used anywhere ATM
# @return [ Boolean ]
def popular?
@popular ||= db_data['popular']
@popular ||= metadata['popular'] ? true : false
end
# @return [ String ]
def last_updated
@last_updated ||= db_data['last_updated']
@last_updated ||= metadata['last_updated']
end
# @return [ Boolean ]

View File

@@ -35,9 +35,16 @@ module WPScan
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end
# @return [ JSON ]
# 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 ]
def db_data
@db_data ||= DB::Version.db_data(number)
@db_data ||= DB::VulnApi.wordpress_data(number)
end
# @return [ Array<Vulnerability> ]
@@ -55,12 +62,12 @@ module WPScan
# @return [ String ]
def release_date
@release_date ||= db_data['release_date'] || 'Unknown'
@release_date ||= metadata['release_date'] || 'Unknown'
end
# @return [ String ]
def status
@status ||= db_data['status'] || 'Unknown'
@status ||= metadata['status'] || 'Unknown'
end
end
end

View File

@@ -1,14 +1,14 @@
_______________________________________________________________
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
__ _______ _____
\ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_|
WordPress Security Scanner by the WPScan Team
Version <%= WPScan::VERSION %>
Sponsored by Sucuri - https://sucuri.net
@_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
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_
_______________________________________________________________

View File

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

View File

@@ -0,0 +1,13 @@
<% 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

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

View File

@@ -0,0 +1,13 @@
"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,6 +5,7 @@ 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,6 +7,7 @@ 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,6 +12,7 @@ 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,7 +13,8 @@ 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'
@@ -38,12 +39,28 @@ 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
"WPScan v#{VERSION} (https://wpscan.org/)"
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.org/)"
end
end
end

View File

@@ -7,9 +7,12 @@ 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

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

View File

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

16
lib/wpscan/db/sponsor.rb Normal file
View File

@@ -0,0 +1,16 @@
# 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 [ String ]
def self.db_file
@db_file ||= DB_DIR.join('themes.json').to_s
# @return [ Hash ]
def self.metadata
@metadata ||= super['themes'] || {}
end
end
end

View File

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

View File

@@ -7,12 +7,15 @@ module WPScan
class Updater
# /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
FILES = %w[
plugins.json themes.json wordpresses.json
metadata.json wp_fingerprints.json
timthumbs-v3.txt config_backups.txt db_exports.txt
dynamic_finders.yml wp_fingerprints.json LICENSE
dynamic_finders.yml LICENSE sponsor.txt
].freeze
OLD_FILES = %w[wordpress.db user-agents.txt dynamic_finders_01.yml].freeze
OLD_FILES = %w[
wordpress.db user-agents.txt dynamic_finders_01.yml
wordpresses.json plugins.json themes.json
].freeze
attr_reader :repo_directory

79
lib/wpscan/db/vuln_api.rb Normal file
View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
module WPScan
module DB
# WPVulnDB API
class VulnApi
NON_ERROR_CODES = [200, 401].freeze
class << self
attr_accessor :token
end
# @return [ Addressable::URI ]
def self.uri
@uri ||= Addressable::URI.parse('https://wpvulndb.com/api/v3/')
end
# @param [ String ] path
# @param [ Hash ] params
#
# @return [ Hash ]
def self.get(path, params = {})
return {} unless token
res = Browser.get(uri.join(path), params.merge(request_params))
return {} if res.code == 404 # This is for API inconsistencies when dots in path
return JSON.parse(res.body) if NON_ERROR_CODES.include?(res.code)
raise Error::HTTP, res
rescue Error::HTTP => e
retries ||= 0
if (retries += 1) <= 3
sleep(1)
retry
end
{ 'http_error' => e }
end
# @return [ Hash ]
def self.plugin_data(slug)
get("plugins/#{slug}")&.dig(slug) || {}
end
# @return [ Hash ]
def self.theme_data(slug)
get("themes/#{slug}")&.dig(slug) || {}
end
# @return [ Hash ]
def self.wordpress_data(version_number)
get("wordpresses/#{version_number.tr('.', '')}")&.dig(version_number) || {}
end
# @return [ Hash ]
def self.status
json = get('status', params: { version: WPScan::VERSION }, cache_ttl: 0)
json['requests_remaining'] = 'Unlimited' if json['requests_remaining'] == -1
json
end
# @return [ Hash ]
def self.request_params
{
headers: {
'Host' => uri.host, # Reset in case user provided a --vhost for the target
'Referer' => nil, # Removes referer set by the cmsscanner to the target url
'User-Agent' => Browser.instance.default_user_agent,
'Authorization' => "Token token=#{token}"
}
}
end
end
end
end

View File

@@ -6,14 +6,19 @@ module WPScan
class WpItem
# @param [ String ] identifier The plugin/theme slug or version number
#
# @return [ Hash ] The JSON data from the DB associated to the identifier
def self.db_data(identifier)
db[identifier] || {}
# @return [ Hash ] The JSON data from the metadata associated to the identifier
def self.metadata_at(identifier)
metadata[identifier] || {}
end
# @return [ JSON ]
def self.db
@db ||= read_json_file(db_file)
def self.metadata
@metadata ||= read_json_file(metadata_file)
end
# @return [ String ]
def self.metadata_file
@metadata_file ||= DB_DIR.join('metadata.json').to_s
end
end
end

View File

@@ -6,17 +6,17 @@ module WPScan
class WpItems
# @return [ Array<String> ] The slug of all items
def self.all_slugs
db.keys
metadata.keys
end
# @return [ Array<String> ] The slug of all popular items
def self.popular_slugs
db.select { |_key, item| item['popular'] == true }.keys
metadata.select { |_key, item| item['popular'] == true }.keys
end
# @return [ Array<String> ] The slug of all vulnerable items
def self.vulnerable_slugs
db.reject { |_key, item| item['vulnerabilities'].empty? }.keys
metadata.select { |_key, item| item['vulnerabilities'] == true }.keys
end
end
end

View File

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

View File

@@ -12,5 +12,6 @@ end
require_relative 'errors/enumeration'
require_relative 'errors/http'
require_relative 'errors/update'
require_relative 'errors/vuln_api'
require_relative 'errors/wordpress'
require_relative 'errors/xmlrpc'

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module WPScan
module Error
# Error raised when the token given via --api-token is invalid
class InvalidApiToken < Standard
def to_s
'The API token provided is invalid'
end
end
# Error raised when the number of API requests has been reached
# currently not implemented on the API side
class ApiLimitReached < Standard
def to_s
'Your API limit has been reached'
end
end
end
end

View File

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

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders
module DynamicFinder
module Version
# Version finder using Body Pattern method. Tipically used when the response is not
# Version finder using Body Pattern method. Typically used when the response is not
# an HTML doc and Xpath can't be used
class BodyPattern < Finders::DynamicFinder::Version::Finder
# @return [ Hash ]
@@ -16,7 +16,7 @@ module WPScan
# @param [ Hash ] opts
# @return [ Version ]
def find(response, _opts = {})
return unless response.body =~ self.class::PATTERN
return unless response.code != 404 && response.body =~ self.class::PATTERN
create_version(
Regexp.last_match[:v],

View File

@@ -31,9 +31,14 @@ module WPScan
passive_configs.each do |slug, configs|
configs.each do |klass, config|
item = process_response(opts, target.homepage_res, slug, klass, config)
[target.homepage_res, target.error_404_res].each do |page_res|
item = process_response(opts, page_res, slug, klass, config)
found << item if item.is_a?(Model::WpItem)
if item.is_a?(Model::WpItem)
found << item
break # No need to check the other page if detected in the current
end
end
end
end

View File

@@ -11,7 +11,9 @@ module WPScan
module WordPress
include CMSScanner::Target::Platform::PHP
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu\-)?plugins|uploads))|wp-includes)/}i.freeze
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu\-)?plugins|uploads))|wp-includes)/}i.freeze
WP_JSON_OEMBED_PATTERN = %r{/wp\-json/oembed/}i.freeze
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp\-admin\\?/admin\-ajax\.php}i.freeze
# These methods are used in the associated interesting_findings finders
# to keep the boolean state of the finding rather than re-check the whole thing again
@@ -22,22 +24,16 @@ module WPScan
# @param [ Symbol ] detection_mode
#
# @return [ Boolean ]
# @return [ Boolean ] Whether or not the target is running WordPress
def wordpress?(detection_mode)
in_scope_uris(homepage_res) do |uri|
return true if uri.path.match(WORDPRESS_PATTERN)
[homepage_res, error_404_res].each do |page_res|
return true if wordpress_from_meta_comments_or_scripts?(page_res)
end
homepage_res.html.css('meta[name="generator"]').each do |node|
return true if /wordpress/i.match?(node['content'])
end
return true unless comments_from_page(/wordpress/i, homepage_res).empty?
if %i[mixed aggressive].include?(detection_mode)
%w[wp-admin/install.php wp-login.php].each do |path|
in_scope_uris(Browser.get_and_follow_location(url(path))).each do |uri|
return true if uri.path.match(WORDPRESS_PATTERN)
return true if in_scope_uris(Browser.get_and_follow_location(url(path))).any? do |uri|
WORDPRESS_PATTERN.match?(uri.path)
end
end
end
@@ -45,6 +41,26 @@ module WPScan
false
end
# @param [ Typhoeus::Response ] response
# @return [ Boolean ]
def wordpress_from_meta_comments_or_scripts?(response)
in_scope_uris(response) do |uri|
return true if WORDPRESS_PATTERN.match?(uri.path) || WP_JSON_OEMBED_PATTERN.match?(uri.path)
end
return true if response.html.css('meta[name="generator"]').any? do |node|
/wordpress/i.match?(node['content'])
end
return true unless comments_from_page(/wordpress/i, response).empty?
return true if response.html.xpath('//script[not(@src)]').any? do |node|
WP_ADMIN_AJAX_PATTERN.match?(node.text)
end
false
end
COOKIE_PATTERNS = {
'vjs' => /createCookie\('vjs','(?<c_value>\d+)',\d+\);/i
}.freeze
@@ -82,7 +98,7 @@ module WPScan
def wordpress_hosted?
return true if /\.wordpress\.com$/i.match?(uri.host)
unless content_dir(:passive)
unless content_dir
pattern = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
uris_from_page(homepage_res) do |uri|
@@ -109,6 +125,7 @@ module WPScan
Browser.instance.forge_request(
login_url,
method: :post,
cache_ttl: 0,
body: { log: username, pwd: password }
)
end

View File

@@ -13,25 +13,24 @@ module WPScan
@plugins_dir = dir.chomp('/')
end
# @param [ Symbol ] detection_mode
# @return [ String ] The wp-content directory
def content_dir(detection_mode = :mixed)
def content_dir
unless @content_dir
# scope_url_pattern is from CMSScanner::Target
pattern = %r{#{scope_url_pattern}([\w\s\-/]+)\\?/(?:themes|plugins|uploads|cache)\\?/}i
pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i
in_scope_uris(homepage_res) do |uri|
return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
[homepage_res, error_404_res].each do |page_res|
in_scope_uris(page_res, '//link/@href|//script/@src|//img/@src') do |uri|
return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
end
# Checks for the pattern in raw JS code, as well as @content attributes of meta tags
xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, page_res) do |match|
return @content_dir = match[1]
end
end
# Checks for the pattern in raw JS code, as well as @content attributes of meta tags
xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, homepage_res) do |match|
return @content_dir = match[1]
end
unless detection_mode == :passive
return @content_dir = 'wp-content' if default_content_dir_exists?
end
return @content_dir = 'wp-content' if default_content_dir_exists?
end
@content_dir
@@ -107,8 +106,10 @@ module WPScan
# url_pattern is from CMSScanner::Target
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i
in_scope_uris(homepage_res) do |uri|
return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
[homepage_res, error_404_res].each do |page_res|
in_scope_uris(page_res) do |uri|
return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
end
end
@sub_dir = false

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module Typhoeus
# Custom Response class
class Response
# @note: Ignores requests done to the /status endpoint of the API
#
# @return [ Boolean ]
def from_vuln_api?
effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) && !effective_url.include?('/status')
end
end
end

View File

@@ -2,5 +2,5 @@
# Version
module WPScan
VERSION = '3.6.2'
VERSION = '3.7.4'
end

View File

@@ -166,6 +166,7 @@ describe WPScan::Controller::Core do
before do
expect(core).to receive(:load_server_module)
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
expect(core.target).to receive(:wordpress_hosted?).and_return(false)
end
it 'calls the formatter when started and finished to update the db' do
@@ -174,56 +175,6 @@ describe WPScan::Controller::Core do
end
end
context 'when a redirect occurs' do
before do
stub_request(:any, target_url)
expect(core.target).to receive(:homepage_res)
.at_least(1)
.and_return(Typhoeus::Response.new(effective_url: redirection, body: ''))
end
context 'to the wp-admin/install.php' do
let(:redirection) { "#{target_url}wp-admin/install.php" }
it 'calls the formatter with the correct parameters and exit' do
expect(core.formatter).to receive(:output)
.with('not_fully_configured', hash_including(url: redirection), 'core').ordered
# TODO: Would be cool to be able to test the exit code
expect { core.before_scan }.to raise_error(SystemExit)
end
end
context 'to something else' do
let(:redirection) { 'http://g.com/' }
it 'raises an error' do
expect { core.before_scan }.to raise_error(CMSScanner::Error::HTTPRedirect)
end
end
context 'to another path with the wp-admin/install.php in the query' do
let(:redirection) { "#{target_url}index.php?a=/wp-admin/install.php" }
context 'when wordpress' do
it 'does not raise an error' do
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
expect { core.before_scan }.to_not raise_error
end
end
context 'when not wordpress' do
it 'raises an error' do
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
end
end
end
end
context 'when hosted on wordpress.com' do
let(:target_url) { 'http://ex.wordpress.com' }
@@ -234,52 +185,106 @@ describe WPScan::Controller::Core do
end
end
context 'when wordpress' do
before do
expect(core).to receive(:load_server_module)
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
end
context 'when not hosted on wordpress.com' do
before { allow(core.target).to receive(:wordpress_hosted?).and_return(false) }
it 'does not raise any error' do
expect { core.before_scan }.to_not raise_error
end
end
context 'when a redirect occurs' do
before do
stub_request(:any, target_url)
context 'when not wordpress' do
before do
expect(core).to receive(:load_server_module)
end
expect(core.target).to receive(:homepage_res)
.at_least(1)
.and_return(Typhoeus::Response.new(effective_url: redirection, body: ''))
end
context 'when no --force' do
before { expect(core.target).to receive(:maybe_add_cookies) }
context 'to the wp-admin/install.php' do
let(:redirection) { "#{target_url}wp-admin/install.php" }
context 'when no cookies added or still not wordpress after being added' do
it 'raises an error' do
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
it 'calls the formatter with the correct parameters and exit' do
expect(core.formatter).to receive(:output)
.with('not_fully_configured', hash_including(url: redirection), 'core').ordered
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
# TODO: Would be cool to be able to test the exit code
expect { core.before_scan }.to raise_error(SystemExit)
end
end
context 'when the added cookies solved it' do
it 'does not raise an error' do
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false).ordered
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true).ordered
context 'to something else' do
let(:redirection) { 'http://g.com/' }
it 'raises an error' do
expect { core.before_scan }.to raise_error(CMSScanner::Error::HTTPRedirect)
end
end
context 'to another path with the wp-admin/install.php in the query' do
let(:redirection) { "#{target_url}index.php?a=/wp-admin/install.php" }
context 'when wordpress' do
it 'does not raise an error' do
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
expect { core.before_scan }.to_not raise_error
end
end
context 'when not wordpress' do
it 'raises an error' do
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
end
end
end
end
context 'when wordpress' do
before do
expect(core).to receive(:load_server_module)
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
end
it 'does not raise any error' do
expect { core.before_scan }.to_not raise_error
end
end
context 'when not wordpress' do
before do
expect(core).to receive(:load_server_module)
end
context 'when no --force' do
before { expect(core.target).to receive(:maybe_add_cookies) }
context 'when no cookies added or still not wordpress after being added' do
it 'raises an error' do
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
end
end
context 'when the added cookies solved it' do
it 'does not raise an error' do
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false).ordered
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true).ordered
expect { core.before_scan }.to_not raise_error
end
end
end
context 'when --force' do
let(:cli_args) { "#{super()} --force" }
it 'does not raise any error' do
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false)
expect { core.before_scan }.to_not raise_error
end
end
end
context 'when --force' do
let(:cli_args) { "#{super()} --force" }
it 'does not raise any error' do
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false)
expect { core.before_scan }.to_not raise_error
end
end
end
end
end

View File

@@ -20,7 +20,7 @@ describe WPScan::Controller::CustomDirectories do
describe '#before_scan' do
context 'when the content_dir is not found and not supplied' do
before { expect(controller.target).to receive(:content_dir).with(:mixed) }
before { expect(controller.target).to receive(:content_dir).and_return(nil) }
it 'raises an exception' do
expect { controller.before_scan }.to raise_error(WPScan::Error::WpContentDirNotDetected)

View File

@@ -0,0 +1,93 @@
# frozen_string_literal: true
describe WPScan::Controller::VulnApi do
subject(:controller) { described_class.new }
let(:target_url) { 'http://ex.lo/' }
let(:cli_args) { "--url #{target_url}" }
before do
WPScan::ParsedCli.options = rspec_parsed_options(cli_args)
end
describe '#cli_options' do
its(:cli_options) { should_not be_empty }
its(:cli_options) { should be_a Array }
it 'contains to correct options' do
expect(controller.cli_options.map(&:to_sym)).to eq %i[api_token]
end
end
describe '#before_scan' do
context 'when no --api-token provided' do
its(:before_scan) { should be nil }
end
context 'when --api-token given' do
let(:cli_args) { "#{super()} --api-token token" }
context 'when the token is invalid' do
before { expect(WPScan::DB::VulnApi).to receive(:status).and_return('error' => 'HTTP Token: Access denied.') }
it 'raise an InvalidApiToken error' do
expect { controller.before_scan }.to raise_error(WPScan::Error::InvalidApiToken)
end
end
context 'when the token is valid' do
context 'when the limit has been reached' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => 0)
end
it 'raises an ApiLimitReached error' do
expect { controller.before_scan }.to raise_error(WPScan::Error::ApiLimitReached)
end
end
context 'when a HTTP error, like a timeout' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return(
'http_error' => WPScan::Error::HTTP.new(
Typhoeus::Response.new(effective_url: 'mock-url', return_code: 28)
)
)
end
it 'raises an HTTP error' do
expect { controller.before_scan }
.to raise_error(WPScan::Error::HTTP, 'HTTP Error: mock-url (Timeout was reached)')
end
end
context 'when the token is valid and no HTTP error' do
before do
expect(WPScan::DB::VulnApi)
.to receive(:status)
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => requests)
end
context 'when limited requests' do
let(:requests) { 100 }
it 'does not raise an error' do
expect { controller.before_scan }.to_not raise_error
end
context 'when unlimited requests' do
let(:requests) { 'Unlimited' }
it 'does not raise an error' do
expect { controller.before_scan }.to_not raise_error
end
end
end
end
end
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
# frozen_string_literal: true
describe WPScan::Finders::Passwords::WpLogin do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url) }
let(:url) { 'http://ex.lo/' }
describe '#valid_credentials?' do
context 'when a non 302' do
it 'returns false' do
expect(finder.valid_credentials?(Typhoeus::Response.new(code: 200, headers: {}))).to be_falsey
end
end
context 'when a 302' do
let(:response) { Typhoeus::Response.new(code: 302, headers: headers) }
context 'when no cookies set' do
let(:headers) { {} }
it 'returns false' do
expect(finder.valid_credentials?(response)).to be_falsey
end
end
context 'when no logged_in cookie set' do
context 'when only one cookie set' do
let(:headers) { 'Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/' }
it 'returns false' do
expect(finder.valid_credentials?(response)).to be_falsey
end
end
context 'when multiple cookies set' do
let(:headers) do
"Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/\r\n" \
'Set-Cookie: something=value; path=/'
end
it 'returns false' do
expect(finder.valid_credentials?(response)).to be_falsey
end
end
end
context 'when logged_in cookie set' do
let(:headers) do
"Set-Cookie: wordpress_test_cookie=WP+Cookie+check; path=/\r\r" \
"Set-Cookie: wordpress_xxx=yyy; path=/wp-content/plugins; httponly\r\n" \
"Set-Cookie: wordpress_xxx=yyy; path=/wp-admin; httponly\r\n" \
'Set-Cookie: wordpress_logged_in_xxx=yyy; path=/; httponly'
end
it 'returns false' do
expect(finder.valid_credentials?(response)).to eql true
end
end
end
end
end

View File

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

View File

@@ -3,14 +3,17 @@
describe WPScan::Finders::Plugins::UrlsInHomepage do
subject(:finder) { described_class.new(target) }
let(:target) { WPScan::Target.new(url) }
let(:url) { 'http://wp.lab/' }
let(:url) { 'https://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('plugins', 'urls_in_homepage') }
it_behaves_like 'App::Finders::WpItems::URLsInHomepage' do
before { target.scope << 'sub.lab' }
it_behaves_like 'App::Finders::WpItems::UrlsInPage' do
let(:page_url) { url }
let(:type) { 'plugins' }
let(:uniq_links) { true }
let(:uniq_codes) { true }
let(:expected_from_links) { (1..4).map { |i| "dl-#{i}" } }
let(:expected_from_links) { (1..5).map { |i| "dl-#{i}" } }
let(:expected_from_codes) { (1..6).map { |i| "dc-#{i}" } }
end

View File

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

View File

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

View File

@@ -6,7 +6,10 @@ describe WPScan::Finders::Themes::UrlsInHomepage do
let(:url) { 'http://wp.lab/' }
let(:fixtures) { FINDERS_FIXTURES.join('themes', 'urls_in_homepage') }
it_behaves_like 'App::Finders::WpItems::URLsInHomepage' do
# before { target.scope << 'sub.lab' }
it_behaves_like 'App::Finders::WpItems::UrlsInPage' do
let(:page_url) { url }
let(:type) { 'themes' }
let(:uniq_links) { true }
let(:uniq_codes) { true }

View File

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

View File

@@ -50,7 +50,7 @@ describe WPScan::Finders::Users::AuthorIdBruteForcing do
end
context 'when no display_name' do
['4.1.1', '3.0', '2.9.2'].each do |file|
%w[4.9-span-tag 4.1.1 3.0 2.9.2].each do |file|
it "returns nil for #{file}-empty.html" do
body = File.read(fixtures.join("#{file}-empty.html"))

View File

@@ -19,15 +19,17 @@ describe WPScan::Finders::Users::OembedApi do
end
context 'when a JSON response' do
let(:body) { File.read(fixture) }
context 'when 404' do
let(:body) { File.read(fixtures.join('404.json')) }
let(:fixture) { fixtures.join('404.json') }
its(:aggressive) { should eql([]) }
end
context 'when 200' do
context 'when author_url present' do
let(:body) { File.read(fixtures.join('200_author_url.json')) }
let(:fixture) { fixtures.join('200_author_url.json') }
it 'returns the expected array of users' do
users = finder.aggressive
@@ -44,7 +46,7 @@ describe WPScan::Finders::Users::OembedApi do
end
context 'when author_url not present but author_name' do
let(:body) { File.read(fixtures.join('200_author_name.json')) }
let(:fixture) { fixtures.join('200_author_name.json') }
it 'returns the expected array of users' do
users = finder.aggressive
@@ -59,6 +61,12 @@ describe WPScan::Finders::Users::OembedApi do
expect(user.interesting_entries).to eql ['http://wp.lab/wp-json/oembed/1.0/embed?url=http://wp.lab/&format=json']
end
end
context 'when body is an array' do
let(:fixture) { fixtures.join('array.json') }
its(:aggressive) { should eql([]) }
end
end
end
end

View File

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

View File

@@ -81,24 +81,39 @@ describe WPScan::Model::Plugin do
end
describe '#latest_version, #last_updated, #popular' do
context 'when none' do
let(:slug) { 'vulnerable-not-popular' }
before { allow(plugin).to receive(:db_data).and_return(db_data) }
context 'when no db_data and no metadata' do
let(:slug) { 'not-known' }
let(:db_data) { {} }
its(:latest_version) { should be_nil }
its(:last_updated) { should be_nil }
its(:popular?) { should be false }
end
context 'when values' do
context 'when no db_data but metadata' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { {} }
its(:latest_version) { should eql WPScan::Model::Version.new('2.0') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' }
its(:popular?) { should be true }
end
context 'when db_data' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') }
its(:latest_version) { should eql WPScan::Model::Version.new('2.1') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' }
its(:popular?) { should be true }
end
end
describe '#outdated?' do
before { allow(plugin).to receive(:db_data).and_return({}) }
context 'when last_version' do
let(:slug) { 'no-vulns-popular' }
@@ -116,13 +131,13 @@ describe WPScan::Model::Plugin do
.and_return(WPScan::Model::Version.new(version_number))
end
context 'when version < last_version' do
context 'when version < latest_version' do
let(:version_number) { '1.2' }
its(:outdated?) { should eql true }
end
context 'when version >= last_version' do
context 'when version >= latest_version' do
let(:version_number) { '3.0' }
its(:outdated?) { should eql false }
@@ -130,7 +145,7 @@ describe WPScan::Model::Plugin do
end
end
context 'when no last_version' do
context 'when no latest_version' do
let(:slug) { 'vulnerable-not-popular' }
context 'when no version' do
@@ -153,13 +168,16 @@ describe WPScan::Model::Plugin do
end
describe '#vulnerabilities' do
before { allow(plugin).to receive(:db_data).and_return(db_data) }
after do
expect(plugin.vulnerabilities).to eq @expected
expect(plugin.vulnerable?).to eql @expected.empty? ? false : true
end
context 'when plugin not in the DB' do
let(:slug) { 'not-in-db' }
let(:slug) { 'not-in-db' }
let(:db_data) { {} }
it 'returns an empty array' do
@expected = []
@@ -168,7 +186,8 @@ describe WPScan::Model::Plugin do
context 'when in the DB' do
context 'when no vulnerabilities' do
let(:slug) { 'no-vulns-popular' }
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('plugins/no-vulns-popular') }
it 'returns an empty array' do
@expected = []
@@ -176,11 +195,13 @@ describe WPScan::Model::Plugin do
end
context 'when vulnerabilities' do
let(:slug) { 'vulnerable-not-popular' }
let(:slug) { 'vulnerable-not-popular' }
let(:db_data) { vuln_api_data_for('plugins/vulnerable-not-popular') }
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'First Vuln',
'First Vuln <= 6.3.10 - LFI',
{ wpvulndb: '1' },
'LFI',
'6.3.10'

View File

@@ -10,10 +10,9 @@ describe WPScan::Model::Theme do
before { expect(blog).to receive(:content_dir).at_least(1).and_return('wp-content') }
describe '#new' do
before do
stub_request(:get, /.*\.css\z/)
.to_return(body: File.read(fixtures.join('style.css')))
end
before { stub_request(:get, /.*\.css\z/).to_return(body: File.read(fixture)) }
let(:fixture) { fixtures.join('style.css') }
its(:url) { should eql 'http://wp.lab/wp-content/themes/spec/' }
its(:style_url) { should eql 'http://wp.lab/wp-content/themes/spec/style.css' }
@@ -34,6 +33,14 @@ describe WPScan::Model::Theme do
its(:style_url) { should eql opts[:style_url] }
end
context 'when some new lines are stripped' do
let(:fixture) { fixtures.join('stripped_new_lines.css') }
its(:style_name) { should eql 'Divi' }
its(:style_uri) { should eql 'http://www.elegantthemes.com/gallery/divi/' }
its(:license_uri) { should eql 'http://www.gnu.org/licenses/gpl-2.0.html' }
end
end
describe '#version' do
@@ -86,8 +93,179 @@ describe WPScan::Model::Theme do
end
end
describe '#latest_version, #last_updated, #popular' do
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return(db_data)
end
context 'when no db_data and no metadata' do
let(:slug) { 'not-known' }
let(:db_data) { {} }
its(:latest_version) { should be_nil }
its(:last_updated) { should be_nil }
its(:popular?) { should be false }
end
context 'when no db_data but metadata' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { {} }
its(:latest_version) { should eql WPScan::Model::Version.new('2.0') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z' }
its(:popular?) { should be true }
end
context 'when db_data' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') }
its(:latest_version) { should eql WPScan::Model::Version.new('2.2') }
its(:last_updated) { should eql '2015-05-16T00:00:00.000Z-via-api' }
its(:popular?) { should be true }
end
end
describe '#outdated?' do
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return({})
end
context 'when last_version' do
let(:slug) { 'no-vulns-popular' }
context 'when no version' do
before { expect(theme).to receive(:version).at_least(1).and_return(nil) }
its(:outdated?) { should eql false }
end
context 'when version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new(version_number))
end
context 'when version < latest_version' do
let(:version_number) { '1.2' }
its(:outdated?) { should eql true }
end
context 'when version >= latest_version' do
let(:version_number) { '3.0' }
its(:outdated?) { should eql false }
end
end
end
context 'when no latest_version' do
let(:slug) { 'vulnerable-not-popular' }
context 'when no version' do
before { expect(theme).to receive(:version).at_least(1).and_return(nil) }
its(:outdated?) { should eql false }
end
context 'when version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new('1.0'))
end
its(:outdated?) { should eql false }
end
end
end
describe '#vulnerabilities' do
xit
before do
stub_request(:get, /.*\.css\z/)
allow(theme).to receive(:db_data).and_return(db_data)
end
after do
expect(theme.vulnerabilities).to eq @expected
expect(theme.vulnerable?).to eql @expected.empty? ? false : true
end
context 'when theme not in the DB' do
let(:slug) { 'not-in-db' }
let(:db_data) { {} }
it 'returns an empty array' do
@expected = []
end
end
context 'when in the DB' do
context 'when no vulnerabilities' do
let(:slug) { 'no-vulns-popular' }
let(:db_data) { vuln_api_data_for('themes/no-vulns-popular') }
it 'returns an empty array' do
@expected = []
end
end
context 'when vulnerabilities' do
let(:slug) { 'vulnerable-not-popular' }
let(:db_data) { vuln_api_data_for('themes/vulnerable-not-popular') }
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'First Vuln',
{ wpvulndb: '1' },
'LFI',
'6.3.10'
),
WPScan::Vulnerability.new('No Fixed In', wpvulndb: '2')
]
end
context 'when no theme version' do
before { expect(theme).to receive(:version).at_least(1).and_return(false) }
it 'returns all the vulnerabilities' do
@expected = all_vulns
end
end
context 'when theme version' do
before do
expect(theme)
.to receive(:version)
.at_least(1)
.and_return(WPScan::Model::Version.new(number))
end
context 'when < to a fixed_in' do
let(:number) { '5.0' }
it 'returns it' do
@expected = all_vulns
end
end
context 'when >= to a fixed_in' do
let(:number) { '6.3.10' }
it 'does not return it ' do
@expected = [all_vulns.last]
end
end
end
end
end
end
describe '#parent_theme' do

View File

@@ -40,11 +40,13 @@ describe WPScan::Model::WpVersion do
describe '#vulnerabilities' do
subject(:version) { described_class.new(number) }
before { allow(version).to receive(:db_data).and_return(db_data) }
context 'when no vulns' do
let(:number) { '4.4' }
let(:db_data) { { 'vulnerabilities' => [] } }
its(:vulnerabilities) { should eql([]) }
its(:vulnerabilities) { should be_empty }
end
context 'when vulnerable' do
@@ -53,13 +55,30 @@ describe WPScan::Model::WpVersion do
expect(version).to be_vulnerable
end
let(:all_vulns) do
[
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 1',
{ wpvulndb: '1' },
'SQLI'
),
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 2',
{ url: %w[url-2 url-3], osvdb: %w[10], cve: %w[2014-0166], wpvulndb: '2' },
nil,
'3.8.2'
)
]
end
context 'when a signle vuln' do
let(:number) { '3.8' }
let(:number) { '3.8.1' }
let(:db_data) { vuln_api_data_for('wordpresses/38') }
it 'returns the expected result' do
@expected = [WPScan::Vulnerability.new(
'WP 3.8 - Vuln 1',
{ url: %w[url-4], osvdb: %w[11], wpvulndb: '3' },
{ url: %w[url-4], wpvulndb: '3' },
'AUTHBYPASS'
)]
end
@@ -67,6 +86,7 @@ describe WPScan::Model::WpVersion do
context 'when multiple vulns' do
let(:number) { '3.8.1' }
let(:db_data) { vuln_api_data_for('wordpresses/381') }
it 'returns the expected results' do
@expected = [
@@ -77,7 +97,7 @@ describe WPScan::Model::WpVersion do
),
WPScan::Vulnerability.new(
'WP 3.8.1 - Vuln 2',
{ url: %w[url-2 url-3], osvdb: %w[10], cve: %w[2014-0166], wpvulndb: '2' },
{ url: %w[url-2 url-3], cve: %w[2014-0166], wpvulndb: '2' },
nil,
'3.8.2'
)
@@ -87,27 +107,30 @@ describe WPScan::Model::WpVersion do
end
end
describe '#release_date' do
describe '#metadata, #release_date, #status' do
subject(:version) { described_class.new('3.8.1') }
its(:release_date) { should eql '2014-01-23' }
before { allow(version).to receive(:db_data).and_return(db_data) }
context 'when the version is not in the DB' do
subject(:version) { described_class.new('3.8.2') }
context 'when no db_data' do
let(:db_data) { {} }
its(:release_date) { should eql 'Unknown' }
its(:release_date) { should eql '2014-01-23' }
its(:status) { should eql 'outdated' }
context 'when the version is not in the metadata' do
subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql 'Unknown' }
its(:status) { should eql 'Unknown' }
end
end
end
describe '#status' do
subject(:version) { described_class.new('3.8.1') }
context 'when db_data' do
let(:db_data) { vuln_api_data_for('wordpresses/381') }
its(:status) { should eql 'outdated' }
context 'when the version is not in the DB' do
subject(:version) { described_class.new('3.8.2') }
its(:release_date) { should eql 'Unknown' }
its(:release_date) { should eql '2014-01-23-via-api' }
its(:status) { should eql 'outdated-via-api' }
end
end
end

View File

@@ -9,6 +9,7 @@ describe 'App::Views' do
# in the expected output.
%i[JSON CliNoColour].each do |formatter|
context "when #{formatter}" do
it_behaves_like 'App::Views::VulnApi'
it_behaves_like 'App::Views::WpVersion'
it_behaves_like 'App::Views::MainTheme'
it_behaves_like 'App::Views::Enumeration'

File diff suppressed because it is too large Load Diff

56
spec/fixtures/db/metadata.json vendored Normal file
View File

@@ -0,0 +1,56 @@
{
"wordpress": {
"4.0": {
"release_date": "2014-09-04",
"status": "latest"
},
"3.8.1": {
"release_date": "2014-01-23",
"status": "outdated"
},
"3.8": {
"release_date": "2013-12-12",
"status": "insecure"
}
},
"plugins": {
"no-vulns-popular": {
"vulnerabilities": false,
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z"
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities": true
}
},
"themes": {
"no-vulns-popular": {
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z",
"vulnerabilities": false
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities": true
},
"dignitas-themes": {
"popular": true,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : true
},
"yaaburnee-themes": {
"popular": false,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : true
}
}
}

View File

@@ -1,25 +0,0 @@
{
"no-vulns-popular": {
"vulnerabilities": [],
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z"
},
"vulnerable-not-popular": {
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}
}

1
spec/fixtures/db/sponsor.txt vendored Normal file
View File

@@ -0,0 +1 @@
Sponsored By Kittens

View File

@@ -1,48 +0,0 @@
{
"no-vulns-popular": {
"popular": true,
"latest_version": "2.0",
"last_updated": "2015-05-16T00:00:00.000Z",
"vulnerabilities": []
},
"dignitas-themes": {
"popular": true,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:59.000Z",
"updated_at" : "2015-03-05T19:37:47.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
"http://packetstormsecurity.com/files/130652/"
]
},
"title" : "Dignitas 1.1.9 - Privilage Escalation",
"id" : 7825,
"vuln_type" : "AUTHBYPASS"
}
]
},
"yaaburnee-themes": {
"popular": false,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:44.000Z",
"updated_at" : "2015-03-05T19:41:14.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
"http://packetstormsecurity.com/files/130652/"
]
},
"title" : "Ya'aburnee 1.0.7 - Privilage Escalation",
"id" : 7824,
"vuln_type" : "AUTHBYPASS"
}
]
}
}

View File

@@ -0,0 +1,6 @@
{
"vulnerabilities": [],
"popular": true,
"latest_version": "2.1",
"last_updated": "2015-05-16T00:00:00.000Z-via-api"
}

View File

@@ -0,0 +1,17 @@
{
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln \u003c= 6.3.10 - LFI",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}

View File

@@ -0,0 +1,22 @@
{
"popular": true,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:59.000Z",
"updated_at" : "2015-03-05T19:37:47.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6"
],
"packetstorm": [
"130652"
]
},
"title" : "Dignitas 1.1.9 - Privilage Escalation",
"id" : 7825,
"vuln_type" : "AUTHBYPASS"
}
]
}

View File

@@ -0,0 +1,6 @@
{
"popular": true,
"latest_version": "2.2",
"last_updated": "2015-05-16T00:00:00.000Z-via-api",
"vulnerabilities": []
}

View File

@@ -0,0 +1,17 @@
{
"latest_version": null,
"last_updated": null,
"popular": false,
"vulnerabilities" : [
{
"title" : "First Vuln",
"fixed_in" : "6.3.10",
"id" : 1,
"vuln_type": "LFI"
},
{
"title": "No Fixed In",
"id": 2
}
]
}

View File

@@ -0,0 +1,22 @@
{
"popular": false,
"latest_version": null,
"last_updated": null,
"vulnerabilities" : [
{
"created_at" : "2015-03-05T19:25:44.000Z",
"updated_at" : "2015-03-05T19:41:14.000Z",
"references": {
"url" : [
"http://research.evex.pw/?vuln=6",
],
"packetstorm": [
"130652"
]
},
"title" : "Ya'aburnee 1.0.7 - Privilage Escalation",
"id" : 7824,
"vuln_type" : "AUTHBYPASS"
}
]
}

View File

@@ -0,0 +1,17 @@
{
"release_date" : "2013-12-12",
"status": "insecure",
"vulnerabilities" : [
{
"references": {
"url" : ["url-4"],
"osvdb" : ["11"]
},
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T15:45:26.000Z",
"title" : "WP 3.8 - Vuln 1",
"id" : 3,
"vuln_type" : "AUTHBYPASS"
}
]
}

View File

@@ -0,0 +1,27 @@
{
"release_date" : "2014-01-23-via-api",
"status": "outdated-via-api",
"vulnerabilities" : [
{
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T13:52:17.000Z",
"title" : "WP 3.8.1 - Vuln 1",
"id" : 1,
"vuln_type" : "SQLI",
"published_date" : null,
"fixed_in" : null
},
{
"references" : {
"cve" : ["2014-0166"],
"osvdb" : ["10"],
"url" : ["url-2","url-3"]
},
"fixed_in" : "3.8.2",
"created_at" : "2014-08-01T10:58:19.000Z",
"updated_at" : "2014-09-16T13:53:11.000Z",
"id" : 2,
"title" : "WP 3.8.1 - Vuln 2"
}
]
}

View File

@@ -0,0 +1,4 @@
{
"release_date" : "2014-09-04",
"status": "latest"
}

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