Compare commits
300 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
630752787a | ||
|
|
c07ecc58cb | ||
|
|
89fccfe7b7 | ||
|
|
ceeb7e538b | ||
|
|
8dab57b59c | ||
|
|
7a00cd8db1 | ||
|
|
daa0915bca | ||
|
|
ca6b6a30d8 | ||
|
|
09f2640879 | ||
|
|
f61c55b350 | ||
|
|
78d0c2540c | ||
|
|
1d0426e816 | ||
|
|
103a4049c8 | ||
|
|
cbcb1dcb33 | ||
|
|
9c36293382 | ||
|
|
2fb36dc425 | ||
|
|
c717ba5a71 | ||
|
|
7572518e3b | ||
|
|
f670133a82 | ||
|
|
a6bbf41e82 | ||
|
|
622c16932a | ||
|
|
5fd7e0ed22 | ||
|
|
d9f6c71015 | ||
|
|
61a3106b3b | ||
|
|
20eb2d825d | ||
|
|
906557d2ec | ||
|
|
c1e278ea80 | ||
|
|
e2d616a53f | ||
|
|
c6802ccdd2 | ||
|
|
abd50fd037 | ||
|
|
4515be53b4 | ||
|
|
920a25bb25 | ||
|
|
648dd05069 | ||
|
|
713edcecca | ||
|
|
ac16a951c5 | ||
|
|
1043bcb267 | ||
|
|
22979a1a77 | ||
|
|
3039d2e7eb | ||
|
|
557dee2d8c | ||
|
|
a506adcb64 | ||
|
|
3bfb120646 | ||
|
|
43e613aa52 | ||
|
|
0d930ed605 | ||
|
|
2014f1e4b3 | ||
|
|
4889d17e0a | ||
|
|
494d31215d | ||
|
|
582bdea431 | ||
|
|
ecf7df9c01 | ||
|
|
a9760e8817 | ||
|
|
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 | ||
|
|
9da326967b | ||
|
|
62600b3a66 | ||
|
|
b236138fb5 | ||
|
|
40c2e9a54b | ||
|
|
a9062db57f | ||
|
|
2621404c5f | ||
|
|
c47211ca79 | ||
|
|
e39a192e8d | ||
|
|
d85035d5ef | ||
|
|
de09a97343 | ||
|
|
a6855345d7 | ||
|
|
a53f88b626 | ||
|
|
7048c82124 | ||
|
|
6aa7cda478 | ||
|
|
ff339b9a8c | ||
|
|
8898cc20fe | ||
|
|
770d1da280 | ||
|
|
6ba4e8a29b | ||
|
|
953ca68495 | ||
|
|
4289dfb37d | ||
|
|
4f6f2f436a | ||
|
|
237979a479 | ||
|
|
2e48968fd3 | ||
|
|
9a0c4a5c8f | ||
|
|
9a011f0007 | ||
|
|
3f907a706f | ||
|
|
9446141716 | ||
|
|
1994826af8 | ||
|
|
ab950d6ffc | ||
|
|
b77e611a90 | ||
|
|
86f0284894 | ||
|
|
9bbe014dfe | ||
|
|
ad92c95500 | ||
|
|
d360190382 | ||
|
|
1737c8a7f6 | ||
|
|
cde262fd66 |
@@ -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
|
||||
25
.rubocop.yml
25
.rubocop.yml
@@ -1,19 +1,18 @@
|
||||
require: rubocop-performance
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
NewCops: enable
|
||||
TargetRubyVersion: 2.5
|
||||
Exclude:
|
||||
- '*.gemspec'
|
||||
- 'vendor/**/*'
|
||||
ClassVars:
|
||||
Enabled: false
|
||||
LineLength:
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
Lint/ConstantDefinitionInBlock:
|
||||
Enabled: false
|
||||
Lint/MissingSuper:
|
||||
Enabled: false
|
||||
Lint/UriEscapeUnescape:
|
||||
Enabled: false
|
||||
MethodLength:
|
||||
Max: 20
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/AbcSize:
|
||||
Max: 25
|
||||
Metrics/BlockLength:
|
||||
@@ -24,7 +23,15 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 8
|
||||
Max: 10
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 11
|
||||
Style/ClassVars:
|
||||
Enabled: false
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
Style/FormatStringToken:
|
||||
|
||||
@@ -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
|
||||
18
Dockerfile
18
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,8 +19,9 @@ 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>"
|
||||
LABEL org.opencontainers.image.source https://github.com/wpscanteam/wpscan
|
||||
|
||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||
|
||||
@@ -38,4 +39,3 @@ USER wpscan
|
||||
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
||||
|
||||
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
|
||||
CMD ["--help"]
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -27,9 +27,7 @@ Example cases which do not require a commercial license, and thus fall under the
|
||||
- Using WPScan to test your own systems.
|
||||
- Any non-commercial use of WPScan.
|
||||
|
||||
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.
|
||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - contact@wpscan.com.
|
||||
|
||||
Free-use Terms and Conditions;
|
||||
|
||||
|
||||
41
README.md
41
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.com/" title="homepage" target="_blank">WPScan WordPress 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,13 +69,21 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
|
||||
|
||||
# Usage
|
||||
|
||||
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings. If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
|
||||
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.
|
||||
As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'.
|
||||
|
||||
For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo)
|
||||
|
||||
The DB is located at ~/.wpscan/db
|
||||
|
||||
## Vulnerability Database
|
||||
|
||||
The WPScan CLI tool uses the [WPScan API](https://wpscan.com/api) to retrieve WordPress vulnerability data in real time. For WPScan to retrieve the vulnerability data an API token must be supplied via the `--api-token` option, or via a configuration file, as discussed below. An API token can be obtained by registering an account on [WPScan](https://wpscan.com/register). Up to 50 API requests per day are given free of charge to registered users. Once the 50 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data. Users can upgrade to paid API usage to increase their API limits within their user profile on [WPScan](https://wpscan.com/).
|
||||
|
||||
## Load CLI options from file/s
|
||||
|
||||
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
|
||||
@@ -124,7 +124,12 @@ cli_options:
|
||||
api_token: YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
Enumerating usernames
|
||||
## 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
|
||||
wpscan --url https://target.tld/ --enumerate u
|
||||
|
||||
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(ParsedCli.detection_mode)
|
||||
|
||||
raise Error::WpContentDirNotDetected
|
||||
raise Error::WpContentDirNotDetected unless target.content_dir
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,10 +18,10 @@ module WPScan
|
||||
choices: {
|
||||
vp: OptBoolean.new(['--vulnerable-plugins']),
|
||||
ap: OptBoolean.new(['--all-plugins']),
|
||||
p: OptBoolean.new(['--plugins']),
|
||||
p: OptBoolean.new(['--popular-plugins']),
|
||||
vt: OptBoolean.new(['--vulnerable-themes']),
|
||||
at: OptBoolean.new(['--all-themes']),
|
||||
t: OptBoolean.new(['--themes']),
|
||||
t: OptBoolean.new(['--popular-themes']),
|
||||
tt: OptBoolean.new(['--timthumbs']),
|
||||
cb: OptBoolean.new(['--config-backups']),
|
||||
dbe: OptBoolean.new(['--db-exports']),
|
||||
@@ -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,14 +62,13 @@ 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(
|
||||
['--plugins-threshold THRESHOLD',
|
||||
'Raise an error when the number of detected plugins via known locations reaches the threshold. ' \
|
||||
'Set to 0 to ignore the threshold.'], default: 100
|
||||
'Set to 0 to ignore the threshold.'], default: 100, advanced: true
|
||||
)
|
||||
]
|
||||
end
|
||||
@@ -98,7 +97,7 @@ module WPScan
|
||||
OptInteger.new(
|
||||
['--themes-threshold THRESHOLD',
|
||||
'Raise an error when the number of detected themes via known locations reaches the threshold. ' \
|
||||
'Set to 0 to ignore the threshold.'], default: 20
|
||||
'Set to 0 to ignore the threshold.'], default: 20, advanced: true
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -56,7 +56,7 @@ module WPScan
|
||||
#
|
||||
# @return [ Boolean ] Wether or not to enumerate the plugins
|
||||
def enum_plugins?(opts)
|
||||
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
||||
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
||||
end
|
||||
|
||||
def enum_plugins
|
||||
@@ -92,7 +92,7 @@ module WPScan
|
||||
|
||||
if opts[:enumerate][:all_plugins]
|
||||
DB::Plugins.all_slugs
|
||||
elsif opts[:enumerate][:plugins]
|
||||
elsif opts[:enumerate][:popular_plugins]
|
||||
DB::Plugins.popular_slugs
|
||||
else
|
||||
DB::Plugins.vulnerable_slugs
|
||||
@@ -103,7 +103,7 @@ module WPScan
|
||||
#
|
||||
# @return [ Boolean ] Wether or not to enumerate the themes
|
||||
def enum_themes?(opts)
|
||||
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
||||
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
||||
end
|
||||
|
||||
def enum_themes
|
||||
@@ -139,7 +139,7 @@ module WPScan
|
||||
|
||||
if opts[:enumerate][:all_themes]
|
||||
DB::Themes.all_slugs
|
||||
elsif opts[:enumerate][:themes]
|
||||
elsif opts[:enumerate][:popular_themes]
|
||||
DB::Themes.popular_slugs
|
||||
else
|
||||
DB::Themes.vulnerable_slugs
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,14 +9,9 @@ module WPScan
|
||||
def aggressive(_opts = {})
|
||||
path = 'installer-log.txt'
|
||||
|
||||
return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body)
|
||||
return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.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
|
||||
#
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
slugs = items_from_links('themes', false) + items_from_codes('themes', false)
|
||||
slugs = items_from_links('themes', uniq: false) + items_from_codes('themes', uniq: false)
|
||||
|
||||
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
||||
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||
@@ -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)
|
||||
|
||||
@@ -8,15 +8,15 @@ module WPScan
|
||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||
|
||||
def login_request(username, password)
|
||||
target.method_call('wp.getUsersBlogs', [username, password])
|
||||
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0)
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
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
|
||||
|
||||
@@ -19,11 +19,33 @@ module WPScan
|
||||
end
|
||||
end
|
||||
|
||||
target.multi_call(methods).run
|
||||
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,8 @@ 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/author_sitemap'
|
||||
require_relative 'users/yoast_seo_author_sitemap'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
@@ -22,6 +23,7 @@ module WPScan
|
||||
Users::WpJsonApi.new(target) <<
|
||||
Users::OembedApi.new(target) <<
|
||||
Users::RSSGenerator.new(target) <<
|
||||
Users::AuthorSitemap.new(target) <<
|
||||
Users::YoastSeoAuthorSitemap.new(target) <<
|
||||
Users::AuthorIdBruteForcing.new(target) <<
|
||||
Users::LoginErrorMessages.new(target)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -97,9 +99,12 @@ module WPScan
|
||||
# @return [ String, nil ]
|
||||
def display_name_from_body(body)
|
||||
page = Nokogiri::HTML.parse(body)
|
||||
|
||||
# WP >= 3.0
|
||||
page.css('h1.page-title span').each do |node|
|
||||
return node.text.to_s
|
||||
text = node.text.to_s.strip
|
||||
|
||||
return text unless text.empty?
|
||||
end
|
||||
|
||||
# WP < 3.0
|
||||
|
||||
@@ -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)
|
||||
|
||||
36
app/finders/users/author_sitemap.rb
Normal file
36
app/finders/users/author_sitemap.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Since WP 5.5, /wp-sitemap-users-1.xml is generated and contains
|
||||
# the usernames of accounts who made a post
|
||||
class AuthorSitemap < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
|
||||
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
||||
username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
|
||||
|
||||
next unless username && !username.strip.empty?
|
||||
|
||||
found << Model::User.new(username,
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [sitemap_url])
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ] The URL of the sitemap
|
||||
def sitemap_url
|
||||
@sitemap_url ||= target.url('wp-sitemap-users-1.xml')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -34,6 +34,8 @@ module WPScan
|
||||
def user_details_from_oembed_data(oembed_data)
|
||||
return unless oembed_data
|
||||
|
||||
oembed_data = oembed_data.first if oembed_data.is_a?(Array)
|
||||
|
||||
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
|
||||
details = [Regexp.last_match[1], 'Author URL', 90]
|
||||
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,27 +5,7 @@ module WPScan
|
||||
module Users
|
||||
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
|
||||
# See https://github.com/wpscanteam/wpscan/issues/1228
|
||||
class YoastSeoAuthorSitemap < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
|
||||
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
||||
username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
|
||||
|
||||
next unless username && !username.strip.empty?
|
||||
|
||||
found << Model::User.new(username,
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [sitemap_url])
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
class YoastSeoAuthorSitemap < AuthorSitemap
|
||||
# @return [ String ] The URL of the author-sitemap
|
||||
def sitemap_url
|
||||
@sitemap_url ||= target.url('author-sitemap.xml')
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'wp_items/urls_in_homepage'
|
||||
require_relative 'wp_items/urls_in_page'
|
||||
|
||||
@@ -4,18 +4,24 @@ 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
|
||||
def items_from_links(type, uniq = true)
|
||||
# @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)
|
||||
|
||||
found << Regexp.last_match[1]
|
||||
slug = Regexp.last_match[1]&.strip
|
||||
|
||||
found << slug unless slug&.empty?
|
||||
end
|
||||
|
||||
uniq ? found.uniq.sort : found.sort
|
||||
@@ -25,10 +31,10 @@ module WPScan
|
||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||
#
|
||||
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
|
||||
def items_from_codes(type, uniq = true)
|
||||
def items_from_codes(type, uniq: true)
|
||||
found = []
|
||||
|
||||
target.homepage_res.html.css('script,style').each do |tag|
|
||||
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
@@ -42,14 +48,14 @@ module WPScan
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_attribute_pattern(type)
|
||||
@item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
|
||||
@item_attribute_pattern ||= %r{#{item_url_pattern(type)}([^/]+)/}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
#
|
||||
# @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
|
||||
@@ -59,10 +65,10 @@ module WPScan
|
||||
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||
|
||||
url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||
item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
|
||||
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 = {})
|
||||
|
||||
@@ -7,46 +7,130 @@ module WPScan
|
||||
include References
|
||||
end
|
||||
|
||||
#
|
||||
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
|
||||
#
|
||||
class BackupDB < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "A backup directory has been found: #{url}"
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['https://github.com/wpscanteam/wpscan/issues/422'] }
|
||||
end
|
||||
end
|
||||
|
||||
class DebugLog < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "Debug Log found: #{url}"
|
||||
end
|
||||
|
||||
# @ 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
|
||||
def to_s
|
||||
@to_s ||= "Full Path Disclosure found: #{url}"
|
||||
end
|
||||
|
||||
# @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
|
||||
def to_s
|
||||
@to_s ||= "WordPress readme found: #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
class Registration < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= "Registration is enabled: #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
class TmmDbMigrate < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "ThemeMakers migration file found: #{url}"
|
||||
end
|
||||
|
||||
# @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
|
||||
def to_s
|
||||
@to_s ||= "SQL Dump found: #{url}"
|
||||
end
|
||||
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[/^\s*#{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/register.
|
||||
<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpscan.com/register
|
||||
<% 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/register."
|
||||
"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://wpscan.com/register"
|
||||
<% end -%>
|
||||
},
|
||||
@@ -31,7 +31,7 @@ module WPScan
|
||||
|
||||
finder_configs(
|
||||
finder_class,
|
||||
Regexp.last_match[1] == 'aggressive'
|
||||
aggressive: Regexp.last_match[1] == 'aggressive'
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ module WPScan
|
||||
# @param [ Symbol ] finder_class
|
||||
# @param [ Boolean ] aggressive
|
||||
# @return [ Hash ]
|
||||
def self.finder_configs(finder_class, aggressive = false)
|
||||
def self.finder_configs(finder_class, aggressive: false)
|
||||
configs = {}
|
||||
|
||||
return configs unless allowed_classes.include?(finder_class)
|
||||
|
||||
@@ -24,7 +24,7 @@ module WPScan
|
||||
# @param [ Symbol ] finder_class
|
||||
# @param [ Boolean ] aggressive
|
||||
# @return [ Hash ]
|
||||
def self.finder_configs(finder_class, aggressive = false)
|
||||
def self.finder_configs(finder_class, aggressive: false)
|
||||
configs = {}
|
||||
|
||||
return configs unless allowed_classes.include?(finder_class)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,7 +4,7 @@ module WPScan
|
||||
module DB
|
||||
# WPVulnDB API
|
||||
class VulnApi
|
||||
NON_ERROR_CODES = [200, 401, 404].freeze
|
||||
NON_ERROR_CODES = [200, 401].freeze
|
||||
|
||||
class << self
|
||||
attr_accessor :token
|
||||
@@ -12,7 +12,7 @@ module WPScan
|
||||
|
||||
# @return [ Addressable::URI ]
|
||||
def self.uri
|
||||
@uri ||= Addressable::URI.parse('https://wpvulndb.com/api/v3/')
|
||||
@uri ||= Addressable::URI.parse('https://wpscan.com/api/v3/')
|
||||
end
|
||||
|
||||
# @param [ String ] path
|
||||
@@ -21,9 +21,12 @@ 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)
|
||||
|
||||
raise Error::HTTP, res
|
||||
@@ -63,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,25 @@ 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)
|
||||
|
||||
return homepage_result unless homepage_result.nil? || homepage_result.is_a?(Array) && homepage_result&.empty?
|
||||
|
||||
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
|
||||
|
||||
@@ -27,7 +27,7 @@ module WPScan
|
||||
end
|
||||
|
||||
def wpvulndb_url(id)
|
||||
"https://wpvulndb.com/vulnerabilities/#{id}"
|
||||
"https://wpscan.com/vulnerability/#{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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,7 +11,9 @@ module WPScan
|
||||
module WordPress
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
|
||||
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu\-)?plugins|uploads))|wp-includes)/}i.freeze
|
||||
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i.freeze
|
||||
WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i.freeze
|
||||
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i.freeze
|
||||
|
||||
# These methods are used in the associated interesting_findings finders
|
||||
# to keep the boolean state of the finding rather than re-check the whole thing again
|
||||
@@ -22,22 +24,20 @@ module WPScan
|
||||
|
||||
# @param [ Symbol ] detection_mode
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
# @return [ Boolean ] Whether or not the target is running WordPress
|
||||
def wordpress?(detection_mode)
|
||||
in_scope_uris(homepage_res) do |uri|
|
||||
return true if uri.path.match(WORDPRESS_PATTERN)
|
||||
[homepage_res, error_404_res].each do |page_res|
|
||||
return true if wordpress_from_meta_comments_or_scripts?(page_res)
|
||||
end
|
||||
|
||||
homepage_res.html.css('meta[name="generator"]').each do |node|
|
||||
return true if /wordpress/i.match?(node['content'])
|
||||
end
|
||||
|
||||
return true unless comments_from_page(/wordpress/i, homepage_res).empty?
|
||||
|
||||
if %i[mixed aggressive].include?(detection_mode)
|
||||
%w[wp-admin/install.php wp-login.php].each do |path|
|
||||
in_scope_uris(Browser.get_and_follow_location(url(path))).each do |uri|
|
||||
return true if uri.path.match(WORDPRESS_PATTERN)
|
||||
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
|
||||
@@ -45,6 +45,26 @@ module WPScan
|
||||
false
|
||||
end
|
||||
|
||||
# @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
|
||||
}.freeze
|
||||
@@ -82,10 +102,11 @@ module WPScan
|
||||
def wordpress_hosted?
|
||||
return true if /\.wordpress\.com$/i.match?(uri.host)
|
||||
|
||||
unless content_dir(:passive)
|
||||
unless content_dir
|
||||
pattern = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
|
||||
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
|
||||
@@ -109,6 +130,7 @@ module WPScan
|
||||
Browser.instance.forge_request(
|
||||
login_url,
|
||||
method: :post,
|
||||
cache_ttl: 0,
|
||||
body: { log: username, pwd: password }
|
||||
)
|
||||
end
|
||||
@@ -117,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
|
||||
|
||||
@@ -13,25 +13,24 @@ module WPScan
|
||||
@plugins_dir = dir.chomp('/')
|
||||
end
|
||||
|
||||
# @param [ Symbol ] detection_mode
|
||||
# @return [ String ] The wp-content directory
|
||||
def content_dir(detection_mode = :mixed)
|
||||
def content_dir
|
||||
unless @content_dir
|
||||
# scope_url_pattern is from CMSScanner::Target
|
||||
pattern = %r{#{scope_url_pattern}([\w\s\-/]+)\\?/(?:themes|plugins|uploads|cache)\\?/}i
|
||||
pattern = %r{#{scope_url_pattern}([\w\s\-/]+?)\\?/(?:themes|plugins|uploads|cache)\\?/}i
|
||||
|
||||
in_scope_uris(homepage_res) do |uri|
|
||||
return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
|
||||
[homepage_res, error_404_res].each do |page_res|
|
||||
in_scope_uris(page_res, '//link/@href|//script/@src|//img/@src') do |uri|
|
||||
return @content_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
|
||||
end
|
||||
|
||||
# Checks for the pattern in raw JS code, as well as @content attributes of meta tags
|
||||
xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, page_res) do |match|
|
||||
return @content_dir = match[1]
|
||||
end
|
||||
end
|
||||
|
||||
# Checks for the pattern in raw JS code, as well as @content attributes of meta tags
|
||||
xpath_pattern_from_page('//script[not(@src)]|//meta/@content', pattern, homepage_res) do |match|
|
||||
return @content_dir = match[1]
|
||||
end
|
||||
|
||||
unless detection_mode == :passive
|
||||
return @content_dir = 'wp-content' if default_content_dir_exists?
|
||||
end
|
||||
return @content_dir = 'wp-content' if default_content_dir_exists?
|
||||
end
|
||||
|
||||
@content_dir
|
||||
@@ -72,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 ]
|
||||
@@ -94,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
|
||||
@@ -105,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
|
||||
@@ -122,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.0'
|
||||
VERSION = '3.8.9'
|
||||
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
|
||||
|
||||
@@ -52,9 +52,10 @@ describe WPScan::Controller::Core do
|
||||
%i[apache iis nginx].each do |server|
|
||||
context "when #{server}" do
|
||||
let(:cli_args) { "#{super()} --server #{server}" }
|
||||
let(:servers) { [:Apache, nil, :IIS, :Nginx] }
|
||||
|
||||
it "loads the #{server.capitalize} module and returns :#{server}" do
|
||||
@stubbed_server = [:Apache, nil, :IIS, :Nginx].sample
|
||||
@stubbed_server = servers.sample
|
||||
@expected = server == :iis ? :IIS : server.to_s.camelize.to_sym
|
||||
end
|
||||
end
|
||||
@@ -70,7 +71,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
|
||||
|
||||
@@ -166,6 +167,7 @@ describe WPScan::Controller::Core do
|
||||
before do
|
||||
expect(core).to receive(:load_server_module)
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
|
||||
expect(core.target).to receive(:wordpress_hosted?).and_return(false)
|
||||
end
|
||||
|
||||
it 'calls the formatter when started and finished to update the db' do
|
||||
@@ -174,56 +176,6 @@ describe WPScan::Controller::Core do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a redirect occurs' do
|
||||
before do
|
||||
stub_request(:any, target_url)
|
||||
|
||||
expect(core.target).to receive(:homepage_res)
|
||||
.at_least(1)
|
||||
.and_return(Typhoeus::Response.new(effective_url: redirection, body: ''))
|
||||
end
|
||||
|
||||
context 'to the wp-admin/install.php' do
|
||||
let(:redirection) { "#{target_url}wp-admin/install.php" }
|
||||
|
||||
it 'calls the formatter with the correct parameters and exit' do
|
||||
expect(core.formatter).to receive(:output)
|
||||
.with('not_fully_configured', hash_including(url: redirection), 'core').ordered
|
||||
|
||||
# TODO: Would be cool to be able to test the exit code
|
||||
expect { core.before_scan }.to raise_error(SystemExit)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to something else' do
|
||||
let(:redirection) { 'http://g.com/' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { core.before_scan }.to raise_error(CMSScanner::Error::HTTPRedirect)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to another path with the wp-admin/install.php in the query' do
|
||||
let(:redirection) { "#{target_url}index.php?a=/wp-admin/install.php" }
|
||||
|
||||
context 'when wordpress' do
|
||||
it 'does not raise an error' do
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
|
||||
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not wordpress' do
|
||||
it 'raises an error' do
|
||||
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
|
||||
|
||||
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when hosted on wordpress.com' do
|
||||
let(:target_url) { 'http://ex.wordpress.com' }
|
||||
|
||||
@@ -234,52 +186,106 @@ describe WPScan::Controller::Core do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wordpress' do
|
||||
before do
|
||||
expect(core).to receive(:load_server_module)
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
|
||||
end
|
||||
context 'when not hosted on wordpress.com' do
|
||||
before { allow(core.target).to receive(:wordpress_hosted?).and_return(false) }
|
||||
|
||||
it 'does not raise any error' do
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
context 'when a redirect occurs' do
|
||||
before do
|
||||
stub_request(:any, target_url)
|
||||
|
||||
context 'when not wordpress' do
|
||||
before do
|
||||
expect(core).to receive(:load_server_module)
|
||||
end
|
||||
expect(core.target).to receive(:homepage_res)
|
||||
.at_least(1)
|
||||
.and_return(Typhoeus::Response.new(effective_url: redirection, body: ''))
|
||||
end
|
||||
|
||||
context 'when no --force' do
|
||||
before { expect(core.target).to receive(:maybe_add_cookies) }
|
||||
context 'to the wp-admin/install.php' do
|
||||
let(:redirection) { "#{target_url}wp-admin/install.php" }
|
||||
|
||||
context 'when no cookies added or still not wordpress after being added' do
|
||||
it 'raises an error' do
|
||||
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
|
||||
it 'calls the formatter with the correct parameters and exit' do
|
||||
expect(core.formatter).to receive(:output)
|
||||
.with('not_fully_configured', hash_including(url: redirection), 'core').ordered
|
||||
|
||||
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
|
||||
# TODO: Would be cool to be able to test the exit code
|
||||
expect { core.before_scan }.to raise_error(SystemExit)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the added cookies solved it' do
|
||||
it 'does not raise an error' do
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false).ordered
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true).ordered
|
||||
context 'to something else' do
|
||||
let(:redirection) { 'http://g.com/' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { core.before_scan }.to raise_error(CMSScanner::Error::HTTPRedirect)
|
||||
end
|
||||
end
|
||||
|
||||
context 'to another path with the wp-admin/install.php in the query' do
|
||||
let(:redirection) { "#{target_url}index.php?a=/wp-admin/install.php" }
|
||||
|
||||
context 'when wordpress' do
|
||||
it 'does not raise an error' do
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
|
||||
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not wordpress' do
|
||||
it 'raises an error' do
|
||||
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
|
||||
|
||||
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wordpress' do
|
||||
before do
|
||||
expect(core).to receive(:load_server_module)
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true)
|
||||
end
|
||||
|
||||
it 'does not raise any error' do
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not wordpress' do
|
||||
before do
|
||||
expect(core).to receive(:load_server_module)
|
||||
end
|
||||
|
||||
context 'when no --force' do
|
||||
before { expect(core.target).to receive(:maybe_add_cookies) }
|
||||
|
||||
context 'when no cookies added or still not wordpress after being added' do
|
||||
it 'raises an error' do
|
||||
expect(core.target).to receive(:wordpress?).twice.with(:mixed).and_return(false)
|
||||
|
||||
expect { core.before_scan }.to raise_error(WPScan::Error::NotWordPress)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the added cookies solved it' do
|
||||
it 'does not raise an error' do
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false).ordered
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(true).ordered
|
||||
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when --force' do
|
||||
let(:cli_args) { "#{super()} --force" }
|
||||
|
||||
it 'does not raise any error' do
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false)
|
||||
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when --force' do
|
||||
let(:cli_args) { "#{super()} --force" }
|
||||
|
||||
it 'does not raise any error' do
|
||||
expect(core.target).to receive(:wordpress?).with(:mixed).and_return(false)
|
||||
|
||||
expect { core.before_scan }.to_not raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ describe WPScan::Controller::CustomDirectories do
|
||||
|
||||
describe '#before_scan' do
|
||||
context 'when the content_dir is not found and not supplied' do
|
||||
before { expect(controller.target).to receive(:content_dir).with(:mixed) }
|
||||
before { expect(controller.target).to receive(:content_dir).and_return(nil) }
|
||||
|
||||
it 'raises an exception' do
|
||||
expect { controller.before_scan }.to raise_error(WPScan::Error::WpContentDirNotDetected)
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user