Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b32e990dd4 | ||
|
|
4320d2436f | ||
|
|
cba6e74b13 | ||
|
|
981bcf5fa2 | ||
|
|
1d79bc37d3 | ||
|
|
2fae3336ba | ||
|
|
cfb98c5139 | ||
|
|
b0260327c4 | ||
|
|
f65532e347 | ||
|
|
ff574b046c | ||
|
|
97c995b64c | ||
|
|
8361ec97e4 | ||
|
|
7a0bbc0acb | ||
|
|
66f5eca841 | ||
|
|
b53e6d1888 | ||
|
|
4b68fa8b60 | ||
|
|
54770c5a50 | ||
|
|
39fb2167f7 | ||
|
|
c33fef9c98 | ||
|
|
08a1117edf | ||
|
|
e14cbed56e | ||
|
|
56e2ab16cc | ||
|
|
d76d4b70f5 | ||
|
|
e223936a81 | ||
|
|
60d067c421 | ||
|
|
4102cf4688 | ||
|
|
dc977e6630 | ||
|
|
05deabd775 | ||
|
|
549ab4aa15 | ||
|
|
b189c71682 | ||
|
|
b909856933 | ||
|
|
5de9084901 | ||
|
|
384ef0b44c | ||
|
|
9307772dc3 | ||
|
|
730c71d103 | ||
|
|
5c710b96f5 | ||
|
|
fe63d0eadf | ||
|
|
a6ca95159a | ||
|
|
677d32fef5 | ||
|
|
14abd05969 | ||
|
|
2e680be34f | ||
|
|
fe29942bf4 | ||
|
|
c8fb717ac1 | ||
|
|
1ff7fcc913 | ||
|
|
419c32702a | ||
|
|
9b63714caa | ||
|
|
f034233607 | ||
|
|
be6fcb51b6 | ||
|
|
e49a682f00 | ||
|
|
23ad3141a1 | ||
|
|
5347e374e0 | ||
|
|
1a49a628de | ||
|
|
8def256d7e | ||
|
|
1cd8e6bad7 | ||
|
|
7a03c0db25 | ||
|
|
e7e3657d1f | ||
|
|
734dfcc9bc | ||
|
|
b0db15099d | ||
|
|
6fbd2369ba | ||
|
|
f4a6674eed | ||
|
|
c0567ad4f5 | ||
|
|
f146ee7e9f | ||
|
|
e606f4ce18 | ||
|
|
945b589a58 | ||
|
|
b18042c4a8 | ||
|
|
a9ff39104b | ||
|
|
f6af6e5880 | ||
|
|
57c6c2d471 | ||
|
|
c362527903 | ||
|
|
a7acbd0738 | ||
|
|
f67192ebce | ||
|
|
c44fde83e4 | ||
|
|
50119285ef | ||
|
|
6216916fed | ||
|
|
2952380200 | ||
|
|
fb42b82e0d | ||
|
|
6d381ab88d | ||
|
|
c5c1de32bc | ||
|
|
8077ad9bcd | ||
|
|
6f22ba350f | ||
|
|
f23d0c0157 | ||
|
|
a9a38edf24 | ||
|
|
a5534f1e49 | ||
|
|
1c6469f384 | ||
|
|
8cfdbc1196 | ||
|
|
88737ca6ea | ||
|
|
45bebc60bd | ||
|
|
4f7dec4635 | ||
|
|
98739cce5a | ||
|
|
0bfbfacc27 | ||
|
|
73cd862e83 | ||
|
|
3305e9b74f | ||
|
|
c37ec0e8d0 | ||
|
|
0b005477c1 | ||
|
|
a1467f8dac | ||
|
|
40d2c34347 | ||
|
|
528270e767 | ||
|
|
f4a04b2387 | ||
|
|
14ed6ae109 | ||
|
|
4fd43694ae | ||
|
|
552d731e6a | ||
|
|
49ac3ef528 | ||
|
|
4379313f12 | ||
|
|
3901949f36 | ||
|
|
a3d8593fed | ||
|
|
7c5baeb9c7 | ||
|
|
c692db5f85 | ||
|
|
9130196ffc | ||
|
|
dad4a65118 | ||
|
|
4c34c2feb7 | ||
|
|
23522f7775 | ||
|
|
82c61398ba | ||
|
|
02871050a6 | ||
|
|
7d3b1fea6b | ||
|
|
24917fa2a6 | ||
|
|
de3d8e4a23 | ||
|
|
1502845d65 | ||
|
|
af3f10f74e | ||
|
|
c100372b31 | ||
|
|
72d699b39a | ||
|
|
7d2b8a2a8b | ||
|
|
8729c68e22 | ||
|
|
e2d48bedd9 | ||
|
|
6b241ce9b3 | ||
|
|
1b68bdb36c | ||
|
|
fb82538441 | ||
|
|
2709d0869a | ||
|
|
343f87bbe7 | ||
|
|
ecbfc6004c | ||
|
|
c57eecc81b | ||
|
|
7ea14dc03f | ||
|
|
4340d27258 | ||
|
|
e911be8f14 | ||
|
|
a4c650cdff | ||
|
|
31a58f8a8f | ||
|
|
ba4f15f111 | ||
|
|
206a913eb9 | ||
|
|
21ba490073 | ||
|
|
2a29e2ed95 | ||
|
|
9517d14fd3 | ||
|
|
3deaa896df | ||
|
|
c117007dc0 | ||
|
|
50baa238b9 | ||
|
|
0e2d771660 | ||
|
|
32b4670755 | ||
|
|
4a032d5e12 | ||
|
|
5887fede15 | ||
|
|
ad4eeb9f81 | ||
|
|
a62c16d7cc | ||
|
|
e766e7392a | ||
|
|
025c9c24ca | ||
|
|
ab052add27 | ||
|
|
15cb99977b | ||
|
|
82d5af926f | ||
|
|
76f73f3dc8 | ||
|
|
575b22320e | ||
|
|
d20c07dc85 | ||
|
|
f89071b87a | ||
|
|
8b4e90f285 | ||
|
|
9c4f57c786 | ||
|
|
902ec24b77 | ||
|
|
7eba77fa63 | ||
|
|
0753bbf7b3 | ||
|
|
6b2333614a | ||
|
|
80b7f458f5 | ||
|
|
dbd8e59cf4 | ||
|
|
9948230ea0 | ||
|
|
e2c858ac69 | ||
|
|
bac8b613e6 | ||
|
|
abbae15c6f | ||
|
|
1548e8bfc1 | ||
|
|
dc8cf3fc34 | ||
|
|
c3cd815567 | ||
|
|
ce543b9384 | ||
|
|
9755c8cf42 | ||
|
|
434a210fb5 | ||
|
|
587602665a | ||
|
|
bfec63df41 | ||
|
|
3b150df1af | ||
|
|
f24ecf0537 | ||
|
|
9ddecbcc0a | ||
|
|
947bb8d3d5 | ||
|
|
30cbf87b35 | ||
|
|
69c3aab35a | ||
|
|
bdeb3547f1 | ||
|
|
99e04b9669 | ||
|
|
680d2fb7eb | ||
|
|
8814eda018 | ||
|
|
7e72ba2885 | ||
|
|
b4d7a8490b | ||
|
|
e9a5bc66df | ||
|
|
edebc77726 | ||
|
|
271dee824d | ||
|
|
1e868d10ca | ||
|
|
4be3f17ae4 | ||
|
|
f24e7be264 | ||
|
|
9adc26445d | ||
|
|
353e7dcbb9 | ||
|
|
430e65c12e | ||
|
|
1aa242a9d8 | ||
|
|
7173cd85fe | ||
|
|
b95a4f55e3 | ||
|
|
6b5e016770 | ||
|
|
85aa9f61cd | ||
|
|
5c187002d6 | ||
|
|
9bc373308b | ||
|
|
cdeb0fc144 | ||
|
|
f1acdd9389 | ||
|
|
d6fac6a210 | ||
|
|
007cfb0801 | ||
|
|
1f9829b7c0 | ||
|
|
e039d22565 | ||
|
|
b0775b1610 | ||
|
|
0e429700c6 | ||
|
|
af7804ca23 |
@@ -14,3 +14,4 @@ Dockerfile
|
||||
*.orig
|
||||
bin/wpscan-*
|
||||
.wpscan/
|
||||
.github/
|
||||
|
||||
41
.github/workflows/build.yml
vendored
Normal file
41
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
ruby: [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: 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 }}
|
||||
40
.github/workflows/gempush.yml
vendored
Normal file
40
.github/workflows/gempush.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
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
5
.rspec
@@ -1,3 +1,2 @@
|
||||
--color
|
||||
--fail-fast
|
||||
--require spec_helper
|
||||
--require spec_helper
|
||||
--color
|
||||
@@ -1,9 +1,12 @@
|
||||
require: rubocop-performance
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
NewCops: enable
|
||||
TargetRubyVersion: 2.5
|
||||
Exclude:
|
||||
- '*.gemspec'
|
||||
- 'vendor/**/*'
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
Lint/UriEscapeUnescape:
|
||||
Enabled: false
|
||||
Metrics/AbcSize:
|
||||
@@ -16,9 +19,7 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 8
|
||||
Metrics/LineLength:
|
||||
Max: 120
|
||||
Max: 10
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Exclude:
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.2
|
||||
2.7.1
|
||||
|
||||
17
.simplecov
17
.simplecov
@@ -1,4 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
33
.travis.yml
33
.travis.yml
@@ -1,33 +0,0 @@
|
||||
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
|
||||
17
Dockerfile
17
Dockerfile
@@ -1,16 +1,16 @@
|
||||
FROM ruby:2.6.3-alpine AS builder
|
||||
FROM ruby:2.7.1-alpine AS builder
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
|
||||
ARG BUNDLER_ARGS="--jobs=8 --without test development"
|
||||
|
||||
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
|
||||
RUN echo "install: --no-document --no-post-install-message\nupdate: --no-document --no-post-install-message" > /etc/gemrc
|
||||
|
||||
COPY . /wpscan
|
||||
|
||||
RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
|
||||
bundle install --system --clean --no-cache --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
|
||||
# temp fix for https://github.com/bundler/bundler/issues/6680
|
||||
rm -rf /usr/local/bundle/cache
|
||||
bundle config force_ruby_platform true && \
|
||||
bundle config disable_version_check 'true' && \
|
||||
bundle config without "test development" && \
|
||||
bundle config path.system 'true' && \
|
||||
bundle install --gemfile=/wpscan/Gemfile --jobs=8
|
||||
|
||||
WORKDIR /wpscan
|
||||
RUN rake install --trace
|
||||
@@ -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.7.1-alpine
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
|
||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||
@@ -38,4 +38,3 @@ USER wpscan
|
||||
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
||||
|
||||
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
|
||||
CMD ["--help"]
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
||||
31
README.md
31
README.md
@@ -7,15 +7,15 @@
|
||||
<h3 align="center">WPScan</h3>
|
||||
|
||||
<p align="center">
|
||||
WordPress Vulnerability Scanner
|
||||
WordPress Security Scanner
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://wpscan.org/" title="homepage" target="_blank">Homepage</a> - <a href="https://wpscan.io/" title="wpscan.io" target="_blank">WPScan.io</a> - <a href="https://wpvulndb.com/" title="vulnerability database" target="_blank">Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress plugin" target="_blank">WordPress Plugin</a>
|
||||
<a href="https://wpscan.org/" title="homepage" target="_blank">Homepage</a> - <a href="https://wpscan.io/" title="wpscan.io" target="_blank">WPScan.io</a> - <a href="https://wpvulndb.com/" title="vulnerability database" target="_blank">Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress security plugin" target="_blank">WordPress Security Plugin</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a>
|
||||
<a href="https://travis-ci.org/wpscanteam/wpscan" target="_blank"><img src="https://travis-ci.org/wpscanteam/wpscan.svg?branch=master"></a>
|
||||
<a href="https://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://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
|
||||
</p>
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
- 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)
|
||||
### In a Pentesting distribution
|
||||
|
||||
When using a pentesting distubution (such as Kali Linux), it is recommended to install/update wpscan via the package manager if available.
|
||||
|
||||
### From RubyGems
|
||||
|
||||
```shell
|
||||
gem install wpscan
|
||||
@@ -39,18 +43,6 @@ gem install wpscan
|
||||
|
||||
On MacOSX, if a ```Gem::FilePermissionError``` is raised due to the Apple's System Integrity Protection (SIP), either install RVM and install wpscan again, or run ```sudo gem install -n /usr/local/bin wpscan``` (see [#1286](https://github.com/wpscanteam/wpscan/issues/1286))
|
||||
|
||||
### From sources (NOT Recommended)
|
||||
|
||||
Prerequisites: Git
|
||||
|
||||
```shell
|
||||
git clone https://github.com/wpscanteam/wpscan
|
||||
|
||||
cd wpscan/
|
||||
|
||||
bundle install && rake install
|
||||
```
|
||||
|
||||
# Updating
|
||||
|
||||
You can update the local database by using ```wpscan --update```
|
||||
@@ -77,6 +69,8 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
|
||||
|
||||
# Usage
|
||||
|
||||
Full user documentation can be found here; https://github.com/wpscanteam/wpscan/wiki/WPScan-User-Documentation
|
||||
|
||||
```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.
|
||||
@@ -130,6 +124,11 @@ cli_options:
|
||||
api_token: YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
## Load API Token From ENV (since v3.7.10)
|
||||
|
||||
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
|
||||
|
||||
```shell
|
||||
|
||||
6
Rakefile
6
Rakefile
@@ -6,14 +6,18 @@ 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)
|
||||
|
||||
RSpec::Core::RakeTask.new(:spec) { |t| t.rspec_opts = %w{--tag ~slow} }
|
||||
|
||||
exec << :spec
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
@@ -18,9 +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
|
||||
|
||||
raise Error::WpContentDirNotDetected
|
||||
raise Error::WpContentDirNotDetected unless target.content_dir
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ module WPScan
|
||||
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
|
||||
OptChoice.new(
|
||||
['--plugins-detection MODE',
|
||||
'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
|
||||
'Use the supplied mode to enumerate Plugins.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
|
||||
),
|
||||
OptBoolean.new(
|
||||
@@ -62,8 +62,7 @@ module WPScan
|
||||
),
|
||||
OptChoice.new(
|
||||
['--plugins-version-detection MODE',
|
||||
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
|
||||
'or --plugins-detection modes.'],
|
||||
'Use the supplied mode to check plugins\' versions.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
||||
),
|
||||
OptInteger.new(
|
||||
|
||||
@@ -23,27 +23,32 @@ module WPScan
|
||||
]
|
||||
end
|
||||
|
||||
def run
|
||||
return unless ParsedCli.passwords
|
||||
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
||||
end
|
||||
|
||||
attack_opts = {
|
||||
def attack_opts
|
||||
@attack_opts ||= {
|
||||
show_progression: user_interaction?,
|
||||
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
||||
}
|
||||
end
|
||||
|
||||
def run
|
||||
return unless ParsedCli.passwords
|
||||
|
||||
begin
|
||||
found = []
|
||||
|
||||
attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user|
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
||||
end
|
||||
|
||||
attacker.attack(users, ParsedCli.passwords, attack_opts) do |user|
|
||||
found << user
|
||||
|
||||
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
||||
end
|
||||
rescue Error::NoLoginInterfaceDetected => e
|
||||
# TODO: Maybe output that in JSON as well.
|
||||
output('@notice', msg: e.to_s) if user_interaction?
|
||||
ensure
|
||||
output('users', users: found)
|
||||
end
|
||||
@@ -65,6 +70,8 @@ module WPScan
|
||||
|
||||
case ParsedCli.password_attack
|
||||
when :wp_login
|
||||
raise Error::NoLoginInterfaceDetected unless target.login_url
|
||||
|
||||
Finders::Passwords::WpLogin.new(target)
|
||||
when :xmlrpc
|
||||
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||
@@ -81,8 +88,8 @@ module WPScan
|
||||
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/
|
||||
!xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
||||
.run.body.match?(/>\s*405\s*</)
|
||||
|
||||
true
|
||||
else
|
||||
@@ -100,8 +107,10 @@ module WPScan
|
||||
else
|
||||
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
end
|
||||
else
|
||||
elsif target.login_url
|
||||
Finders::Passwords::WpLogin.new(target)
|
||||
else
|
||||
raise Error::NoLoginInterfaceDetected
|
||||
end
|
||||
end
|
||||
|
||||
@@ -113,15 +122,6 @@ module WPScan
|
||||
acc << Model::User.new(elem.chomp)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] wordlist_path
|
||||
#
|
||||
# @return [ Array<String> ]
|
||||
def passwords(wordlist_path)
|
||||
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
|
||||
acc << elem.chomp
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,6 +4,8 @@ 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'])
|
||||
@@ -11,9 +13,9 @@ module WPScan
|
||||
end
|
||||
|
||||
def before_scan
|
||||
return unless ParsedCli.api_token
|
||||
return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
|
||||
|
||||
DB::VulnApi.token = ParsedCli.api_token
|
||||
DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
|
||||
|
||||
api_status = DB::VulnApi.status
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ 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
|
||||
|
||||
@@ -41,7 +40,7 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def potential_urls(opts = {})
|
||||
urls = {}
|
||||
domain_name = target.uri.host[/(^[\w|-]+)/, 1]
|
||||
domain_name = (PublicSuffix.domain(target.uri.host) || target.uri.host)[/(^[\w|-]+)/, 1]
|
||||
|
||||
File.open(opts[:list]).each_with_index do |path, index|
|
||||
path.gsub!('{domain_name}', domain_name)
|
||||
|
||||
@@ -16,8 +16,7 @@ module WPScan
|
||||
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' }
|
||||
interesting_entries: target.directory_listing_entries(path)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,11 +11,7 @@ module WPScan
|
||||
|
||||
return unless target.debug_log?(path)
|
||||
|
||||
Model::DebugLog.new(
|
||||
target.url(path),
|
||||
confidence: 100, found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
|
||||
)
|
||||
Model::DebugLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,12 +11,7 @@ module WPScan
|
||||
|
||||
return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body)
|
||||
|
||||
Model::DuplicatorInstallerLog.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
||||
)
|
||||
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,10 +15,7 @@ module WPScan
|
||||
Model::EmergencyPwdResetScript.new(
|
||||
target.url(path),
|
||||
confidence: /password/i.match?(res.body) ? 100 : 40,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: {
|
||||
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
|
||||
}
|
||||
found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,8 +16,7 @@ module WPScan
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: fpd_entries,
|
||||
references: { url: 'https://www.owasp.org/index.php/Full_Path_Disclosure' }
|
||||
interesting_entries: fpd_entries
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,22 +7,16 @@ module WPScan
|
||||
class MuPlugins < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def passive(_opts = {})
|
||||
pattern = %r{#{target.content_dir}/mu\-plugins/}i
|
||||
pattern = %r{#{target.content_dir}/mu-plugins/}i
|
||||
|
||||
target.in_scope_uris(target.homepage_res) do |uri|
|
||||
target.in_scope_uris(target.homepage_res, '(//@href|//@src)[contains(., "mu-plugins")]') do |uri|
|
||||
next unless uri.path&.match?(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)',
|
||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
||||
)
|
||||
return Model::MuPlugins.new(url, confidence: 70, found_by: 'URLs In Homepage (Passive Detection)')
|
||||
end
|
||||
nil
|
||||
end
|
||||
@@ -37,13 +31,7 @@ module WPScan
|
||||
|
||||
target.mu_plugins = true
|
||||
|
||||
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' }
|
||||
)
|
||||
Model::MuPlugins.new(url, confidence: 80, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,18 +12,12 @@ module WPScan
|
||||
location = res.headers_hash['location']
|
||||
|
||||
return unless [200, 302].include?(res.code)
|
||||
return if res.code == 302 && location =~ /wp-login\.php\?action=register/
|
||||
return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
|
||||
return if res.code == 302 && location&.include?('wp-login.php?action=register')
|
||||
return unless res.code == 200 || res.code == 302 && location&.include?('wp-signup.php')
|
||||
|
||||
target.multisite = true
|
||||
|
||||
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' }
|
||||
)
|
||||
Model::Multisite.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,12 +20,7 @@ module WPScan
|
||||
|
||||
target.registration_enabled = true
|
||||
|
||||
Model::Registration.new(
|
||||
res.effective_url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "Registration is enabled: #{res.effective_url}"
|
||||
)
|
||||
Model::Registration.new(res.effective_url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,12 +13,7 @@ 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,
|
||||
references: { packetstorm: 131_957 }
|
||||
)
|
||||
Model::TmmDbMigrate.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,12 +13,7 @@ module WPScan
|
||||
|
||||
url = target.url(path)
|
||||
|
||||
Model::UploadDirectoryListing.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "Upload directory has listing enabled: #{url}"
|
||||
)
|
||||
Model::UploadDirectoryListing.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,11 +14,7 @@ module WPScan
|
||||
|
||||
return unless SQL_PATTERN.match?(res.body)
|
||||
|
||||
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
|
||||
|
||||
@@ -11,17 +11,7 @@ module WPScan
|
||||
|
||||
return unless res.code == 200
|
||||
|
||||
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'
|
||||
]
|
||||
}
|
||||
)
|
||||
Model::WPCron.new(wp_cron_url, confidence: 60, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
|
||||
def wp_cron_url
|
||||
|
||||
@@ -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
|
||||
|
||||
14
app/finders/main_theme/css_style_in_404_page.rb
Normal file
14
app/finders/main_theme/css_style_in_404_page.rb
Normal 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
|
||||
@@ -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(
|
||||
@@ -20,8 +20,8 @@ module WPScan
|
||||
end
|
||||
|
||||
def passive_from_css_href(res, opts)
|
||||
target.in_scope_uris(res, '//style/@src|//link/@href') do |uri|
|
||||
next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
|
||||
target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri|
|
||||
next unless uri.path =~ %r{/themes/([^/]+)/style.css\z}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], uri.to_s, opts)
|
||||
end
|
||||
@@ -33,7 +33,7 @@ module WPScan
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
|
||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'( ]*}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
||||
end
|
||||
15
app/finders/main_theme/urls_in_404_page.rb
Normal file
15
app/finders/main_theme/urls_in_404_page.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 302 &&
|
||||
[*response.headers['Set-Cookie']]&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
||||
Array(response.headers['Set-Cookie'])&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
|
||||
@@ -12,11 +12,11 @@ module WPScan
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 200 && response.body =~ /blogName/
|
||||
response.code == 200 && response.body.include?('blogName')
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
response.code != 200 && response.body !~ /login_error/i
|
||||
response.code != 200 && response.body !~ /Incorrect username or password/i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,8 +22,30 @@ module WPScan
|
||||
target.multi_call(methods, cache_ttl: 0).run
|
||||
end
|
||||
|
||||
# @param [ IO ] file
|
||||
# @param [ Integer ] passwords_size
|
||||
# @return [ Array<String> ] The passwords from the last checked position in the file until there are
|
||||
# passwords_size passwords retrieved
|
||||
def passwords_from_wordlist(file, passwords_size)
|
||||
pwds = []
|
||||
added_pwds = 0
|
||||
|
||||
return pwds if passwords_size.zero?
|
||||
|
||||
# Make sure that the main code does not call #sysseek or #count etc
|
||||
# otherwise the file descriptor will be set to somwehere else
|
||||
file.each_line(chomp: true) do |line|
|
||||
pwds << line
|
||||
added_pwds += 1
|
||||
|
||||
break if added_pwds == passwords_size
|
||||
end
|
||||
|
||||
pwds
|
||||
end
|
||||
|
||||
# @param [ Array<Model::User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
# @param [ String ] wordlist_path
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
# @option opts [ Integer ] :multicall_max_passwords
|
||||
@@ -33,18 +55,22 @@ module WPScan
|
||||
# TODO: Make rubocop happy about metrics etc
|
||||
#
|
||||
# rubocop:disable all
|
||||
def attack(users, passwords, opts = {})
|
||||
wordlist_index = 0
|
||||
def attack(users, wordlist_path, opts = {})
|
||||
checked_passwords = 0
|
||||
wordlist = File.open(wordlist_path)
|
||||
wordlist_size = wordlist.count
|
||||
max_passwords = opts[:multicall_max_passwords]
|
||||
current_passwords_size = passwords_size(max_passwords, users.size)
|
||||
|
||||
create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
|
||||
create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,
|
||||
show_progression: opts[:show_progression])
|
||||
|
||||
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
|
||||
|
||||
loop do
|
||||
current_users = users.select { |user| user.password.nil? }
|
||||
current_passwords = passwords[wordlist_index, current_passwords_size]
|
||||
wordlist_index += current_passwords_size
|
||||
current_users = users.select { |user| user.password.nil? }
|
||||
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
|
||||
checked_passwords += current_passwords_size
|
||||
|
||||
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
||||
|
||||
@@ -76,16 +102,19 @@ module WPScan
|
||||
break
|
||||
end
|
||||
|
||||
progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
|
||||
begin
|
||||
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
|
||||
rescue ProgressBar::InvalidProgressError
|
||||
end
|
||||
end
|
||||
end
|
||||
# Maybe a progress_bar.stop ?
|
||||
end
|
||||
# rubocop:disable all
|
||||
# rubocop:enable all
|
||||
|
||||
def passwords_size(max_passwords, users_size)
|
||||
return 1 if max_passwords < users_size
|
||||
return 0 if users_size == 0
|
||||
return 0 if users_size.zero?
|
||||
|
||||
max_passwords / users_size
|
||||
end
|
||||
@@ -94,9 +123,13 @@ module WPScan
|
||||
def check_and_output_errors(res)
|
||||
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
||||
|
||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
|
||||
if /parse error. not well formed/i.match?(res.body)
|
||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
|
||||
end
|
||||
|
||||
progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
|
||||
return unless /requested method [^ ]+ does not exist/i.match?(res.body)
|
||||
|
||||
progress_bar.log('The requested method is not supported')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String, nil ] The version number detected from the stable tag
|
||||
def from_stable_tag(body)
|
||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
|
||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i
|
||||
|
||||
number = Regexp.last_match[1]
|
||||
|
||||
@@ -59,7 +59,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String, nil ] The best version number detected from the changelog section
|
||||
def from_changelog_section(body)
|
||||
extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
|
||||
extracted_versions = body.scan(%r{=+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[ \ta-z0-9().\-/]*=+}i)
|
||||
|
||||
return if extracted_versions.nil? || extracted_versions.empty?
|
||||
|
||||
@@ -68,11 +68,9 @@ module WPScan
|
||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||
|
||||
sorted = extracted_versions.sort do |x, y|
|
||||
begin
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
|
||||
sorted.last
|
||||
|
||||
@@ -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) <<
|
||||
|
||||
@@ -19,8 +19,12 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
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))
|
||||
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]
|
||||
end
|
||||
|
||||
16
app/finders/plugins/urls_in_404_page.rb
Normal file
16
app/finders/plugins/urls_in_404_page.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,7 @@ module WPScan
|
||||
|
||||
# @return [ Version ]
|
||||
def style_version
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z.-]+)/i
|
||||
|
||||
Model::Version.new(
|
||||
Regexp.last_match[1],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,8 +19,12 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
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))
|
||||
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]
|
||||
end
|
||||
|
||||
15
app/finders/themes/urls_in_404_page.rb
Normal file
15
app/finders/themes/urls_in_404_page.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ require_relative 'users/oembed_api'
|
||||
require_relative 'users/rss_generator'
|
||||
require_relative 'users/author_id_brute_forcing'
|
||||
require_relative 'users/login_error_messages'
|
||||
require_relative 'users/yoast_seo_author_sitemap.rb'
|
||||
require_relative 'users/yoast_seo_author_sitemap'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
|
||||
@@ -71,11 +71,13 @@ module WPScan
|
||||
return username, 'Display Name', 50 if username
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ String, Addressable::URI ] uri
|
||||
#
|
||||
# @return [ String, nil ]
|
||||
def username_from_author_url(url)
|
||||
url[%r{/author/([^/\b]+)/?}i, 1]
|
||||
def username_from_author_url(uri)
|
||||
uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
|
||||
|
||||
uri.path[%r{/author/([^/\b]+)/?}i, 1]
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
@@ -83,12 +85,12 @@ module WPScan
|
||||
# @return [ String, nil ] The username found
|
||||
def username_from_response(res)
|
||||
# Permalink enabled
|
||||
target.in_scope_uris(res, '//link/@href|//a/@href') do |uri|
|
||||
username = username_from_author_url(uri.to_s)
|
||||
target.in_scope_uris(res, '//@href[contains(., "author/")]') do |uri|
|
||||
username = username_from_author_url(uri)
|
||||
return username if username
|
||||
end
|
||||
|
||||
# No permalink
|
||||
# No permalink, TODO Maybe use xpath to extract the classes ?
|
||||
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
||||
end
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ module WPScan
|
||||
def potential_usernames(res)
|
||||
usernames = []
|
||||
|
||||
target.in_scope_uris(res, '//a/@href') do |uri, node|
|
||||
target.in_scope_uris(res, '//a/@href[contains(., "author")]') 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)
|
||||
|
||||
@@ -37,7 +37,7 @@ module WPScan
|
||||
# usernames from the potential Users found
|
||||
unames = opts[:found].map(&:username)
|
||||
|
||||
[*opts[:list]].each { |uname| unames << uname.chomp }
|
||||
Array(opts[:list]).each { |uname| unames << uname.chomp }
|
||||
|
||||
unames.uniq
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
next unless res.code == 200 && res.body =~ /<dc\:creator>/i
|
||||
next unless res.code == 200 && res.body =~ /<dc:creator>/i
|
||||
|
||||
potential_usernames = []
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ module WPScan
|
||||
loop do
|
||||
current_page += 1
|
||||
|
||||
res = Typhoeus.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
|
||||
res = Browser.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
|
||||
|
||||
total_pages ||= res.headers['X-WP-TotalPages'].to_i
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
found = []
|
||||
|
||||
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
||||
username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
|
||||
username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
|
||||
|
||||
next unless username && !username.strip.empty?
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'wp_items/urls_in_homepage'
|
||||
require_relative 'wp_items/urls_in_page'
|
||||
|
||||
@@ -4,15 +4,19 @@ 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
|
||||
#
|
||||
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
|
||||
# @return [ Array<String> ] The plugins/themes detected in the href, src attributes of the page
|
||||
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(target.homepage_res) do |uri|
|
||||
target.in_scope_uris(page_res, xpath) do |uri|
|
||||
next unless uri.to_s =~ item_attribute_pattern(type)
|
||||
|
||||
slug = Regexp.last_match[1]&.strip
|
||||
@@ -30,7 +34,7 @@ module WPScan
|
||||
def items_from_codes(type, uniq = true)
|
||||
found = []
|
||||
|
||||
target.homepage_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
|
||||
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
@@ -51,7 +55,7 @@ module WPScan
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_code_pattern(type)
|
||||
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
|
||||
@item_code_pattern ||= %r{["'( ]#{item_url_pattern(type)}([^\\/)"']+)}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
@@ -62,9 +66,9 @@ module WPScan
|
||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||
|
||||
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?/}i
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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 = {})
|
||||
|
||||
@@ -8,45 +8,110 @@ module WPScan
|
||||
end
|
||||
|
||||
#
|
||||
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
|
||||
# Some classes are empty 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
|
||||
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
|
||||
|
||||
@@ -38,7 +38,7 @@ module WPScan
|
||||
|
||||
# @return [ Array<String> ]
|
||||
def potential_readme_filenames
|
||||
@potential_readme_filenames ||= [*(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)]
|
||||
@potential_readme_filenames ||= Array((DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ module WPScan
|
||||
# @return [ Theme ]
|
||||
def parent_theme
|
||||
return unless template
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"')]+)["']?\);\s*$/i
|
||||
|
||||
opts = detection_opts.merge(
|
||||
style_url: url(Regexp.last_match[1]),
|
||||
@@ -101,7 +101,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String ]
|
||||
def parse_style_tag(body, tag)
|
||||
value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n\*]+)/i, 1]
|
||||
value = body[/\b#{Regexp.escape(tag)}:[\t ]*([^\r\n*]+)/, 1]
|
||||
|
||||
value && !value.strip.empty? ? value.strip : nil
|
||||
end
|
||||
|
||||
@@ -40,9 +40,9 @@ module WPScan
|
||||
def rce_132_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 1.32 Remote Code Execution',
|
||||
{ exploitdb: ['17602'] },
|
||||
'RCE',
|
||||
'1.33'
|
||||
references: { exploitdb: ['17602'] },
|
||||
type: 'RCE',
|
||||
fixed_in: '1.33'
|
||||
)
|
||||
end
|
||||
|
||||
@@ -50,12 +50,12 @@ module WPScan
|
||||
def rce_webshot_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||
{
|
||||
references: {
|
||||
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
|
||||
cve: '2014-4663'
|
||||
},
|
||||
'RCE',
|
||||
'2.8.14'
|
||||
type: 'RCE',
|
||||
fixed_in: '2.8.14'
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -23,7 +23,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 = URI.decode(slug)
|
||||
@slug = Addressable::URI.unencode(slug)
|
||||
@blog = blog
|
||||
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
||||
|
||||
@@ -39,7 +39,7 @@ module WPScan
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
Array(db_data['vulnerabilities']).each do |json_vuln|
|
||||
vulnerability = Vulnerability.load_from_json(json_vuln)
|
||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||
end
|
||||
@@ -83,11 +83,6 @@ 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 ]
|
||||
@@ -95,7 +90,7 @@ module WPScan
|
||||
return unless @uri
|
||||
return @uri.to_s unless path
|
||||
|
||||
@uri.join(URI.encode(path)).to_s
|
||||
@uri.join(Addressable::URI.encode(path)).to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
@@ -166,7 +161,7 @@ module WPScan
|
||||
# @return [ Typhoeus::Response ]
|
||||
def head_and_get(path, codes = [200], params = {})
|
||||
final_path = +@path_from_blog
|
||||
final_path << URI.encode(path) unless path.nil?
|
||||
final_path << path unless path.nil?
|
||||
|
||||
blog.head_and_get(final_path, codes, params)
|
||||
end
|
||||
|
||||
@@ -53,7 +53,7 @@ module WPScan
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
Array(db_data['vulnerabilities']).each do |json_vuln|
|
||||
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
||||
end
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
_______________________________________________________________
|
||||
__ _______ _____
|
||||
\ \ / / __ \ / ____|
|
||||
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
|
||||
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
|
||||
\ /\ / | | ____) | (__| (_| | | | |
|
||||
\/ \/ |_| |_____/ \___|\__,_|_| |_|
|
||||
__ _______ _____
|
||||
\ \ / / __ \ / ____|
|
||||
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
|
||||
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
|
||||
\ /\ / | | ____) | (__| (_| | | | |
|
||||
\/ \/ |_| |_____/ \___|\__,_|_| |_|
|
||||
|
||||
WordPress Security Scanner by the WPScan Team
|
||||
Version <%= WPScan::VERSION %>
|
||||
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_
|
||||
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
|
||||
_______________________________________________________________
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
| Detected By: <%= @item.found_by %>
|
||||
| Found By: <%= @item.found_by %>
|
||||
<% @item.interesting_entries.each do |entry| -%>
|
||||
| - <%= entry %>
|
||||
<% end -%>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<% if @users.empty? -%>
|
||||
<%= notice_icon %> No Valid Passwords Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Valid Combinations Found:
|
||||
<%= critical_icon %> Valid Combinations Found:
|
||||
<% @users.each do |user| -%>
|
||||
| Username: <%= user.username %>, Password: <%= user.password %>
|
||||
<% end -%>
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
<% 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.
|
||||
<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up
|
||||
<% end -%>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
| <%= critical_icon %> Title: <%= @v.title %>
|
||||
<% if @v.cvss -%>
|
||||
| CVSS: <%= @v.cvss[:score] %> (<%= @v.cvss[:vector] %>)
|
||||
<% end -%>
|
||||
<% if @v.fixed_in -%>
|
||||
| Fixed in: <%= @v.fixed_in %>
|
||||
<% end -%>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"@_WPScan_",
|
||||
"@ethicalhack3r",
|
||||
"@erwan_lr",
|
||||
"@_FireFart_"
|
||||
"@firefart"
|
||||
],
|
||||
"sponsor": <%= WPScan::DB::Sponsor.text.to_json %>
|
||||
},
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<% vulns.each_with_index do |v, index| -%>
|
||||
{
|
||||
"title": <%= v.title.to_json %>,
|
||||
<% if v.cvss -%>
|
||||
"cvss": <%= v.cvss.to_json %>,
|
||||
<% end -%>
|
||||
"fixed_in": <%= v.fixed_in.to_json %>,
|
||||
"references": <%= v.references.to_json %>
|
||||
}<% unless index == last_index -%>,<% end -%>
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"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."
|
||||
"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 -%>
|
||||
},
|
||||
@@ -67,13 +67,13 @@ module WPScan
|
||||
# @return [ Hash ] The params for Typhoeus::Request
|
||||
# @note Those params can't be overriden by CLI options
|
||||
def request_params
|
||||
@request_params ||= {
|
||||
@request_params ||= Browser.instance.default_connect_request_params.merge(
|
||||
timeout: 600,
|
||||
connecttimeout: 300,
|
||||
accept_encoding: 'gzip, deflate',
|
||||
cache_ttl: 0,
|
||||
headers: { 'User-Agent' => Browser.instance.default_user_agent, 'Referer' => nil }
|
||||
}
|
||||
headers: { 'User-Agent' => Browser.instance.default_user_agent }
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ String ] The raw file URL associated with the given filename
|
||||
@@ -85,7 +85,7 @@ module WPScan
|
||||
def remote_file_checksum(filename)
|
||||
url = "#{remote_file_url(filename)}.sha512"
|
||||
|
||||
res = Browser.get(url, request_params)
|
||||
res = Typhoeus.get(url, request_params)
|
||||
raise Error::Download, res if res.timed_out? || res.code != 200
|
||||
|
||||
res.body.chomp
|
||||
@@ -126,7 +126,7 @@ module WPScan
|
||||
file_path = local_file_path(filename)
|
||||
file_url = remote_file_url(filename)
|
||||
|
||||
res = Browser.get(file_url, request_params)
|
||||
res = Typhoeus.get(file_url, request_params)
|
||||
raise Error::Download, res if res.timed_out? || res.code != 200
|
||||
|
||||
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
||||
@@ -139,24 +139,22 @@ module WPScan
|
||||
updated = []
|
||||
|
||||
FILES.each do |filename|
|
||||
begin
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
|
||||
# Checking if the file needs to be updated
|
||||
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
# Checking if the file needs to be updated
|
||||
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
|
||||
create_backup(filename)
|
||||
dl_checksum = download(filename)
|
||||
create_backup(filename)
|
||||
dl_checksum = download(filename)
|
||||
|
||||
raise "#{filename}: checksums do not match" unless dl_checksum == db_checksum
|
||||
raise Error::ChecksumsMismatch, filename unless dl_checksum == db_checksum
|
||||
|
||||
updated << filename
|
||||
rescue StandardError => e
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
||||
end
|
||||
updated << filename
|
||||
rescue StandardError => e
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
||||
end
|
||||
|
||||
File.write(last_update_file, Time.now)
|
||||
|
||||
@@ -21,8 +21,10 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def self.get(path, params = {})
|
||||
return {} unless token
|
||||
return {} if path.end_with?('/latest') # Remove this when api/v4 is up
|
||||
|
||||
res = Browser.get(uri.join(path), params.merge(request_params))
|
||||
# Typhoeus.get is used rather than Browser.get to avoid merging irrelevant params from the CLI
|
||||
res = Typhoeus.get(uri.join(path), default_request_params.merge(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)
|
||||
@@ -64,15 +66,14 @@ module WPScan
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.request_params
|
||||
{
|
||||
# @note Those params can not be overriden by CLI options
|
||||
def self.default_request_params
|
||||
Browser.instance.default_connect_request_params.merge(
|
||||
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
|
||||
|
||||
@@ -8,5 +8,17 @@ module WPScan
|
||||
'Update required, you can not run a scan if a database file is missing.'
|
||||
end
|
||||
end
|
||||
|
||||
class ChecksumsMismatch < Standard
|
||||
attr_reader :db_file
|
||||
|
||||
def initialize(db_file)
|
||||
@db_file = db_file
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{db_file}: checksums do not match. Please try again in a few minutes."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,5 +29,11 @@ module WPScan
|
||||
' use the --scope option or make sure the --url value given is the correct one'
|
||||
end
|
||||
end
|
||||
|
||||
class NoLoginInterfaceDetected < Standard
|
||||
def to_s
|
||||
'Could not find a login interface to perform the password attack against'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ module WPScan
|
||||
end
|
||||
|
||||
# Needed to have inheritance of the @child_class_constants
|
||||
# If inheritance is not needed, then the #child_class_constant can be used in the classe definition, ie
|
||||
# If inheritance is not needed, then the #child_class_constant can be used in the class definition, ie
|
||||
# child_class_constant :FILES, PATTERN: /aaa/i
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ module WPScan
|
||||
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super.merge(
|
||||
PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 70
|
||||
PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[.\d]+)/, CONFIDENCE: 70
|
||||
)
|
||||
end
|
||||
|
||||
@@ -21,13 +21,11 @@ module WPScan
|
||||
parsers = ALLOWED_PARSERS.include?(self.class::PARSER) ? [self.class::PARSER] : ALLOWED_PARSERS
|
||||
|
||||
parsers.each do |parser|
|
||||
begin
|
||||
parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
|
||||
parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
|
||||
|
||||
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
|
||||
nil # Make sure nil is returned in case none of the parsers managed to parse the body correctly
|
||||
|
||||
@@ -9,7 +9,7 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)\=(?<v>\d+\.[\.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
|
||||
XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)=(?<v>\d+\.[.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: nil, PATTERN: /\A(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 60
|
||||
XPATH: nil, PATTERN: /\A(?<v>\d+\.[.\d]+)/, CONFIDENCE: 60
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -33,10 +33,12 @@ module WPScan
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(PATTERN: /ver\=(?<v>\d+\.[\.\d]+)/i)
|
||||
@child_class_constants ||= super().merge(PATTERN: /ver=(?<v>\d+\.[.\d]+)/i)
|
||||
end
|
||||
end
|
||||
|
||||
# This one has been disabled from the DF.yml as it was causing FPs when a plugin had numerous
|
||||
# files matching a known WP version.
|
||||
class WpItemQueryParameter < QueryParameter
|
||||
def xpath
|
||||
@xpath ||=
|
||||
|
||||
@@ -13,7 +13,7 @@ end
|
||||
#
|
||||
# @return [ Symbol ]
|
||||
def classify_slug(slug)
|
||||
classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/\-{1,}/, '_').camelize.to_s
|
||||
classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/-{1,}/, '_').camelize.to_s
|
||||
classified = "D_#{classified}" if /\d/.match?(classified[0])
|
||||
|
||||
classified.to_sym
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module WPScan
|
||||
# References module (which should be included along with the CMSScanner::References)
|
||||
# to allow the use of the wpvulndb reference
|
||||
# to allow the use of the wpvulndb reference.
|
||||
module References
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ module WPScan
|
||||
# @return [ Boolean ]
|
||||
def vulnerable?
|
||||
[@wp_version, @main_theme, @plugins, @themes, @timthumbs].each do |e|
|
||||
[*e].each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
|
||||
Array(e).each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
|
||||
end
|
||||
|
||||
return true unless [*@config_backups].empty?
|
||||
return true unless [*@db_exports].empty?
|
||||
return true unless Array(@config_backups).empty?
|
||||
return true unless Array(@db_exports).empty?
|
||||
|
||||
[*@users].each { |u| return true if u.password }
|
||||
Array(@users).each { |u| return true if u.password }
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
@@ -11,9 +11,9 @@ module WPScan
|
||||
module WordPress
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
|
||||
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
|
||||
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
|
||||
@@ -24,34 +24,46 @@ module WPScan
|
||||
|
||||
# @param [ Symbol ] detection_mode
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
# @return [ Boolean ] Whether or not the target is running WordPress
|
||||
def wordpress?(detection_mode)
|
||||
in_scope_uris(homepage_res) do |uri|
|
||||
return true if WORDPRESS_PATTERN.match?(uri.path) || WP_JSON_OEMBED_PATTERN.match?(uri.path)
|
||||
end
|
||||
|
||||
return true if homepage_res.html.css('meta[name="generator"]').any? do |node|
|
||||
/wordpress/i.match?(node['content'])
|
||||
end
|
||||
|
||||
return true unless comments_from_page(/wordpress/i, homepage_res).empty?
|
||||
|
||||
return true if homepage_res.html.xpath('//script[not(@src)]').any? do |node|
|
||||
WP_ADMIN_AJAX_PATTERN.match?(node.text)
|
||||
[homepage_res, error_404_res].each do |page_res|
|
||||
return true if wordpress_from_meta_comments_or_scripts?(page_res)
|
||||
end
|
||||
|
||||
if %i[mixed aggressive].include?(detection_mode)
|
||||
%w[wp-admin/install.php wp-login.php].each do |path|
|
||||
return true if in_scope_uris(Browser.get_and_follow_location(url(path))).any? do |uri|
|
||||
WORDPRESS_PATTERN.match?(uri.path)
|
||||
res = Browser.get_and_follow_location(url(path))
|
||||
|
||||
next unless res.code == 200
|
||||
|
||||
in_scope_uris(res, '//link/@href|//script/@src') do |uri|
|
||||
return true if WORDPRESS_PATTERN.match?(uri.path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @return [ Boolean ]
|
||||
def wordpress_from_meta_comments_or_scripts?(response)
|
||||
in_scope_uris(response, '//link/@href|//script/@src') 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
|
||||
@@ -92,8 +104,9 @@ module WPScan
|
||||
|
||||
unless content_dir
|
||||
pattern = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
|
||||
xpath = '(//@href|//@src)[contains(., "wp.com")]'
|
||||
|
||||
uris_from_page(homepage_res) do |uri|
|
||||
uris_from_page(homepage_res, xpath) do |uri|
|
||||
return true if uri.to_s.match?(pattern)
|
||||
end
|
||||
end
|
||||
@@ -126,15 +139,16 @@ module WPScan
|
||||
# the first time the method is called, and the effective_url is then used
|
||||
# if suitable, otherwise the default wp-login will be.
|
||||
#
|
||||
# @return [ String ] The URL to the login page
|
||||
# @return [ String, false ] The URL to the login page or false if not detected
|
||||
def login_url
|
||||
return @login_url if @login_url
|
||||
return @login_url unless @login_url.nil?
|
||||
|
||||
@login_url = url('wp-login.php')
|
||||
@login_url = url('wp-login.php') # TODO: url(ParsedCli.login_uri)
|
||||
|
||||
res = Browser.get_and_follow_location(@login_url)
|
||||
|
||||
@login_url = res.effective_url if res.effective_url =~ /wp\-login\.php\z/i && in_scope?(res.effective_url)
|
||||
@login_url = res.effective_url if res.effective_url =~ /wp-login\.php\z/i && in_scope?(res.effective_url)
|
||||
@login_url = false if res.code == 404
|
||||
|
||||
@login_url
|
||||
end
|
||||
|
||||
@@ -19,13 +19,15 @@ module WPScan
|
||||
# scope_url_pattern is from CMSScanner::Target
|
||||
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)
|
||||
end
|
||||
[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, homepage_res) do |match|
|
||||
return @content_dir = match[1]
|
||||
# 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
|
||||
|
||||
return @content_dir = 'wp-content' if default_content_dir_exists?
|
||||
@@ -69,7 +71,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String ]
|
||||
def plugin_url(slug)
|
||||
plugins_uri.join("#{URI.encode(slug)}/").to_s
|
||||
plugins_uri.join("#{Addressable::URI.encode(slug)}/").to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
@@ -91,7 +93,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String ]
|
||||
def theme_url(slug)
|
||||
themes_uri.join("#{URI.encode(slug)}/").to_s
|
||||
themes_uri.join("#{Addressable::URI.encode(slug)}/").to_s
|
||||
end
|
||||
|
||||
# @return [ String, False ] String of the sub_dir found, false otherwise
|
||||
@@ -102,10 +104,13 @@ module WPScan
|
||||
return @sub_dir unless @sub_dir.nil?
|
||||
|
||||
# url_pattern is from CMSScanner::Target
|
||||
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i
|
||||
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp-includes/)}i
|
||||
xpath = '(//@src|//@href|//@data-src)[contains(., "xmlrpc.php") or contains(., "wp-includes/")]'
|
||||
|
||||
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, xpath) do |uri|
|
||||
return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
|
||||
end
|
||||
end
|
||||
|
||||
@sub_dir = false
|
||||
@@ -119,9 +124,9 @@ module WPScan
|
||||
def url(path = nil)
|
||||
return @uri.to_s unless path
|
||||
|
||||
if %r{wp\-content/plugins}i.match?(path)
|
||||
if %r{wp-content/plugins}i.match?(path)
|
||||
path = +path.gsub('wp-content/plugins', plugins_dir)
|
||||
elsif /wp\-content/i.match?(path)
|
||||
elsif /wp-content/i.match?(path)
|
||||
path = +path.gsub('wp-content', content_dir)
|
||||
elsif path[0] != '/' && sub_dir
|
||||
path = "#{sub_dir}/#{path}"
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
# Version
|
||||
module WPScan
|
||||
VERSION = '3.7.3'
|
||||
VERSION = '3.8.4'
|
||||
end
|
||||
|
||||
@@ -18,9 +18,10 @@ module WPScan
|
||||
|
||||
new(
|
||||
json_data['title'],
|
||||
references,
|
||||
json_data['vuln_type'],
|
||||
json_data['fixed_in']
|
||||
references: references,
|
||||
type: json_data['vuln_type'],
|
||||
fixed_in: json_data['fixed_in'],
|
||||
cvss: json_data['cvss']&.symbolize_keys
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,7 +70,7 @@ describe WPScan::Controller::Core do
|
||||
let(:cli_args) { "#{super()} --no-update" }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { core.update_db_required? }. to raise_error(WPScan::Error::MissingDatabaseFile)
|
||||
expect { core.update_db_required? }.to raise_error(WPScan::Error::MissingDatabaseFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
XMLRPC_FAILED_BODY = '
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<methodResponse>
|
||||
<fault>
|
||||
<value>
|
||||
<struct>
|
||||
<member>
|
||||
<name>faultCode</name>
|
||||
<value><int>405</int></value>
|
||||
</member>
|
||||
<member>
|
||||
<name>faultString</name>
|
||||
<value><string>%s</string></value>
|
||||
</member>
|
||||
</struct>
|
||||
</value>
|
||||
</fault>
|
||||
</methodResponse>'
|
||||
|
||||
describe WPScan::Controller::PasswordAttack do
|
||||
subject(:controller) { described_class.new }
|
||||
let(:target_url) { 'http://ex.lo/' }
|
||||
@@ -21,7 +40,7 @@ describe WPScan::Controller::PasswordAttack do
|
||||
|
||||
describe '#users' do
|
||||
context 'when no --usernames' do
|
||||
it 'calles target.users' do
|
||||
it 'calls target.users' do
|
||||
expect(controller.target).to receive(:users)
|
||||
controller.users
|
||||
end
|
||||
@@ -40,10 +59,6 @@ describe WPScan::Controller::PasswordAttack do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#passwords' do
|
||||
xit
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
context 'when no --passwords is supplied' do
|
||||
it 'does not run the attacker' do
|
||||
@@ -85,20 +100,34 @@ describe WPScan::Controller::PasswordAttack do
|
||||
end
|
||||
|
||||
context 'when wp.getUsersBlogs method listed' do
|
||||
before { expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2]) }
|
||||
before do
|
||||
expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2])
|
||||
|
||||
stub_request(:post, xmlrpc.url).to_return(body: body)
|
||||
end
|
||||
|
||||
context 'when wp.getUsersBlogs method disabled' do
|
||||
it 'returns false' do
|
||||
stub_request(:post, xmlrpc.url).to_return(body: 'XML-RPC services are disabled on this site.')
|
||||
context 'when blog is in EN' do
|
||||
let(:body) { format(XMLRPC_FAILED_BODY, 'XML-RPC services are disabled on this site.') }
|
||||
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
|
||||
it 'returns false' do
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when blog is in FR' do
|
||||
let(:body) { format(XMLRPC_FAILED_BODY, 'Les services XML-RPC sont désactivés sur ce site.') }
|
||||
|
||||
it 'returns false' do
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wp.getUsersBlogs method enabled' do
|
||||
it 'returns true' do
|
||||
stub_request(:post, xmlrpc.url).to_return(body: 'Incorrect username or password.')
|
||||
let(:body) { 'Incorrect username or password.' }
|
||||
|
||||
it 'returns true' do
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be true
|
||||
end
|
||||
end
|
||||
@@ -107,15 +136,34 @@ describe WPScan::Controller::PasswordAttack do
|
||||
end
|
||||
|
||||
describe '#attacker' do
|
||||
before do
|
||||
allow(controller.target).to receive(:sub_dir)
|
||||
controller.target.instance_variable_set(:@login_url, nil)
|
||||
end
|
||||
|
||||
context 'when --password-attack provided' do
|
||||
let(:cli_args) { "#{super()} --password-attack #{attack}" }
|
||||
|
||||
context 'when wp-login' do
|
||||
before { stub_request(:get, controller.target.url('wp-login.php')).to_return(status: status) }
|
||||
|
||||
let(:attack) { 'wp-login' }
|
||||
|
||||
it 'returns the correct object' do
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
context 'when available' do
|
||||
let(:status) { 200 }
|
||||
|
||||
it 'returns the correct object' do
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not available (404)' do
|
||||
let(:status) { 404 }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { controller.attacker }.to raise_error(WPScan::Error::NoLoginInterfaceDetected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -172,11 +220,26 @@ describe WPScan::Controller::PasswordAttack do
|
||||
|
||||
context 'when automatic detection' do
|
||||
context 'when xmlrpc_get_users_blogs_enabled? is false' do
|
||||
it 'returns the WpLogin' do
|
||||
before do
|
||||
expect(controller).to receive(:xmlrpc_get_users_blogs_enabled?).and_return(false)
|
||||
stub_request(:get, controller.target.url('wp-login.php')).to_return(status: status)
|
||||
end
|
||||
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
context 'when wp-login available' do
|
||||
let(:status) { 200 }
|
||||
|
||||
it 'returns the WpLogin' do
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wp-login.php not available' do
|
||||
let(:status) { 404 }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { controller.attacker }.to raise_error(WPScan::Error::NoLoginInterfaceDetected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -74,20 +74,40 @@ describe WPScan::Controller::VulnApi do
|
||||
context 'when limited requests' do
|
||||
let(:requests) { 100 }
|
||||
|
||||
it 'does not raise an error' do
|
||||
it 'sets the token and does not raise an error' do
|
||||
expect { controller.before_scan }.to_not raise_error
|
||||
|
||||
expect(WPScan::DB::VulnApi.token).to eql 'token'
|
||||
end
|
||||
|
||||
context 'when unlimited requests' do
|
||||
let(:requests) { 'Unlimited' }
|
||||
|
||||
it 'does not raise an error' do
|
||||
it 'sets the token and does not raise an error' do
|
||||
expect { controller.before_scan }.to_not raise_error
|
||||
|
||||
expect(WPScan::DB::VulnApi.token).to eql 'token'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token in ENV' do
|
||||
before do
|
||||
ENV[described_class::ENV_KEY] = 'token-from-env'
|
||||
|
||||
expect(WPScan::DB::VulnApi)
|
||||
.to receive(:status)
|
||||
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => 'Unlimited')
|
||||
end
|
||||
|
||||
it 'sets the token and does not raise an error' do
|
||||
expect { controller.before_scan }.to_not raise_error
|
||||
|
||||
expect(WPScan::DB::VulnApi.token).to eql 'token-from-env'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,10 +9,10 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
|
||||
describe '#potential_urls' do
|
||||
before do
|
||||
expect(target).to receive(:sub_dir).at_least(1).and_return(false)
|
||||
allow(target).to receive(:sub_dir).and_return(false)
|
||||
end
|
||||
|
||||
it 'replace {domain_name} by its value' do
|
||||
it 'replaces {domain_name} by its value' do
|
||||
expect(finder.potential_urls(opts).keys).to eql %w[
|
||||
http://ex.lo/aa/ex.sql
|
||||
http://ex.lo/aa/wordpress.sql
|
||||
@@ -22,11 +22,53 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
http://ex.lo/aa/backups/db_backup.sql
|
||||
]
|
||||
end
|
||||
|
||||
%w[dev poc www].each do |sub_domain|
|
||||
context "when #{sub_domain} sub-domain" do
|
||||
let(:url) { "https://#{sub_domain}.domain.tld" }
|
||||
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/domain.sql"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multi-level tlds' do
|
||||
let(:url) { 'https://something.com.tr' }
|
||||
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include 'https://something.com.tr/something.sql'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when multi-level tlds and sub-domain' do
|
||||
let(:url) { 'https://dev.something.com.tr' }
|
||||
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include 'https://dev.something.com.tr/something.sql'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when some weird stuff' do
|
||||
let(:url) { 'https://098f6bcd4621d373cade4e832627b4f6.aa-bb-ccc-dd.domain-test.com' }
|
||||
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/domain-test.sql"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a non standard URL' do
|
||||
let(:url) { 'http://dc-2' }
|
||||
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/dc-2.sql"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#aggressive' do
|
||||
before do
|
||||
expect(target).to receive(:sub_dir).at_least(1).and_return(false)
|
||||
allow(target).to receive(:sub_dir).and_return(false)
|
||||
expect(target).to receive(:head_or_get_params).and_return(method: :head)
|
||||
|
||||
finder.potential_urls(opts).each_key do |url|
|
||||
|
||||
@@ -6,8 +6,55 @@ describe WPScan::Finders::InterestingFindings::MuPlugins do
|
||||
let(:url) { 'http://ex.lo/' }
|
||||
let(:fixtures) { FINDERS_FIXTURES.join('interesting_findings', 'mu_plugins') }
|
||||
|
||||
before do
|
||||
expect(target).to receive(:content_dir).at_least(1).and_return('wp-content')
|
||||
end
|
||||
|
||||
describe '#passive' do
|
||||
xit
|
||||
before { stub_request(:get, url).to_return(body: body) }
|
||||
|
||||
context 'when no uris' do
|
||||
let(:body) { '' }
|
||||
|
||||
its(:passive) { should be nil }
|
||||
end
|
||||
|
||||
context 'when a large amount of unrelated uris' do
|
||||
let(:body) do
|
||||
Array.new(250) { |i| "<a href='#{url}#{i}.html'>Some Link</a><img src='#{url}img-#{i}.png'/>" }.join("\n")
|
||||
end
|
||||
|
||||
it 'should not take a while to process the page' do
|
||||
time_start = Time.now
|
||||
result = finder.passive
|
||||
time_end = Time.now
|
||||
|
||||
expect(result).to be nil
|
||||
expect(time_end - time_start).to be < 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when uris' do
|
||||
let(:body) { File.read(fixtures.join(fixture)) }
|
||||
|
||||
context 'when none matching' do
|
||||
let(:fixture) { 'no_match.html' }
|
||||
|
||||
its(:passive) { should be nil }
|
||||
end
|
||||
|
||||
context 'when matching via href' do
|
||||
let(:fixture) { 'match_href.html' }
|
||||
|
||||
its(:passive) { should be_a WPScan::Model::MuPlugins }
|
||||
end
|
||||
|
||||
context 'when matching from src' do
|
||||
let(:fixture) { 'match_src.html' }
|
||||
|
||||
its(:passive) { should be_a WPScan::Model::MuPlugins }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#aggressive' do
|
||||
|
||||
11
spec/app/finders/main_theme/css_style_in_404_page_spec.rb
Normal file
11
spec/app/finders/main_theme/css_style_in_404_page_spec.rb
Normal 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
|
||||
@@ -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'
|
||||
)
|
||||
11
spec/app/finders/main_theme/urls_in_404_page_spec.rb.rb
Normal file
11
spec/app/finders/main_theme/urls_in_404_page_spec.rb.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user