Compare commits
396 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
188c8f31b2 | ||
|
|
76b2c067f6 | ||
|
|
01316ceac1 | ||
|
|
52f14c5f06 | ||
|
|
6782730d80 | ||
|
|
4235871a00 | ||
|
|
cb27a22fc4 | ||
|
|
e639d4eee3 | ||
|
|
d95b70f1c2 | ||
|
|
fb97553f7c | ||
|
|
b3b3bec6b0 | ||
|
|
baab7a49f6 | ||
|
|
6843fe700e | ||
|
|
0c193de70e | ||
|
|
e42ce414de | ||
|
|
6d347ada98 | ||
|
|
3638241513 | ||
|
|
1c30743a11 | ||
|
|
48d363031b | ||
|
|
d083719b9c | ||
|
|
7fd59b27f4 | ||
|
|
6f4b216bb0 | ||
|
|
5fa82a3f27 | ||
|
|
02d1e30b08 | ||
|
|
1e2d227c56 | ||
|
|
cfc895e658 | ||
|
|
3f789b39c4 | ||
|
|
16002576d2 | ||
|
|
7b0e352d29 | ||
|
|
da85729254 | ||
|
|
6a48f6c42b | ||
|
|
8eabcd9df3 | ||
|
|
6b89bc9f55 | ||
|
|
1e250796ca | ||
|
|
c73f9028f0 | ||
|
|
71c89371a9 | ||
|
|
9043ddca71 | ||
|
|
6c461e778d | ||
|
|
3f1a71c643 | ||
|
|
b0df6dcade | ||
|
|
ac0d2fb536 | ||
|
|
48f107021a | ||
|
|
64ac4ecf72 | ||
|
|
ddc680a9ae | ||
|
|
43b04da5a2 | ||
|
|
bc28dd392d | ||
|
|
0d2e1fee43 | ||
|
|
eaf2cec8c9 | ||
|
|
611d3dfd4d | ||
|
|
c4030d8267 | ||
|
|
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 | ||
|
|
bd74689079 | ||
|
|
248942bdea | ||
|
|
d9f203300b | ||
|
|
aceabc969f | ||
|
|
dedc24d3a7 | ||
|
|
6e583e78e8 | ||
|
|
c012e83355 | ||
|
|
264355d185 | ||
|
|
fdbfd1ec60 | ||
|
|
7a8b27a255 | ||
|
|
ec4bfac98b | ||
|
|
c63ffe37c9 | ||
|
|
d2f3ce82c9 | ||
|
|
3e24a0b0a4 | ||
|
|
1a07e29ff4 | ||
|
|
1aa46a8928 | ||
|
|
d9083f8b5f | ||
|
|
23d558a6d7 | ||
|
|
665a5b7b12 | ||
|
|
1d73418969 | ||
|
|
f67b5e4cc4 | ||
|
|
ae2515444f | ||
|
|
463e77f0a5 | ||
|
|
d7b796b1a7 | ||
|
|
9b07d53077 | ||
|
|
8ee9b2bc31 | ||
|
|
c5989477a4 | ||
|
|
96d8a4e4f8 | ||
|
|
e865e11731 | ||
|
|
f0997bfe0d | ||
|
|
8b67dad456 | ||
|
|
53fdac1038 | ||
|
|
534a7602e6 | ||
|
|
30f329fe43 | ||
|
|
4ce39951a9 | ||
|
|
0e9eb34626 | ||
|
|
0ff299c425 | ||
|
|
6366258ce9 | ||
|
|
bca69a026e | ||
|
|
adc26ea42a | ||
|
|
84422b10c8 | ||
|
|
d05ad0f8f4 | ||
|
|
3f70ddaffa | ||
|
|
b16e8d84d7 | ||
|
|
5ee405d5a0 | ||
|
|
a5b9470636 |
@@ -14,3 +14,4 @@ Dockerfile
|
|||||||
*.orig
|
*.orig
|
||||||
bin/wpscan-*
|
bin/wpscan-*
|
||||||
.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
|
--require spec_helper
|
||||||
--fail-fast
|
--color
|
||||||
--require spec_helper
|
|
||||||
28
.rubocop.yml
28
.rubocop.yml
@@ -1,17 +1,16 @@
|
|||||||
require: rubocop-performance
|
require: rubocop-performance
|
||||||
AllCops:
|
AllCops:
|
||||||
TargetRubyVersion: 2.4
|
NewCops: enable
|
||||||
|
TargetRubyVersion: 2.5
|
||||||
Exclude:
|
Exclude:
|
||||||
- '*.gemspec'
|
- '*.gemspec'
|
||||||
- 'vendor/**/*'
|
- 'vendor/**/*'
|
||||||
ClassVars:
|
Layout/LineLength:
|
||||||
Enabled: false
|
|
||||||
LineLength:
|
|
||||||
Max: 120
|
Max: 120
|
||||||
MethodLength:
|
Lint/ConstantDefinitionInBlock:
|
||||||
Max: 20
|
Enabled: false
|
||||||
Exclude:
|
Lint/MissingSuper:
|
||||||
- 'app/controllers/enumeration/cli_options.rb'
|
Enabled: false
|
||||||
Lint/UriEscapeUnescape:
|
Lint/UriEscapeUnescape:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
@@ -24,8 +23,19 @@ Metrics/ClassLength:
|
|||||||
Exclude:
|
Exclude:
|
||||||
- 'app/controllers/enumeration/cli_options.rb'
|
- 'app/controllers/enumeration/cli_options.rb'
|
||||||
Metrics/CyclomaticComplexity:
|
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:
|
Style/Documentation:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Style/FormatStringToken:
|
Style/FormatStringToken:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Exclude:
|
||||||
|
- 'app/controllers/vuln_api.rb'
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.6.2
|
2.7.2
|
||||||
|
|||||||
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
|
SimpleCov.start do
|
||||||
|
enable_coverage :branch # Only supported for Ruby >= 2.5
|
||||||
|
|
||||||
add_filter '/spec/'
|
add_filter '/spec/'
|
||||||
add_filter 'helper'
|
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
|
|
||||||
22
Dockerfile
22
Dockerfile
@@ -1,16 +1,16 @@
|
|||||||
FROM ruby:2.6.2-alpine3.9 AS builder
|
FROM ruby:2.7.2-alpine AS builder
|
||||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
LABEL maintainer="WPScan Team <contact@wpscan.com>"
|
||||||
|
|
||||||
ARG BUNDLER_ARGS="--jobs=8 --without test development"
|
RUN echo "install: --no-document --no-post-install-message\nupdate: --no-document --no-post-install-message" > /etc/gemrc
|
||||||
|
|
||||||
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
|
|
||||||
|
|
||||||
COPY . /wpscan
|
COPY . /wpscan
|
||||||
|
|
||||||
RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
|
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 && \
|
bundle config force_ruby_platform true && \
|
||||||
# temp fix for https://github.com/bundler/bundler/issues/6680
|
bundle config disable_version_check 'true' && \
|
||||||
rm -rf /usr/local/bundle/cache
|
bundle config without "test development" && \
|
||||||
|
bundle config path.system 'true' && \
|
||||||
|
bundle install --gemfile=/wpscan/Gemfile --jobs=8
|
||||||
|
|
||||||
WORKDIR /wpscan
|
WORKDIR /wpscan
|
||||||
RUN rake install --trace
|
RUN rake install --trace
|
||||||
@@ -19,8 +19,9 @@ RUN rake install --trace
|
|||||||
RUN chmod -R a+r /usr/local/bundle
|
RUN chmod -R a+r /usr/local/bundle
|
||||||
|
|
||||||
|
|
||||||
FROM ruby:2.6.2-alpine3.9
|
FROM ruby:2.7.2-alpine
|
||||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
LABEL maintainer="WPScan Team <contact@wpscan.com>"
|
||||||
|
LABEL org.opencontainers.image.source https://github.com/wpscanteam/wpscan
|
||||||
|
|
||||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||||
|
|
||||||
@@ -38,4 +39,3 @@ USER wpscan
|
|||||||
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
|
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.
|
- Using WPScan to test your own systems.
|
||||||
- Any non-commercial use of WPScan.
|
- 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.
|
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - contact@wpscan.com.
|
||||||
|
|
||||||
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
|
|
||||||
|
|
||||||
Free-use Terms and Conditions;
|
Free-use Terms and Conditions;
|
||||||
|
|
||||||
|
|||||||
81
README.md
81
README.md
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://wpscan.org/">
|
<a href="https://wpscan.com/">
|
||||||
<img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo">
|
<img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -7,17 +7,16 @@
|
|||||||
<h3 align="center">WPScan</h3>
|
<h3 align="center">WPScan</h3>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
WordPress Vulnerability Scanner
|
WordPress Security Scanner
|
||||||
<br>
|
<br>
|
||||||
<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>
|
||||||
|
|
||||||
<p align="center">
|
<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://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>
|
<a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
|
||||||
<a href="https://www.patreon.com/wpscan" target="_blank"><img src="https://img.shields.io/badge/patreon-donate-green.svg"></a>
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# INSTALL
|
# INSTALL
|
||||||
@@ -32,7 +31,11 @@
|
|||||||
- RubyGems - Recommended: latest
|
- 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
|
- 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
|
```shell
|
||||||
gem install wpscan
|
gem install wpscan
|
||||||
@@ -40,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))
|
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
|
# Updating
|
||||||
|
|
||||||
You can update the local database by using ```wpscan --update```
|
You can update the local database by using ```wpscan --update```
|
||||||
@@ -78,41 +69,67 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
|
|||||||
|
|
||||||
# Usage
|
# 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'.
|
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)
|
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
|
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):
|
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
|
||||||
|
|
||||||
- ~/.wpscan/cli_options.json
|
- ~/.wpscan/scan.json
|
||||||
- ~/.wpscan/cli_options.yml
|
- ~/.wpscan/scan.yml
|
||||||
- pwd/.wpscan/cli_options.json
|
- pwd/.wpscan/scan.json
|
||||||
- pwd/.wpscan/cli_options.yml
|
- pwd/.wpscan/scan.yml
|
||||||
|
|
||||||
If those files exist, options from them will be loaded and overridden if found twice.
|
If those files exist, options from the `cli_options` key will be loaded and overridden if found twice.
|
||||||
|
|
||||||
e.g:
|
e.g:
|
||||||
|
|
||||||
~/.wpscan/cli_options.yml:
|
~/.wpscan/scan.yml:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
proxy: 'http://127.0.0.1:8080'
|
cli_options:
|
||||||
verbose: true
|
proxy: 'http://127.0.0.1:8080'
|
||||||
|
verbose: true
|
||||||
```
|
```
|
||||||
|
|
||||||
pwd/.wpscan/cli_options.yml:
|
pwd/.wpscan/scan.yml:
|
||||||
|
|
||||||
```yml
|
```yml
|
||||||
proxy: 'socks5://127.0.0.1:9090'
|
cli_options:
|
||||||
url: 'http://target.tld'
|
proxy: 'socks5://127.0.0.1:9090'
|
||||||
|
url: 'http://target.tld'
|
||||||
```
|
```
|
||||||
|
|
||||||
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
|
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
|
||||||
|
|
||||||
Enumerating usernames
|
## Save API Token in a file
|
||||||
|
|
||||||
|
The feature mentioned above is useful to keep the API Token in a config file and not have to supply it via the CLI each time. To do so, create the ~/.wpscan/scan.yml file containing the below:
|
||||||
|
|
||||||
|
```yml
|
||||||
|
cli_options:
|
||||||
|
api_token: YOUR_API_TOKEN
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
```shell
|
||||||
wpscan --url https://target.tld/ --enumerate u
|
wpscan --url https://target.tld/ --enumerate u
|
||||||
@@ -159,7 +176,7 @@ Example cases which do not require a commercial license, and thus fall under the
|
|||||||
- Using WPScan to test your own systems.
|
- Using WPScan to test your own systems.
|
||||||
- Any non-commercial use of WPScan.
|
- 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.
|
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;
|
Free-use Terms and Conditions;
|
||||||
|
|
||||||
|
|||||||
6
Rakefile
6
Rakefile
@@ -6,14 +6,18 @@ exec = []
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
require 'rubocop/rake_task'
|
require 'rubocop/rake_task'
|
||||||
|
|
||||||
RuboCop::RakeTask.new
|
RuboCop::RakeTask.new
|
||||||
|
|
||||||
exec << :rubocop
|
exec << :rubocop
|
||||||
rescue LoadError
|
rescue LoadError
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
require 'rspec/core/rake_task'
|
require 'rspec/core/rake_task'
|
||||||
RSpec::Core::RakeTask.new(:spec)
|
|
||||||
|
RSpec::Core::RakeTask.new(:spec) { |t| t.rspec_opts = %w{--tag ~slow} }
|
||||||
|
|
||||||
exec << :spec
|
exec << :spec
|
||||||
rescue LoadError
|
rescue LoadError
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'controllers/core'
|
require_relative 'controllers/core'
|
||||||
|
require_relative 'controllers/vuln_api'
|
||||||
require_relative 'controllers/custom_directories'
|
require_relative 'controllers/custom_directories'
|
||||||
require_relative 'controllers/wp_version'
|
require_relative 'controllers/wp_version'
|
||||||
require_relative 'controllers/main_theme'
|
require_relative 'controllers/main_theme'
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ module WPScan
|
|||||||
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
|
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
|
||||||
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
|
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
|
||||||
|
|
||||||
return if target.content_dir(ParsedCli.detection_mode)
|
raise Error::WpContentDirNotDetected unless target.content_dir
|
||||||
|
|
||||||
raise Error::WpContentDirNotDetected
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ module WPScan
|
|||||||
choices: {
|
choices: {
|
||||||
vp: OptBoolean.new(['--vulnerable-plugins']),
|
vp: OptBoolean.new(['--vulnerable-plugins']),
|
||||||
ap: OptBoolean.new(['--all-plugins']),
|
ap: OptBoolean.new(['--all-plugins']),
|
||||||
p: OptBoolean.new(['--plugins']),
|
p: OptBoolean.new(['--popular-plugins']),
|
||||||
vt: OptBoolean.new(['--vulnerable-themes']),
|
vt: OptBoolean.new(['--vulnerable-themes']),
|
||||||
at: OptBoolean.new(['--all-themes']),
|
at: OptBoolean.new(['--all-themes']),
|
||||||
t: OptBoolean.new(['--themes']),
|
t: OptBoolean.new(['--popular-themes']),
|
||||||
tt: OptBoolean.new(['--timthumbs']),
|
tt: OptBoolean.new(['--timthumbs']),
|
||||||
cb: OptBoolean.new(['--config-backups']),
|
cb: OptBoolean.new(['--config-backups']),
|
||||||
dbe: OptBoolean.new(['--db-exports']),
|
dbe: OptBoolean.new(['--db-exports']),
|
||||||
@@ -51,7 +51,7 @@ module WPScan
|
|||||||
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
|
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--plugins-detection MODE',
|
['--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
|
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
|
||||||
),
|
),
|
||||||
OptBoolean.new(
|
OptBoolean.new(
|
||||||
@@ -62,14 +62,13 @@ module WPScan
|
|||||||
),
|
),
|
||||||
OptChoice.new(
|
OptChoice.new(
|
||||||
['--plugins-version-detection MODE',
|
['--plugins-version-detection MODE',
|
||||||
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
|
'Use the supplied mode to check plugins\' versions.'],
|
||||||
'or --plugins-detection modes.'],
|
|
||||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
||||||
),
|
),
|
||||||
OptInteger.new(
|
OptInteger.new(
|
||||||
['--plugins-threshold THRESHOLD',
|
['--plugins-threshold THRESHOLD',
|
||||||
'Raise an error when the number of detected plugins via known locations reaches the 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
|
end
|
||||||
@@ -98,7 +97,7 @@ module WPScan
|
|||||||
OptInteger.new(
|
OptInteger.new(
|
||||||
['--themes-threshold THRESHOLD',
|
['--themes-threshold THRESHOLD',
|
||||||
'Raise an error when the number of detected themes via known locations reaches the 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
|
end
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Boolean ] Wether or not to enumerate the plugins
|
# @return [ Boolean ] Wether or not to enumerate the plugins
|
||||||
def enum_plugins?(opts)
|
def enum_plugins?(opts)
|
||||||
opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_plugins
|
def enum_plugins
|
||||||
@@ -92,7 +92,7 @@ module WPScan
|
|||||||
|
|
||||||
if opts[:enumerate][:all_plugins]
|
if opts[:enumerate][:all_plugins]
|
||||||
DB::Plugins.all_slugs
|
DB::Plugins.all_slugs
|
||||||
elsif opts[:enumerate][:plugins]
|
elsif opts[:enumerate][:popular_plugins]
|
||||||
DB::Plugins.popular_slugs
|
DB::Plugins.popular_slugs
|
||||||
else
|
else
|
||||||
DB::Plugins.vulnerable_slugs
|
DB::Plugins.vulnerable_slugs
|
||||||
@@ -103,7 +103,7 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Boolean ] Wether or not to enumerate the themes
|
# @return [ Boolean ] Wether or not to enumerate the themes
|
||||||
def enum_themes?(opts)
|
def enum_themes?(opts)
|
||||||
opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
|
||||||
end
|
end
|
||||||
|
|
||||||
def enum_themes
|
def enum_themes
|
||||||
@@ -139,7 +139,7 @@ module WPScan
|
|||||||
|
|
||||||
if opts[:enumerate][:all_themes]
|
if opts[:enumerate][:all_themes]
|
||||||
DB::Themes.all_slugs
|
DB::Themes.all_slugs
|
||||||
elsif opts[:enumerate][:themes]
|
elsif opts[:enumerate][:popular_themes]
|
||||||
DB::Themes.popular_slugs
|
DB::Themes.popular_slugs
|
||||||
else
|
else
|
||||||
DB::Themes.vulnerable_slugs
|
DB::Themes.vulnerable_slugs
|
||||||
|
|||||||
@@ -19,31 +19,37 @@ module WPScan
|
|||||||
OptChoice.new(['--password-attack ATTACK',
|
OptChoice.new(['--password-attack ATTACK',
|
||||||
'Force the supplied attack to be used rather than automatically determining one.'],
|
'Force the supplied attack to be used rather than automatically determining one.'],
|
||||||
choices: %w[wp-login xmlrpc xmlrpc-multicall],
|
choices: %w[wp-login xmlrpc xmlrpc-multicall],
|
||||||
normalize: %i[downcase underscore to_sym])
|
normalize: %i[downcase underscore to_sym]),
|
||||||
|
OptString.new(['--login-uri URI', 'The URI of the login page if different from /wp-login.php'])
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def attack_opts
|
||||||
|
@attack_opts ||= {
|
||||||
|
show_progression: user_interaction?,
|
||||||
|
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
return unless ParsedCli.passwords
|
return unless ParsedCli.passwords
|
||||||
|
|
||||||
if user_interaction?
|
|
||||||
output('@info',
|
|
||||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
|
||||||
end
|
|
||||||
|
|
||||||
attack_opts = {
|
|
||||||
show_progression: user_interaction?,
|
|
||||||
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
|
||||||
}
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
found = []
|
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
|
found << user
|
||||||
|
|
||||||
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
||||||
end
|
end
|
||||||
|
rescue Error::NoLoginInterfaceDetected => e
|
||||||
|
# TODO: Maybe output that in JSON as well.
|
||||||
|
output('@notice', msg: e.to_s) if user_interaction?
|
||||||
ensure
|
ensure
|
||||||
output('users', users: found)
|
output('users', users: found)
|
||||||
end
|
end
|
||||||
@@ -65,6 +71,8 @@ module WPScan
|
|||||||
|
|
||||||
case ParsedCli.password_attack
|
case ParsedCli.password_attack
|
||||||
when :wp_login
|
when :wp_login
|
||||||
|
raise Error::NoLoginInterfaceDetected unless target.login_url
|
||||||
|
|
||||||
Finders::Passwords::WpLogin.new(target)
|
Finders::Passwords::WpLogin.new(target)
|
||||||
when :xmlrpc
|
when :xmlrpc
|
||||||
raise Error::XMLRPCNotDetected unless xmlrpc
|
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||||
@@ -81,8 +89,8 @@ module WPScan
|
|||||||
def xmlrpc_get_users_blogs_enabled?
|
def xmlrpc_get_users_blogs_enabled?
|
||||||
if xmlrpc&.enabled? &&
|
if xmlrpc&.enabled? &&
|
||||||
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
|
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
|
||||||
xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
!xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
||||||
.run.body !~ /XML\-RPC services are disabled/
|
.run.body.match?(/>\s*405\s*</)
|
||||||
|
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
@@ -100,8 +108,10 @@ module WPScan
|
|||||||
else
|
else
|
||||||
Finders::Passwords::XMLRPC.new(xmlrpc)
|
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||||
end
|
end
|
||||||
else
|
elsif target.login_url
|
||||||
Finders::Passwords::WpLogin.new(target)
|
Finders::Passwords::WpLogin.new(target)
|
||||||
|
else
|
||||||
|
raise Error::NoLoginInterfaceDetected
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -113,15 +123,6 @@ module WPScan
|
|||||||
acc << Model::User.new(elem.chomp)
|
acc << Model::User.new(elem.chomp)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
35
app/controllers/vuln_api.rb
Normal file
35
app/controllers/vuln_api.rb
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module Controller
|
||||||
|
# Controller to handle the API token
|
||||||
|
class VulnApi < CMSScanner::Controller::Base
|
||||||
|
ENV_KEY = 'WPSCAN_API_TOKEN'
|
||||||
|
|
||||||
|
def cli_options
|
||||||
|
[
|
||||||
|
OptString.new(
|
||||||
|
['--api-token TOKEN',
|
||||||
|
'The WPScan API Token to display vulnerability data, available at https://wpscan.com/profile']
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def before_scan
|
||||||
|
return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
|
||||||
|
|
||||||
|
DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
|
||||||
|
|
||||||
|
api_status = DB::VulnApi.status
|
||||||
|
|
||||||
|
raise Error::InvalidApiToken if api_status['status'] == 'forbidden'
|
||||||
|
raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
|
||||||
|
raise api_status['http_error'] if api_status['http_error']
|
||||||
|
end
|
||||||
|
|
||||||
|
def after_scan
|
||||||
|
output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,7 +4,6 @@ module WPScan
|
|||||||
module Finders
|
module Finders
|
||||||
module DbExports
|
module DbExports
|
||||||
# DB Exports finder
|
# DB Exports finder
|
||||||
# See https://github.com/wpscanteam/wpscan-v3/issues/62
|
|
||||||
class KnownLocations < CMSScanner::Finders::Finder
|
class KnownLocations < CMSScanner::Finders::Finder
|
||||||
include CMSScanner::Finders::Finder::Enumerator
|
include CMSScanner::Finders::Finder::Enumerator
|
||||||
|
|
||||||
@@ -20,9 +19,9 @@ module WPScan
|
|||||||
|
|
||||||
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
|
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
|
||||||
if res.effective_url.end_with?('.zip')
|
if res.effective_url.end_with?('.zip')
|
||||||
next unless res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
next unless %r{\Aapplication/zip}i.match?(res.headers['Content-Type'])
|
||||||
else
|
else
|
||||||
next unless res.body =~ SQL_PATTERN
|
next unless SQL_PATTERN.match?(res.body)
|
||||||
end
|
end
|
||||||
|
|
||||||
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||||
@@ -41,7 +40,7 @@ module WPScan
|
|||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def potential_urls(opts = {})
|
def potential_urls(opts = {})
|
||||||
urls = {}
|
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|
|
File.open(opts[:list]).each_with_index do |path, index|
|
||||||
path.gsub!('{domain_name}', domain_name)
|
path.gsub!('{domain_name}', domain_name)
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ module WPScan
|
|||||||
target.url(path),
|
target.url(path),
|
||||||
confidence: 70,
|
confidence: 70,
|
||||||
found_by: DIRECT_ACCESS,
|
found_by: DIRECT_ACCESS,
|
||||||
interesting_entries: target.directory_listing_entries(path),
|
interesting_entries: target.directory_listing_entries(path)
|
||||||
references: { url: 'https://github.com/wpscanteam/wpscan/issues/422' }
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,11 +11,7 @@ module WPScan
|
|||||||
|
|
||||||
return unless target.debug_log?(path)
|
return unless target.debug_log?(path)
|
||||||
|
|
||||||
Model::DebugLog.new(
|
Model::DebugLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
target.url(path),
|
|
||||||
confidence: 100, found_by: DIRECT_ACCESS,
|
|
||||||
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,14 +9,9 @@ module WPScan
|
|||||||
def aggressive(_opts = {})
|
def aggressive(_opts = {})
|
||||||
path = 'installer-log.txt'
|
path = 'installer-log.txt'
|
||||||
|
|
||||||
return unless target.head_and_get(path).body =~ /DUPLICATOR INSTALL-LOG/
|
return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.match?(target.head_and_get(path).body)
|
||||||
|
|
||||||
Model::DuplicatorInstallerLog.new(
|
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
target.url(path),
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,10 +15,7 @@ module WPScan
|
|||||||
Model::EmergencyPwdResetScript.new(
|
Model::EmergencyPwdResetScript.new(
|
||||||
target.url(path),
|
target.url(path),
|
||||||
confidence: /password/i.match?(res.body) ? 100 : 40,
|
confidence: /password/i.match?(res.body) ? 100 : 40,
|
||||||
found_by: DIRECT_ACCESS,
|
found_by: DIRECT_ACCESS
|
||||||
references: {
|
|
||||||
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ module WPScan
|
|||||||
target.url(path),
|
target.url(path),
|
||||||
confidence: 100,
|
confidence: 100,
|
||||||
found_by: DIRECT_ACCESS,
|
found_by: DIRECT_ACCESS,
|
||||||
interesting_entries: fpd_entries,
|
interesting_entries: fpd_entries
|
||||||
references: { url: 'https://www.owasp.org/index.php/Full_Path_Disclosure' }
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,22 +7,16 @@ module WPScan
|
|||||||
class MuPlugins < CMSScanner::Finders::Finder
|
class MuPlugins < CMSScanner::Finders::Finder
|
||||||
# @return [ InterestingFinding ]
|
# @return [ InterestingFinding ]
|
||||||
def passive(_opts = {})
|
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 =~ pattern
|
next unless uri.path&.match?(pattern)
|
||||||
|
|
||||||
url = target.url('wp-content/mu-plugins/')
|
url = target.url('wp-content/mu-plugins/')
|
||||||
|
|
||||||
target.mu_plugins = true
|
target.mu_plugins = true
|
||||||
|
|
||||||
return Model::MuPlugins.new(
|
return Model::MuPlugins.new(url, confidence: 70, found_by: 'URLs In Homepage (Passive Detection)')
|
||||||
url,
|
|
||||||
confidence: 70,
|
|
||||||
found_by: 'URLs In Homepage (Passive Detection)',
|
|
||||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
|
||||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@@ -37,13 +31,7 @@ module WPScan
|
|||||||
|
|
||||||
target.mu_plugins = true
|
target.mu_plugins = true
|
||||||
|
|
||||||
Model::MuPlugins.new(
|
Model::MuPlugins.new(url, confidence: 80, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 80,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
|
||||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,18 +12,12 @@ module WPScan
|
|||||||
location = res.headers_hash['location']
|
location = res.headers_hash['location']
|
||||||
|
|
||||||
return unless [200, 302].include?(res.code)
|
return unless [200, 302].include?(res.code)
|
||||||
return if res.code == 302 && location =~ /wp-login\.php\?action=register/
|
return if res.code == 302 && location&.include?('wp-login.php?action=register')
|
||||||
return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
|
return unless res.code == 200 || res.code == 302 && location&.include?('wp-signup.php')
|
||||||
|
|
||||||
target.multisite = true
|
target.multisite = true
|
||||||
|
|
||||||
Model::Multisite.new(
|
Model::Multisite.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: 'This site seems to be a multisite',
|
|
||||||
references: { url: 'http://codex.wordpress.org/Glossary#Multisite' }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,12 +20,7 @@ module WPScan
|
|||||||
|
|
||||||
target.registration_enabled = true
|
target.registration_enabled = true
|
||||||
|
|
||||||
Model::Registration.new(
|
Model::Registration.new(res.effective_url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
res.effective_url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: "Registration is enabled: #{res.effective_url}"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ module WPScan
|
|||||||
|
|
||||||
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
||||||
|
|
||||||
Model::TmmDbMigrate.new(
|
Model::TmmDbMigrate.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
references: { packetstorm: 131_957 }
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,12 +13,7 @@ module WPScan
|
|||||||
|
|
||||||
url = target.url(path)
|
url = target.url(path)
|
||||||
|
|
||||||
Model::UploadDirectoryListing.new(
|
Model::UploadDirectoryListing.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
url,
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
to_s: "Upload directory has listing enabled: #{url}"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,13 +12,9 @@ module WPScan
|
|||||||
path = 'wp-content/uploads/dump.sql'
|
path = 'wp-content/uploads/dump.sql'
|
||||||
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
|
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
|
||||||
|
|
||||||
return unless res.body =~ SQL_PATTERN
|
return unless SQL_PATTERN.match?(res.body)
|
||||||
|
|
||||||
Model::UploadSQLDump.new(
|
Model::UploadSQLDump.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||||
target.url(path),
|
|
||||||
confidence: 100,
|
|
||||||
found_by: DIRECT_ACCESS
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -11,17 +11,7 @@ module WPScan
|
|||||||
|
|
||||||
return unless res.code == 200
|
return unless res.code == 200
|
||||||
|
|
||||||
Model::WPCron.new(
|
Model::WPCron.new(wp_cron_url, confidence: 60, found_by: DIRECT_ACCESS)
|
||||||
wp_cron_url,
|
|
||||||
confidence: 60,
|
|
||||||
found_by: DIRECT_ACCESS,
|
|
||||||
references: {
|
|
||||||
url: [
|
|
||||||
'https://www.iplocation.net/defend-wordpress-from-ddos',
|
|
||||||
'https://github.com/wpscanteam/wpscan/issues/1299'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def wp_cron_url
|
def wp_cron_url
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# 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/woo_framework_meta_generator'
|
||||||
require_relative 'main_theme/urls_in_homepage'
|
require_relative 'main_theme/urls_in_homepage'
|
||||||
|
require_relative 'main_theme/urls_in_404_page'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
@@ -14,9 +16,11 @@ module WPScan
|
|||||||
# @param [ WPScan::Target ] target
|
# @param [ WPScan::Target ] target
|
||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders <<
|
finders <<
|
||||||
MainTheme::CssStyle.new(target) <<
|
MainTheme::CssStyleInHomepage.new(target) <<
|
||||||
|
MainTheme::CssStyleIn404Page.new(target) <<
|
||||||
MainTheme::WooFrameworkMetaGenerator.new(target) <<
|
MainTheme::WooFrameworkMetaGenerator.new(target) <<
|
||||||
MainTheme::UrlsInHomepage.new(target)
|
MainTheme::UrlsInHomepage.new(target) <<
|
||||||
|
MainTheme::UrlsIn404Page.new(target)
|
||||||
end
|
end
|
||||||
end
|
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 WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module MainTheme
|
module MainTheme
|
||||||
# From the css style
|
# From the CSS style in the homepage
|
||||||
class CssStyle < CMSScanner::Finders::Finder
|
class CssStyleInHomepage < CMSScanner::Finders::Finder
|
||||||
include Finders::WpItems::URLsInHomepage
|
include Finders::WpItems::UrlsInPage # To have the item_code_pattern method available here
|
||||||
|
|
||||||
def create_theme(slug, style_url, opts)
|
def create_theme(slug, style_url, opts)
|
||||||
Model::Theme.new(
|
Model::Theme.new(
|
||||||
@@ -20,8 +20,8 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def passive_from_css_href(res, opts)
|
def passive_from_css_href(res, opts)
|
||||||
target.in_scope_uris(res, '//style/@src|//link/@href') do |uri|
|
target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri|
|
||||||
next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
|
next unless uri.path =~ %r{/themes/([^/]+)/style.css\z}i
|
||||||
|
|
||||||
return create_theme(Regexp.last_match[1], uri.to_s, opts)
|
return create_theme(Regexp.last_match[1], uri.to_s, opts)
|
||||||
end
|
end
|
||||||
@@ -33,7 +33,7 @@ module WPScan
|
|||||||
code = tag.text.to_s
|
code = tag.text.to_s
|
||||||
next if code.empty?
|
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)
|
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
||||||
end
|
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
|
module MainTheme
|
||||||
# URLs In Homepage Finder
|
# URLs In Homepage Finder
|
||||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||||
include WpItems::URLsInHomepage
|
include WpItems::UrlsInPage
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
@@ -13,7 +13,7 @@ module WPScan
|
|||||||
def passive(opts = {})
|
def passive(opts = {})
|
||||||
found = []
|
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|
|
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))
|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||||
@@ -21,6 +21,11 @@ module WPScan
|
|||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.homepage_res
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module WPScan
|
|||||||
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
|
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
|
||||||
|
|
||||||
def passive(opts = {})
|
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(
|
Model::Theme.new(
|
||||||
Regexp.last_match[1],
|
Regexp.last_match[1],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module WPScan
|
|||||||
|
|
||||||
def valid_credentials?(response)
|
def valid_credentials?(response)
|
||||||
response.code == 302 &&
|
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
|
end
|
||||||
|
|
||||||
def errored_response?(response)
|
def errored_response?(response)
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ module WPScan
|
|||||||
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
|
||||||
|
|
||||||
def login_request(username, password)
|
def login_request(username, password)
|
||||||
target.method_call('wp.getUsersBlogs', [username, password])
|
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_credentials?(response)
|
def valid_credentials?(response)
|
||||||
response.code == 200 && response.body =~ /blogName/
|
response.code == 200 && response.body.include?('blogName')
|
||||||
end
|
end
|
||||||
|
|
||||||
def errored_response?(response)
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,11 +19,33 @@ module WPScan
|
|||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
# @param [ Array<Model::User> ] users
|
# @param [ Array<Model::User> ] users
|
||||||
# @param [ Array<String> ] passwords
|
# @param [ String ] wordlist_path
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
# @option opts [ Boolean ] :show_progression
|
# @option opts [ Boolean ] :show_progression
|
||||||
# @option opts [ Integer ] :multicall_max_passwords
|
# @option opts [ Integer ] :multicall_max_passwords
|
||||||
@@ -33,18 +55,22 @@ module WPScan
|
|||||||
# TODO: Make rubocop happy about metrics etc
|
# TODO: Make rubocop happy about metrics etc
|
||||||
#
|
#
|
||||||
# rubocop:disable all
|
# rubocop:disable all
|
||||||
def attack(users, passwords, opts = {})
|
def attack(users, wordlist_path, opts = {})
|
||||||
wordlist_index = 0
|
checked_passwords = 0
|
||||||
|
wordlist = File.open(wordlist_path)
|
||||||
|
wordlist_size = wordlist.count
|
||||||
max_passwords = opts[:multicall_max_passwords]
|
max_passwords = opts[:multicall_max_passwords]
|
||||||
current_passwords_size = passwords_size(max_passwords, users.size)
|
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])
|
show_progression: opts[:show_progression])
|
||||||
|
|
||||||
|
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
current_users = users.select { |user| user.password.nil? }
|
current_users = users.select { |user| user.password.nil? }
|
||||||
current_passwords = passwords[wordlist_index, current_passwords_size]
|
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
|
||||||
wordlist_index += current_passwords_size
|
checked_passwords += current_passwords_size
|
||||||
|
|
||||||
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
||||||
|
|
||||||
@@ -76,16 +102,19 @@ module WPScan
|
|||||||
break
|
break
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
# Maybe a progress_bar.stop ?
|
# Maybe a progress_bar.stop ?
|
||||||
end
|
end
|
||||||
# rubocop:disable all
|
# rubocop:enable all
|
||||||
|
|
||||||
def passwords_size(max_passwords, users_size)
|
def passwords_size(max_passwords, users_size)
|
||||||
return 1 if 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
|
max_passwords / users_size
|
||||||
end
|
end
|
||||||
@@ -94,9 +123,13 @@ module WPScan
|
|||||||
def check_and_output_errors(res)
|
def check_and_output_errors(res)
|
||||||
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -48,18 +48,18 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ String, nil ] The version number detected from the stable tag
|
# @return [ String, nil ] The version number detected from the stable tag
|
||||||
def from_stable_tag(body)
|
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]
|
number = Regexp.last_match[1]
|
||||||
|
|
||||||
number if number =~ /[0-9]+/
|
number if /[0-9]+/.match?(number)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] body
|
# @param [ String ] body
|
||||||
#
|
#
|
||||||
# @return [ String, nil ] The best version number detected from the changelog section
|
# @return [ String, nil ] The best version number detected from the changelog section
|
||||||
def from_changelog_section(body)
|
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?
|
return if extracted_versions.nil? || extracted_versions.empty?
|
||||||
|
|
||||||
@@ -68,11 +68,9 @@ module WPScan
|
|||||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||||
|
|
||||||
sorted = extracted_versions.sort do |x, y|
|
sorted = extracted_versions.sort do |x, y|
|
||||||
begin
|
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
rescue StandardError
|
||||||
rescue StandardError
|
0
|
||||||
0
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
sorted.last
|
sorted.last
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'plugins/urls_in_homepage'
|
require_relative 'plugins/urls_in_homepage'
|
||||||
|
require_relative 'plugins/urls_in_404_page'
|
||||||
require_relative 'plugins/known_locations'
|
require_relative 'plugins/known_locations'
|
||||||
# From the DynamicFinders
|
# From the DynamicFinders
|
||||||
require_relative 'plugins/comment'
|
require_relative 'plugins/comment'
|
||||||
@@ -22,6 +23,7 @@ module WPScan
|
|||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders <<
|
finders <<
|
||||||
Plugins::UrlsInHomepage.new(target) <<
|
Plugins::UrlsInHomepage.new(target) <<
|
||||||
|
Plugins::UrlsIn404Page.new(target) <<
|
||||||
Plugins::HeaderPattern.new(target) <<
|
Plugins::HeaderPattern.new(target) <<
|
||||||
Plugins::Comment.new(target) <<
|
Plugins::Comment.new(target) <<
|
||||||
Plugins::Xpath.new(target) <<
|
Plugins::Xpath.new(target) <<
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||||
def process_response(opts, response, slug, klass, config)
|
def process_response(opts, response, slug, klass, config)
|
||||||
return unless response.body =~ config['pattern']
|
return unless response.body&.match?(config['pattern'])
|
||||||
|
|
||||||
Model::Plugin.new(
|
Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ module WPScan
|
|||||||
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
||||||
comment = node.text.to_s.strip
|
comment = node.text.to_s.strip
|
||||||
|
|
||||||
next unless comment =~ config['pattern']
|
next unless comment&.match?(config['pattern'])
|
||||||
|
|
||||||
return Model::Plugin.new(
|
return Model::Plugin.new(
|
||||||
slug,
|
slug,
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
|
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))
|
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]
|
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||||
end
|
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 Finders
|
||||||
module Plugins
|
module Plugins
|
||||||
# URLs In Homepage Finder
|
# URLs In Homepage Finder
|
||||||
# Typically, the items detected from URLs like
|
# Typically, the items detected from URLs like /wp-content/plugins/<slug>/
|
||||||
# /wp-content/plugins/<slug>/
|
|
||||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||||
include WpItems::URLsInHomepage
|
include WpItems::UrlsInPage
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
@@ -21,6 +20,11 @@ module WPScan
|
|||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.homepage_res
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ Version ]
|
# @return [ Version ]
|
||||||
def style_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(
|
Model::Version.new(
|
||||||
Regexp.last_match[1],
|
Regexp.last_match[1],
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require_relative 'themes/urls_in_homepage'
|
require_relative 'themes/urls_in_homepage'
|
||||||
|
require_relative 'themes/urls_in_404_page'
|
||||||
require_relative 'themes/known_locations'
|
require_relative 'themes/known_locations'
|
||||||
|
|
||||||
module WPScan
|
module WPScan
|
||||||
module Finders
|
module Finders
|
||||||
module Themes
|
module Themes
|
||||||
# themes Finder
|
# Themes Finder
|
||||||
class Base
|
class Base
|
||||||
include CMSScanner::Finders::SameTypeFinder
|
include CMSScanner::Finders::SameTypeFinder
|
||||||
|
|
||||||
@@ -14,6 +15,7 @@ module WPScan
|
|||||||
def initialize(target)
|
def initialize(target)
|
||||||
finders <<
|
finders <<
|
||||||
Themes::UrlsInHomepage.new(target) <<
|
Themes::UrlsInHomepage.new(target) <<
|
||||||
|
Themes::UrlsIn404Page.new(target) <<
|
||||||
Themes::KnownLocations.new(target)
|
Themes::KnownLocations.new(target)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,8 +19,12 @@ module WPScan
|
|||||||
def aggressive(opts = {})
|
def aggressive(opts = {})
|
||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
|
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))
|
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]
|
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||||
end
|
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
|
module Themes
|
||||||
# URLs In Homepage Finder
|
# URLs In Homepage Finder
|
||||||
class UrlsInHomepage < CMSScanner::Finders::Finder
|
class UrlsInHomepage < CMSScanner::Finders::Finder
|
||||||
include WpItems::URLsInHomepage
|
include WpItems::UrlsInPage
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
#
|
#
|
||||||
@@ -19,6 +19,11 @@ module WPScan
|
|||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Typhoeus::Response ]
|
||||||
|
def page_res
|
||||||
|
@page_res ||= target.homepage_res
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ module WPScan
|
|||||||
found = []
|
found = []
|
||||||
|
|
||||||
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
|
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
|
||||||
next unless res.body =~ /no image specified/i
|
next unless /no image specified/i.match?(res.body)
|
||||||
|
|
||||||
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ require_relative 'users/oembed_api'
|
|||||||
require_relative 'users/rss_generator'
|
require_relative 'users/rss_generator'
|
||||||
require_relative 'users/author_id_brute_forcing'
|
require_relative 'users/author_id_brute_forcing'
|
||||||
require_relative 'users/login_error_messages'
|
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 WPScan
|
||||||
module Finders
|
module Finders
|
||||||
@@ -22,6 +23,7 @@ module WPScan
|
|||||||
Users::WpJsonApi.new(target) <<
|
Users::WpJsonApi.new(target) <<
|
||||||
Users::OembedApi.new(target) <<
|
Users::OembedApi.new(target) <<
|
||||||
Users::RSSGenerator.new(target) <<
|
Users::RSSGenerator.new(target) <<
|
||||||
|
Users::AuthorSitemap.new(target) <<
|
||||||
Users::YoastSeoAuthorSitemap.new(target) <<
|
Users::YoastSeoAuthorSitemap.new(target) <<
|
||||||
Users::AuthorIdBruteForcing.new(target) <<
|
Users::AuthorIdBruteForcing.new(target) <<
|
||||||
Users::LoginErrorMessages.new(target)
|
Users::LoginErrorMessages.new(target)
|
||||||
|
|||||||
@@ -71,11 +71,13 @@ module WPScan
|
|||||||
return username, 'Display Name', 50 if username
|
return username, 'Display Name', 50 if username
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] url
|
# @param [ String, Addressable::URI ] uri
|
||||||
#
|
#
|
||||||
# @return [ String, nil ]
|
# @return [ String, nil ]
|
||||||
def username_from_author_url(url)
|
def username_from_author_url(uri)
|
||||||
url[%r{/author/([^/\b]+)/?}i, 1]
|
uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
|
||||||
|
|
||||||
|
uri.path[%r{/author/([^/\b]+)/?}i, 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ Typhoeus::Response ] res
|
# @param [ Typhoeus::Response ] res
|
||||||
@@ -83,12 +85,12 @@ module WPScan
|
|||||||
# @return [ String, nil ] The username found
|
# @return [ String, nil ] The username found
|
||||||
def username_from_response(res)
|
def username_from_response(res)
|
||||||
# Permalink enabled
|
# Permalink enabled
|
||||||
target.in_scope_uris(res, '//link/@href|//a/@href') do |uri|
|
target.in_scope_uris(res, '//@href[contains(., "author/")]') do |uri|
|
||||||
username = username_from_author_url(uri.to_s)
|
username = username_from_author_url(uri)
|
||||||
return username if username
|
return username if username
|
||||||
end
|
end
|
||||||
|
|
||||||
# No permalink
|
# No permalink, TODO Maybe use xpath to extract the classes ?
|
||||||
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -97,9 +99,12 @@ module WPScan
|
|||||||
# @return [ String, nil ]
|
# @return [ String, nil ]
|
||||||
def display_name_from_body(body)
|
def display_name_from_body(body)
|
||||||
page = Nokogiri::HTML.parse(body)
|
page = Nokogiri::HTML.parse(body)
|
||||||
|
|
||||||
# WP >= 3.0
|
# WP >= 3.0
|
||||||
page.css('h1.page-title span').each do |node|
|
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
|
end
|
||||||
|
|
||||||
# WP < 3.0
|
# WP < 3.0
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ module WPScan
|
|||||||
def potential_usernames(res)
|
def potential_usernames(res)
|
||||||
usernames = []
|
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
|
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
||||||
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
||||||
elsif /author=[0-9]+/.match?(uri.query)
|
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
|
||||||
@@ -24,7 +24,7 @@ module WPScan
|
|||||||
|
|
||||||
return found if error.empty? # Protection plugin / error disabled
|
return found if error.empty? # Protection plugin / error disabled
|
||||||
|
|
||||||
next unless error =~ /The password you entered for the username|Incorrect Password/i
|
next unless /The password you entered for the username|Incorrect Password/i.match?(error)
|
||||||
|
|
||||||
found << Model::User.new(username, found_by: found_by, confidence: 100)
|
found << Model::User.new(username, found_by: found_by, confidence: 100)
|
||||||
end
|
end
|
||||||
@@ -37,7 +37,7 @@ module WPScan
|
|||||||
# usernames from the potential Users found
|
# usernames from the potential Users found
|
||||||
unames = opts[:found].map(&:username)
|
unames = opts[:found].map(&:username)
|
||||||
|
|
||||||
[*opts[:list]].each { |uname| unames << uname.chomp }
|
Array(opts[:list]).each { |uname| unames << uname.chomp }
|
||||||
|
|
||||||
unames.uniq
|
unames.uniq
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ module WPScan
|
|||||||
def user_details_from_oembed_data(oembed_data)
|
def user_details_from_oembed_data(oembed_data)
|
||||||
return unless 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}
|
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
|
||||||
details = [Regexp.last_match[1], 'Author URL', 90]
|
details = [Regexp.last_match[1], 'Author URL', 90]
|
||||||
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module WPScan
|
|||||||
urls.each do |url|
|
urls.each do |url|
|
||||||
res = Browser.get_and_follow_location(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 = []
|
potential_usernames = []
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ module WPScan
|
|||||||
loop do
|
loop do
|
||||||
current_page += 1
|
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
|
total_pages ||= res.headers['X-WP-TotalPages'].to_i
|
||||||
|
|
||||||
|
|||||||
@@ -5,27 +5,7 @@ module WPScan
|
|||||||
module Users
|
module Users
|
||||||
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
|
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
|
||||||
# See https://github.com/wpscanteam/wpscan/issues/1228
|
# See https://github.com/wpscanteam/wpscan/issues/1228
|
||||||
class YoastSeoAuthorSitemap < CMSScanner::Finders::Finder
|
class YoastSeoAuthorSitemap < AuthorSitemap
|
||||||
# @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 author-sitemap
|
# @return [ String ] The URL of the author-sitemap
|
||||||
def sitemap_url
|
def sitemap_url
|
||||||
@sitemap_url ||= target.url('author-sitemap.xml')
|
@sitemap_url ||= target.url('author-sitemap.xml')
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
# frozen_string_literal: true
|
# 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 Finders
|
||||||
module WpItems
|
module WpItems
|
||||||
# URLs In Homepage Module to use in plugins & themes finders
|
# URLs In Homepage Module to use in plugins & themes finders
|
||||||
module URLsInHomepage
|
module UrlsInPage
|
||||||
# @param [ String ] type plugins / themes
|
# @param [ String ] type plugins / themes
|
||||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||||
#
|
#
|
||||||
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
|
# @return [ Array<String> ] The plugins/themes detected in the href, src attributes of the page
|
||||||
def items_from_links(type, uniq = true)
|
def items_from_links(type, uniq: true)
|
||||||
found = []
|
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)
|
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
|
end
|
||||||
|
|
||||||
uniq ? found.uniq.sort : found.sort
|
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
|
# @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
|
# @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 = []
|
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
|
code = tag.text.to_s
|
||||||
next if code.empty?
|
next if code.empty?
|
||||||
|
|
||||||
@@ -42,14 +48,14 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ Regexp ]
|
# @return [ Regexp ]
|
||||||
def item_attribute_pattern(type)
|
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
|
end
|
||||||
|
|
||||||
# @param [ String ] type
|
# @param [ String ] type
|
||||||
#
|
#
|
||||||
# @return [ Regexp ]
|
# @return [ Regexp ]
|
||||||
def item_code_pattern(type)
|
def item_code_pattern(type)
|
||||||
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
|
@item_code_pattern ||= %r{["'( ]#{item_url_pattern(type)}([^\\/)"']+)}i
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ String ] type
|
# @param [ String ] type
|
||||||
@@ -59,10 +65,10 @@ module WPScan
|
|||||||
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||||
|
|
||||||
url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
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
|
end
|
||||||
end
|
end
|
||||||
@@ -28,7 +28,7 @@ module WPScan
|
|||||||
end
|
end
|
||||||
|
|
||||||
def passive_urls_xpath
|
def passive_urls_xpath
|
||||||
'//a[contains(@href, "rdf")]/@href'
|
'//a[contains(@href, "/rdf")]/@href'
|
||||||
end
|
end
|
||||||
|
|
||||||
def aggressive_urls(_opts = {})
|
def aggressive_urls(_opts = {})
|
||||||
|
|||||||
@@ -7,46 +7,130 @@ module WPScan
|
|||||||
include References
|
include References
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
|
||||||
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
|
|
||||||
#
|
|
||||||
class BackupDB < InterestingFinding
|
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
|
end
|
||||||
|
|
||||||
class DebugLog < InterestingFinding
|
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
|
end
|
||||||
|
|
||||||
class DuplicatorInstallerLog < InterestingFinding
|
class DuplicatorInstallerLog < InterestingFinding
|
||||||
|
# @return [ Hash ]
|
||||||
|
def references
|
||||||
|
@references ||= { url: ['https://www.exploit-db.com/ghdb/3981/'] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class EmergencyPwdResetScript < InterestingFinding
|
class EmergencyPwdResetScript < InterestingFinding
|
||||||
|
def references
|
||||||
|
@references ||= {
|
||||||
|
url: ['https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script']
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class FullPathDisclosure < InterestingFinding
|
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
|
end
|
||||||
|
|
||||||
class MuPlugins < InterestingFinding
|
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
|
end
|
||||||
|
|
||||||
class Multisite < InterestingFinding
|
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
|
end
|
||||||
|
|
||||||
class Readme < InterestingFinding
|
class Readme < InterestingFinding
|
||||||
|
def to_s
|
||||||
|
@to_s ||= "WordPress readme found: #{url}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Registration < InterestingFinding
|
class Registration < InterestingFinding
|
||||||
|
# @return [ String ]
|
||||||
|
def to_s
|
||||||
|
@to_s ||= "Registration is enabled: #{url}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TmmDbMigrate < InterestingFinding
|
class TmmDbMigrate < InterestingFinding
|
||||||
|
def to_s
|
||||||
|
@to_s ||= "ThemeMakers migration file found: #{url}"
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
|
def references
|
||||||
|
@references ||= { packetstorm: [131_957] }
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class UploadDirectoryListing < InterestingFinding
|
class UploadDirectoryListing < InterestingFinding
|
||||||
|
# @return [ String ]
|
||||||
|
def to_s
|
||||||
|
@to_s ||= "Upload directory has listing enabled: #{url}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class UploadSQLDump < InterestingFinding
|
class UploadSQLDump < InterestingFinding
|
||||||
|
def to_s
|
||||||
|
@to_s ||= "SQL Dump found: #{url}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class WPCron < InterestingFinding
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,9 +15,16 @@ module WPScan
|
|||||||
@uri = Addressable::URI.parse(blog.url(path_from_blog))
|
@uri = Addressable::URI.parse(blog.url(path_from_blog))
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ JSON ]
|
# Retrieve the metadata from the vuln API if available (and a valid token is given),
|
||||||
|
# or the local metadata db otherwise
|
||||||
|
# @return [ Hash ]
|
||||||
|
def metadata
|
||||||
|
@metadata ||= db_data.empty? ? DB::Plugin.metadata_at(slug) : db_data
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
def db_data
|
def db_data
|
||||||
@db_data ||= DB::Plugin.db_data(slug)
|
@db_data ||= DB::VulnApi.plugin_data(slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
@@ -31,7 +38,7 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ Array<String> ]
|
# @return [ Array<String> ]
|
||||||
def potential_readme_filenames
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ module WPScan
|
|||||||
parse_style
|
parse_style
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Retrieve the metadata from the vuln API if available (and a valid token is given),
|
||||||
|
# or the local metadata db otherwise
|
||||||
# @return [ JSON ]
|
# @return [ JSON ]
|
||||||
|
def metadata
|
||||||
|
@metadata ||= db_data.empty? ? DB::Theme.metadata_at(slug) : db_data
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
def db_data
|
def db_data
|
||||||
@db_data ||= DB::Theme.db_data(slug)
|
@db_data ||= DB::VulnApi.theme_data(slug)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [ Hash ] opts
|
# @param [ Hash ] opts
|
||||||
@@ -38,7 +45,7 @@ module WPScan
|
|||||||
# @return [ Theme ]
|
# @return [ Theme ]
|
||||||
def parent_theme
|
def parent_theme
|
||||||
return unless template
|
return unless template
|
||||||
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
|
return unless style_body =~ /^@import\surl\(["']?([^"')]+)["']?\);\s*$/i
|
||||||
|
|
||||||
opts = detection_opts.merge(
|
opts = detection_opts.merge(
|
||||||
style_url: url(Regexp.last_match[1]),
|
style_url: url(Regexp.last_match[1]),
|
||||||
@@ -94,7 +101,7 @@ module WPScan
|
|||||||
#
|
#
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def parse_style_tag(body, tag)
|
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
|
value && !value.strip.empty? ? value.strip : nil
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ module WPScan
|
|||||||
def rce_132_vuln
|
def rce_132_vuln
|
||||||
Vulnerability.new(
|
Vulnerability.new(
|
||||||
'Timthumb <= 1.32 Remote Code Execution',
|
'Timthumb <= 1.32 Remote Code Execution',
|
||||||
{ exploitdb: ['17602'] },
|
references: { exploitdb: ['17602'] },
|
||||||
'RCE',
|
type: 'RCE',
|
||||||
'1.33'
|
fixed_in: '1.33'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -50,12 +50,12 @@ module WPScan
|
|||||||
def rce_webshot_vuln
|
def rce_webshot_vuln
|
||||||
Vulnerability.new(
|
Vulnerability.new(
|
||||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||||
{
|
references: {
|
||||||
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
|
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
|
||||||
cve: '2014-4663'
|
cve: '2014-4663'
|
||||||
},
|
},
|
||||||
'RCE',
|
type: 'RCE',
|
||||||
'2.8.14'
|
fixed_in: '2.8.14'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module WPScan
|
|||||||
|
|
||||||
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
|
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
|
||||||
|
|
||||||
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_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 [ String ] slug The plugin/theme slug
|
||||||
# @param [ Target ] blog The targeted blog
|
# @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 [ Hash ] :version_detection The options to use when looking for the version
|
||||||
# @option opts [ String ] :url The URL of the item
|
# @option opts [ String ] :url The URL of the item
|
||||||
def initialize(slug, blog, opts = {})
|
def initialize(slug, blog, opts = {})
|
||||||
@slug = URI.decode(slug)
|
@slug = Addressable::URI.unencode(slug)
|
||||||
@blog = blog
|
@blog = blog
|
||||||
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ module WPScan
|
|||||||
|
|
||||||
@vulnerabilities = []
|
@vulnerabilities = []
|
||||||
|
|
||||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
Array(db_data['vulnerabilities']).each do |json_vuln|
|
||||||
vulnerability = Vulnerability.load_from_json(json_vuln)
|
vulnerability = Vulnerability.load_from_json(json_vuln)
|
||||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||||
end
|
end
|
||||||
@@ -60,18 +60,18 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def latest_version
|
def latest_version
|
||||||
@latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
|
@latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# Not used anywhere ATM
|
# Not used anywhere ATM
|
||||||
# @return [ Boolean ]
|
# @return [ Boolean ]
|
||||||
def popular?
|
def popular?
|
||||||
@popular ||= db_data['popular']
|
@popular ||= metadata['popular'] ? true : false
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def last_updated
|
def last_updated
|
||||||
@last_updated ||= db_data['last_updated']
|
@last_updated ||= metadata['last_updated']
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Boolean ]
|
# @return [ Boolean ]
|
||||||
@@ -83,11 +83,6 @@ module WPScan
|
|||||||
end
|
end
|
||||||
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
|
# @param [ String ] path Optional path to merge with the uri
|
||||||
#
|
#
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
@@ -95,7 +90,7 @@ module WPScan
|
|||||||
return unless @uri
|
return unless @uri
|
||||||
return @uri.to_s unless path
|
return @uri.to_s unless path
|
||||||
|
|
||||||
@uri.join(URI.encode(path)).to_s
|
@uri.join(Addressable::URI.encode(path)).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Boolean ]
|
# @return [ Boolean ]
|
||||||
@@ -166,7 +161,7 @@ module WPScan
|
|||||||
# @return [ Typhoeus::Response ]
|
# @return [ Typhoeus::Response ]
|
||||||
def head_and_get(path, codes = [200], params = {})
|
def head_and_get(path, codes = [200], params = {})
|
||||||
final_path = +@path_from_blog
|
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)
|
blog.head_and_get(final_path, codes, params)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,9 +35,16 @@ module WPScan
|
|||||||
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
|
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ JSON ]
|
# Retrieve the metadata from the vuln API if available (and a valid token is given),
|
||||||
|
# or the local metadata db otherwise
|
||||||
|
# @return [ Hash ]
|
||||||
|
def metadata
|
||||||
|
@metadata ||= db_data.empty? ? DB::Version.metadata_at(number) : db_data
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
def db_data
|
def db_data
|
||||||
@db_data ||= DB::Version.db_data(number)
|
@db_data ||= DB::VulnApi.wordpress_data(number)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Array<Vulnerability> ]
|
# @return [ Array<Vulnerability> ]
|
||||||
@@ -46,7 +53,7 @@ module WPScan
|
|||||||
|
|
||||||
@vulnerabilities = []
|
@vulnerabilities = []
|
||||||
|
|
||||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
Array(db_data['vulnerabilities']).each do |json_vuln|
|
||||||
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -55,12 +62,12 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def release_date
|
def release_date
|
||||||
@release_date ||= db_data['release_date'] || 'Unknown'
|
@release_date ||= metadata['release_date'] || 'Unknown'
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def status
|
def status
|
||||||
@status ||= db_data['status'] || 'Unknown'
|
@status ||= metadata['status'] || 'Unknown'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def references
|
def references
|
||||||
{
|
@references ||= {
|
||||||
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
|
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
|
||||||
metasploit: [
|
metasploit: [
|
||||||
'auxiliary/scanner/http/wordpress_ghost_scanner',
|
'auxiliary/scanner/http/wordpress_ghost_scanner',
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
_______________________________________________________________
|
_______________________________________________________________
|
||||||
__ _______ _____
|
__ _______ _____
|
||||||
\ \ / / __ \ / ____|
|
\ \ / / __ \ / ____|
|
||||||
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
|
||||||
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
|
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
|
||||||
\ /\ / | | ____) | (__| (_| | | | |
|
\ /\ / | | ____) | (__| (_| | | | |
|
||||||
\/ \/ |_| |_____/ \___|\__,_|_| |_|
|
\/ \/ |_| |_____/ \___|\__,_|_| |_|
|
||||||
|
|
||||||
WordPress Security Scanner by the WPScan Team
|
WordPress Security Scanner by the WPScan Team
|
||||||
Version <%= WPScan::VERSION %>
|
Version <%= WPScan::VERSION %>
|
||||||
Sponsored by Sucuri - https://sucuri.net
|
<%= ' ' * ((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| -%>
|
<% @item.interesting_entries.each do |entry| -%>
|
||||||
| - <%= entry %>
|
| - <%= entry %>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<% if @users.empty? -%>
|
<% if @users.empty? -%>
|
||||||
<%= notice_icon %> No Valid Passwords Found.
|
<%= notice_icon %> No Valid Passwords Found.
|
||||||
<% else -%>
|
<% else -%>
|
||||||
<%= notice_icon %> Valid Combinations Found:
|
<%= critical_icon %> Valid Combinations Found:
|
||||||
<% @users.each do |user| -%>
|
<% @users.each do |user| -%>
|
||||||
| Username: <%= user.username %>, Password: <%= user.password %>
|
| Username: <%= user.username %>, Password: <%= user.password %>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|||||||
13
app/views/cli/vuln_api/status.erb
Normal file
13
app/views/cli/vuln_api/status.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<% unless @status.empty? -%>
|
||||||
|
<% if @status['http_error'] -%>
|
||||||
|
<%= critical_icon %> WPScan DB API, <%= @status['http_error'].to_s %>
|
||||||
|
<% else -%>
|
||||||
|
<%= info_icon %> WPScan DB API OK
|
||||||
|
| Plan: <%= @status['plan'] %>
|
||||||
|
| Requests Done (during the scan): <%= @api_requests %>
|
||||||
|
| Requests Remaining: <%= @status['requests_remaining'] %>
|
||||||
|
<% end -%>
|
||||||
|
<% else -%>
|
||||||
|
<%= warning_icon %> No WPScan 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://wpscan.com/register
|
||||||
|
<% end -%>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
| <%= critical_icon %> Title: <%= @v.title %>
|
| <%= critical_icon %> Title: <%= @v.title %>
|
||||||
|
<% if @v.cvss -%>
|
||||||
|
| CVSS: <%= @v.cvss[:score] %> (<%= @v.cvss[:vector] %>)
|
||||||
|
<% end -%>
|
||||||
<% if @v.fixed_in -%>
|
<% if @v.fixed_in -%>
|
||||||
| Fixed in: <%= @v.fixed_in %>
|
| Fixed in: <%= @v.fixed_in %>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"@_WPScan_",
|
"@_WPScan_",
|
||||||
"@ethicalhack3r",
|
"@ethicalhack3r",
|
||||||
"@erwan_lr",
|
"@erwan_lr",
|
||||||
"@_FireFart_"
|
"@firefart"
|
||||||
],
|
],
|
||||||
"sponsored_by": "Sucuri - https://sucuri.net"
|
"sponsor": <%= WPScan::DB::Sponsor.text.to_json %>
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
<% vulns.each_with_index do |v, index| -%>
|
<% vulns.each_with_index do |v, index| -%>
|
||||||
{
|
{
|
||||||
"title": <%= v.title.to_json %>,
|
"title": <%= v.title.to_json %>,
|
||||||
|
<% if v.cvss -%>
|
||||||
|
"cvss": <%= v.cvss.to_json %>,
|
||||||
|
<% end -%>
|
||||||
"fixed_in": <%= v.fixed_in.to_json %>,
|
"fixed_in": <%= v.fixed_in.to_json %>,
|
||||||
"references": <%= v.references.to_json %>
|
"references": <%= v.references.to_json %>
|
||||||
}<% unless index == last_index -%>,<% end -%>
|
}<% unless index == last_index -%>,<% end -%>
|
||||||
|
|||||||
13
app/views/json/vuln_api/status.erb
Normal file
13
app/views/json/vuln_api/status.erb
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"vuln_api": {
|
||||||
|
<% unless @status.empty? -%>
|
||||||
|
<% if @status['http_error'] -%>
|
||||||
|
"http_error": <%= @status['http_error'].to_s.to_json %>
|
||||||
|
<% else -%>
|
||||||
|
"plan": <%= @status['plan'].to_json %>,
|
||||||
|
"requests_done_during_scan": <%= @api_requests.to_json %>,
|
||||||
|
"requests_remaining": <%= @status['requests_remaining'].to_json %>
|
||||||
|
<% end -%>
|
||||||
|
<% else -%>
|
||||||
|
"error": "No WPScan 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 -%>
|
||||||
|
},
|
||||||
@@ -5,6 +5,7 @@ require 'wpscan'
|
|||||||
|
|
||||||
WPScan::Scan.new do |s|
|
WPScan::Scan.new do |s|
|
||||||
s.controllers <<
|
s.controllers <<
|
||||||
|
WPScan::Controller::VulnApi.new <<
|
||||||
WPScan::Controller::CustomDirectories.new <<
|
WPScan::Controller::CustomDirectories.new <<
|
||||||
WPScan::Controller::InterestingFindings.new <<
|
WPScan::Controller::InterestingFindings.new <<
|
||||||
WPScan::Controller::WpVersion.new <<
|
WPScan::Controller::WpVersion.new <<
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ require 'wpscan'
|
|||||||
report = MemoryProfiler.report(top: 15) do
|
report = MemoryProfiler.report(top: 15) do
|
||||||
WPScan::Scan.new do |s|
|
WPScan::Scan.new do |s|
|
||||||
s.controllers <<
|
s.controllers <<
|
||||||
|
WPScan::Controller::VulnApi.new <<
|
||||||
WPScan::Controller::CustomDirectories.new <<
|
WPScan::Controller::CustomDirectories.new <<
|
||||||
WPScan::Controller::InterestingFindings.new <<
|
WPScan::Controller::InterestingFindings.new <<
|
||||||
WPScan::Controller::WpVersion.new <<
|
WPScan::Controller::WpVersion.new <<
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ StackProf.run(mode: :cpu, out: '/tmp/stackprof-cpu.dump', interval: 500) do
|
|||||||
# require_relative 'wpscan' doesn't work
|
# require_relative 'wpscan' doesn't work
|
||||||
WPScan::Scan.new do |s|
|
WPScan::Scan.new do |s|
|
||||||
s.controllers <<
|
s.controllers <<
|
||||||
|
WPScan::Controller::VulnApi.new <<
|
||||||
WPScan::Controller::CustomDirectories.new <<
|
WPScan::Controller::CustomDirectories.new <<
|
||||||
WPScan::Controller::InterestingFindings.new <<
|
WPScan::Controller::InterestingFindings.new <<
|
||||||
WPScan::Controller::WpVersion.new <<
|
WPScan::Controller::WpVersion.new <<
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ require 'uri'
|
|||||||
require 'time'
|
require 'time'
|
||||||
require 'readline'
|
require 'readline'
|
||||||
require 'securerandom'
|
require 'securerandom'
|
||||||
|
# Monkey Patches/Fixes/Override
|
||||||
|
require 'wpscan/typhoeus/response' # Adds a from_vuln_api? method
|
||||||
# Custom Libs
|
# Custom Libs
|
||||||
require 'wpscan/helper'
|
require 'wpscan/helper'
|
||||||
require 'wpscan/db'
|
require 'wpscan/db'
|
||||||
@@ -38,12 +39,28 @@ module WPScan
|
|||||||
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
|
APP_DIR = Pathname.new(__FILE__).dirname.join('..', 'app').expand_path
|
||||||
DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db')
|
DB_DIR = Pathname.new(Dir.home).join('.wpscan', 'db')
|
||||||
|
|
||||||
|
Typhoeus.on_complete do |response|
|
||||||
|
next if response.cached? || !response.from_vuln_api?
|
||||||
|
|
||||||
|
self.api_requests += 1
|
||||||
|
end
|
||||||
|
|
||||||
# Override, otherwise it would be returned as 'wp_scan'
|
# Override, otherwise it would be returned as 'wp_scan'
|
||||||
#
|
#
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def self.app_name
|
def self.app_name
|
||||||
'wpscan'
|
'wpscan'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# @return [ Integer ]
|
||||||
|
def self.api_requests
|
||||||
|
@@api_requests ||= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [ Integer ] value
|
||||||
|
def self.api_requests=(value)
|
||||||
|
@@api_requests = value
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
require "#{WPScan::APP_DIR}/app"
|
require "#{WPScan::APP_DIR}/app"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ module WPScan
|
|||||||
|
|
||||||
# @return [ String ]
|
# @return [ String ]
|
||||||
def default_user_agent
|
def default_user_agent
|
||||||
"WPScan v#{VERSION} (https://wpscan.org/)"
|
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.com/wordpress-security-scanner)"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,9 +7,12 @@ require_relative 'db/plugins'
|
|||||||
require_relative 'db/themes'
|
require_relative 'db/themes'
|
||||||
require_relative 'db/plugin'
|
require_relative 'db/plugin'
|
||||||
require_relative 'db/theme'
|
require_relative 'db/theme'
|
||||||
|
require_relative 'db/sponsor'
|
||||||
require_relative 'db/wp_version'
|
require_relative 'db/wp_version'
|
||||||
require_relative 'db/fingerprints'
|
require_relative 'db/fingerprints'
|
||||||
|
|
||||||
|
require_relative 'db/vuln_api'
|
||||||
|
|
||||||
require_relative 'db/dynamic_finders/base'
|
require_relative 'db/dynamic_finders/base'
|
||||||
require_relative 'db/dynamic_finders/plugin'
|
require_relative 'db/dynamic_finders/plugin'
|
||||||
require_relative 'db/dynamic_finders/theme'
|
require_relative 'db/dynamic_finders/theme'
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ module WPScan
|
|||||||
|
|
||||||
finder_configs(
|
finder_configs(
|
||||||
finder_class,
|
finder_class,
|
||||||
Regexp.last_match[1] == 'aggressive'
|
aggressive: Regexp.last_match[1] == 'aggressive'
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ module WPScan
|
|||||||
# @param [ Symbol ] finder_class
|
# @param [ Symbol ] finder_class
|
||||||
# @param [ Boolean ] aggressive
|
# @param [ Boolean ] aggressive
|
||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def self.finder_configs(finder_class, aggressive = false)
|
def self.finder_configs(finder_class, aggressive: false)
|
||||||
configs = {}
|
configs = {}
|
||||||
|
|
||||||
return configs unless allowed_classes.include?(finder_class)
|
return configs unless allowed_classes.include?(finder_class)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ module WPScan
|
|||||||
# @param [ Symbol ] finder_class
|
# @param [ Symbol ] finder_class
|
||||||
# @param [ Boolean ] aggressive
|
# @param [ Boolean ] aggressive
|
||||||
# @return [ Hash ]
|
# @return [ Hash ]
|
||||||
def self.finder_configs(finder_class, aggressive = false)
|
def self.finder_configs(finder_class, aggressive: false)
|
||||||
configs = {}
|
configs = {}
|
||||||
|
|
||||||
return configs unless allowed_classes.include?(finder_class)
|
return configs unless allowed_classes.include?(finder_class)
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ module WPScan
|
|||||||
module DB
|
module DB
|
||||||
# Plugin DB
|
# Plugin DB
|
||||||
class Plugin < WpItem
|
class Plugin < WpItem
|
||||||
# @return [ String ]
|
# @return [ Hash ]
|
||||||
def self.db_file
|
def self.metadata
|
||||||
@db_file ||= DB_DIR.join('plugins.json').to_s
|
@metadata ||= super['plugins'] || {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ module WPScan
|
|||||||
# WP Plugins
|
# WP Plugins
|
||||||
class Plugins < WpItems
|
class Plugins < WpItems
|
||||||
# @return [ JSON ]
|
# @return [ JSON ]
|
||||||
def self.db
|
def self.metadata
|
||||||
Plugin.db
|
Plugin.metadata
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
16
lib/wpscan/db/sponsor.rb
Normal file
16
lib/wpscan/db/sponsor.rb
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module DB
|
||||||
|
class Sponsor
|
||||||
|
# @return [ Hash ]
|
||||||
|
def self.text
|
||||||
|
@text ||= file_path.exist? ? File.read(file_path).chomp : ''
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.file_path
|
||||||
|
@file_path ||= DB_DIR.join('sponsor.txt')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -4,9 +4,9 @@ module WPScan
|
|||||||
module DB
|
module DB
|
||||||
# Theme DB
|
# Theme DB
|
||||||
class Theme < WpItem
|
class Theme < WpItem
|
||||||
# @return [ String ]
|
# @return [ Hash ]
|
||||||
def self.db_file
|
def self.metadata
|
||||||
@db_file ||= DB_DIR.join('themes.json').to_s
|
@metadata ||= super['themes'] || {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ module WPScan
|
|||||||
# WP Themes
|
# WP Themes
|
||||||
class Themes < WpItems
|
class Themes < WpItems
|
||||||
# @return [ JSON ]
|
# @return [ JSON ]
|
||||||
def self.db
|
def self.metadata
|
||||||
Theme.db
|
Theme.metadata
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,12 +7,15 @@ module WPScan
|
|||||||
class Updater
|
class Updater
|
||||||
# /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
|
# /!\ Might want to also update the Enumeration#cli_options when some filenames are changed here
|
||||||
FILES = %w[
|
FILES = %w[
|
||||||
plugins.json themes.json wordpresses.json
|
metadata.json wp_fingerprints.json
|
||||||
timthumbs-v3.txt config_backups.txt db_exports.txt
|
timthumbs-v3.txt config_backups.txt db_exports.txt
|
||||||
dynamic_finders.yml wp_fingerprints.json LICENSE
|
dynamic_finders.yml LICENSE sponsor.txt
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
OLD_FILES = %w[wordpress.db user-agents.txt dynamic_finders_01.yml].freeze
|
OLD_FILES = %w[
|
||||||
|
wordpress.db user-agents.txt dynamic_finders_01.yml
|
||||||
|
wordpresses.json plugins.json themes.json
|
||||||
|
].freeze
|
||||||
|
|
||||||
attr_reader :repo_directory
|
attr_reader :repo_directory
|
||||||
|
|
||||||
@@ -64,12 +67,13 @@ module WPScan
|
|||||||
# @return [ Hash ] The params for Typhoeus::Request
|
# @return [ Hash ] The params for Typhoeus::Request
|
||||||
# @note Those params can't be overriden by CLI options
|
# @note Those params can't be overriden by CLI options
|
||||||
def request_params
|
def request_params
|
||||||
{
|
@request_params ||= Browser.instance.default_connect_request_params.merge(
|
||||||
timeout: 600,
|
timeout: 600,
|
||||||
connecttimeout: 300,
|
connecttimeout: 300,
|
||||||
accept_encoding: 'gzip, deflate',
|
accept_encoding: 'gzip, deflate',
|
||||||
cache_ttl: 0
|
cache_ttl: 0,
|
||||||
}
|
headers: { 'User-Agent' => Browser.instance.default_user_agent }
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ String ] The raw file URL associated with the given filename
|
# @return [ String ] The raw file URL associated with the given filename
|
||||||
@@ -81,7 +85,7 @@ module WPScan
|
|||||||
def remote_file_checksum(filename)
|
def remote_file_checksum(filename)
|
||||||
url = "#{remote_file_url(filename)}.sha512"
|
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
|
raise Error::Download, res if res.timed_out? || res.code != 200
|
||||||
|
|
||||||
res.body.chomp
|
res.body.chomp
|
||||||
@@ -122,7 +126,7 @@ module WPScan
|
|||||||
file_path = local_file_path(filename)
|
file_path = local_file_path(filename)
|
||||||
file_url = remote_file_url(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
|
raise Error::Download, res if res.timed_out? || res.code != 200
|
||||||
|
|
||||||
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
||||||
@@ -135,24 +139,22 @@ module WPScan
|
|||||||
updated = []
|
updated = []
|
||||||
|
|
||||||
FILES.each do |filename|
|
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
|
# Checking if the file needs to be updated
|
||||||
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||||
|
|
||||||
create_backup(filename)
|
create_backup(filename)
|
||||||
dl_checksum = download(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
|
updated << filename
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
restore_backup(filename)
|
restore_backup(filename)
|
||||||
raise e
|
raise e
|
||||||
ensure
|
ensure
|
||||||
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
File.write(last_update_file, Time.now)
|
File.write(last_update_file, Time.now)
|
||||||
|
|||||||
82
lib/wpscan/db/vuln_api.rb
Normal file
82
lib/wpscan/db/vuln_api.rb
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module WPScan
|
||||||
|
module DB
|
||||||
|
# WPVulnDB API
|
||||||
|
class VulnApi
|
||||||
|
NON_ERROR_CODES = [200, 403].freeze
|
||||||
|
|
||||||
|
class << self
|
||||||
|
attr_accessor :token
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Addressable::URI ]
|
||||||
|
def self.uri
|
||||||
|
@uri ||= Addressable::URI.parse('https://wpscan.com/api/v3/')
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param [ String ] path
|
||||||
|
# @param [ Hash ] params
|
||||||
|
#
|
||||||
|
# @return [ Hash ]
|
||||||
|
def self.get(path, params = {})
|
||||||
|
return {} unless token
|
||||||
|
return {} if path.end_with?('/latest') # Remove this when api/v4 is up
|
||||||
|
|
||||||
|
# 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 || res.code == 429
|
||||||
|
return JSON.parse(res.body) if NON_ERROR_CODES.include?(res.code)
|
||||||
|
|
||||||
|
raise Error::HTTP, res
|
||||||
|
rescue Error::HTTP => e
|
||||||
|
retries ||= 0
|
||||||
|
|
||||||
|
if (retries += 1) <= 3
|
||||||
|
@default_request_params[:headers]['X-Retry'] = retries
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
retry
|
||||||
|
end
|
||||||
|
|
||||||
|
{ 'http_error' => e }
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
|
def self.plugin_data(slug)
|
||||||
|
get("plugins/#{slug}")&.dig(slug) || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
|
def self.theme_data(slug)
|
||||||
|
get("themes/#{slug}")&.dig(slug) || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
|
def self.wordpress_data(version_number)
|
||||||
|
get("wordpresses/#{version_number.tr('.', '')}")&.dig(version_number) || {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
|
def self.status
|
||||||
|
json = get('status', params: { version: WPScan::VERSION }, cache_ttl: 0)
|
||||||
|
|
||||||
|
json['requests_remaining'] = 'Unlimited' if json['requests_remaining'] == -1
|
||||||
|
|
||||||
|
json
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ Hash ]
|
||||||
|
# @note Those params can not be overriden by CLI options
|
||||||
|
def self.default_request_params
|
||||||
|
@default_request_params ||= Browser.instance.default_connect_request_params.merge(
|
||||||
|
headers: {
|
||||||
|
'User-Agent' => Browser.instance.default_user_agent,
|
||||||
|
'Authorization' => "Token token=#{token}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -6,14 +6,19 @@ module WPScan
|
|||||||
class WpItem
|
class WpItem
|
||||||
# @param [ String ] identifier The plugin/theme slug or version number
|
# @param [ String ] identifier The plugin/theme slug or version number
|
||||||
#
|
#
|
||||||
# @return [ Hash ] The JSON data from the DB associated to the identifier
|
# @return [ Hash ] The JSON data from the metadata associated to the identifier
|
||||||
def self.db_data(identifier)
|
def self.metadata_at(identifier)
|
||||||
db[identifier] || {}
|
metadata[identifier] || {}
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ JSON ]
|
# @return [ JSON ]
|
||||||
def self.db
|
def self.metadata
|
||||||
@db ||= read_json_file(db_file)
|
@metadata ||= read_json_file(metadata_file)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [ String ]
|
||||||
|
def self.metadata_file
|
||||||
|
@metadata_file ||= DB_DIR.join('metadata.json').to_s
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,17 +6,17 @@ module WPScan
|
|||||||
class WpItems
|
class WpItems
|
||||||
# @return [ Array<String> ] The slug of all items
|
# @return [ Array<String> ] The slug of all items
|
||||||
def self.all_slugs
|
def self.all_slugs
|
||||||
db.keys
|
metadata.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Array<String> ] The slug of all popular items
|
# @return [ Array<String> ] The slug of all popular items
|
||||||
def self.popular_slugs
|
def self.popular_slugs
|
||||||
db.select { |_key, item| item['popular'] == true }.keys
|
metadata.select { |_key, item| item['popular'] == true }.keys
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [ Array<String> ] The slug of all vulnerable items
|
# @return [ Array<String> ] The slug of all vulnerable items
|
||||||
def self.vulnerable_slugs
|
def self.vulnerable_slugs
|
||||||
db.reject { |_key, item| item['vulnerabilities'].empty? }.keys
|
metadata.select { |_key, item| item['vulnerabilities'] == true }.keys
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ module WPScan
|
|||||||
module DB
|
module DB
|
||||||
# WP Version
|
# WP Version
|
||||||
class Version < WpItem
|
class Version < WpItem
|
||||||
# @return [ String ]
|
# @return [ Hash ]
|
||||||
def self.db_file
|
def self.metadata
|
||||||
@db_file ||= DB_DIR.join('wordpresses.json').to_s
|
@metadata ||= super['wordpress'] || {}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,5 +12,6 @@ end
|
|||||||
require_relative 'errors/enumeration'
|
require_relative 'errors/enumeration'
|
||||||
require_relative 'errors/http'
|
require_relative 'errors/http'
|
||||||
require_relative 'errors/update'
|
require_relative 'errors/update'
|
||||||
|
require_relative 'errors/vuln_api'
|
||||||
require_relative 'errors/wordpress'
|
require_relative 'errors/wordpress'
|
||||||
require_relative 'errors/xmlrpc'
|
require_relative 'errors/xmlrpc'
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user