Compare commits
430 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97ad2dc158 | ||
|
|
8c1b8b5fda | ||
|
|
e179b7ae66 | ||
|
|
3af18b4fcb | ||
|
|
f4ddff64e9 | ||
|
|
0f587c4292 | ||
|
|
cc7f284d93 | ||
|
|
ebe6c50e15 | ||
|
|
19a4671e59 | ||
|
|
e505afe9df | ||
|
|
2632c6ed86 | ||
|
|
2f5ad3a338 | ||
|
|
7ea45a5f9b | ||
|
|
9d06ffe83a | ||
|
|
11394eaa9f | ||
|
|
8353161451 | ||
|
|
cbc52b977f | ||
|
|
e0b47d7501 | ||
|
|
d92152a557 | ||
|
|
2cc243ac13 | ||
|
|
e735a68102 | ||
|
|
d50ee2217e | ||
|
|
e78d948f82 | ||
|
|
dd10991f93 | ||
|
|
99963c9b24 | ||
|
|
9acac36b85 | ||
|
|
460f10b4ad | ||
|
|
48f5e7c4ab | ||
|
|
866504c2ab | ||
|
|
ec6417feed | ||
|
|
b02f65888c | ||
|
|
9db97d0730 | ||
|
|
36a35279f2 | ||
|
|
d9f3c682d0 | ||
|
|
42dee6cfa9 | ||
|
|
d41e73727a | ||
|
|
9b2755020d | ||
|
|
91388a787a | ||
|
|
2690ab324f | ||
|
|
84993f7bd6 | ||
|
|
5b5cac925c | ||
|
|
854ad45d84 | ||
|
|
7078943b90 | ||
|
|
d59a4799f5 | ||
|
|
cac9d4cc71 | ||
|
|
8aff900d4a | ||
|
|
3c99593599 | ||
|
|
d14bc739c8 | ||
|
|
6060fc7a69 | ||
|
|
d79163fcb5 | ||
|
|
145cae912b | ||
|
|
058eac160f | ||
|
|
f38cac8d8a | ||
|
|
2e19a423fc | ||
|
|
52e3e25741 | ||
|
|
1201ecbfd3 | ||
|
|
75de6316d2 | ||
|
|
4dbef70bd2 | ||
|
|
a0b5fb1107 | ||
|
|
14800f1f6c | ||
|
|
78231becd9 | ||
|
|
da180e1e20 | ||
|
|
c48be5e980 | ||
|
|
98a71d3af6 | ||
|
|
111693ce9e | ||
|
|
d926520b29 | ||
|
|
1a6e359d02 | ||
|
|
46d7ce0a65 | ||
|
|
fd63bfd5fa | ||
|
|
0778d7e5f6 | ||
|
|
9cb53bbf43 | ||
|
|
a98e37918b | ||
|
|
1d18514ab5 | ||
|
|
75d6a16298 | ||
|
|
d0ce7cb5c5 | ||
|
|
fa0d068c30 | ||
|
|
33d5199f51 | ||
|
|
ac14ce71be | ||
|
|
f4bb6e521e | ||
|
|
5f8aa862b4 | ||
|
|
3621f4cc15 | ||
|
|
b6e36b2605 | ||
|
|
7b55570cbb | ||
|
|
308997523c | ||
|
|
90433d77c6 | ||
|
|
c4bc3bf0e7 | ||
|
|
e9638bee06 | ||
|
|
850662902b | ||
|
|
26867873e2 | ||
|
|
7f491c2403 | ||
|
|
776ca22e77 | ||
|
|
dec31b5a1c | ||
|
|
96dbe526cf | ||
|
|
3113e7309e | ||
|
|
1809c6c195 | ||
|
|
adb84ef7da | ||
|
|
824697490f | ||
|
|
62e01cb9d6 | ||
|
|
87c2f82b80 | ||
|
|
f887f8baa4 | ||
|
|
4d00d97be9 | ||
|
|
b0e946ee29 | ||
|
|
1220b9f47b | ||
|
|
12d2d0ffb0 | ||
|
|
4581113741 | ||
|
|
a20c769eae | ||
|
|
3259316cf1 | ||
|
|
9cc06234e4 | ||
|
|
1ee73268d7 | ||
|
|
f477620899 | ||
|
|
8a9dc1ce2c | ||
|
|
b584aa24bd | ||
|
|
8dfe78a210 | ||
|
|
7143cb5def | ||
|
|
e6c49d99b6 | ||
|
|
6e71f9771c | ||
|
|
452126b56a | ||
|
|
28dfd8b3b9 | ||
|
|
d3196bc03f | ||
|
|
0bff3231cd | ||
|
|
6e9d147dd0 | ||
|
|
9a7872a7c4 | ||
|
|
e8f10fb2db | ||
|
|
221f3fcbfd | ||
|
|
ab5153363f | ||
|
|
8576145d3f | ||
|
|
7908fb7d97 | ||
|
|
44b934540e | ||
|
|
9978595237 | ||
|
|
109c701e4f | ||
|
|
11f35d86ff | ||
|
|
fa3005f2b7 | ||
|
|
f30255d6d8 | ||
|
|
183df75112 | ||
|
|
105d06c8f8 | ||
|
|
82941906ca | ||
|
|
470fbb1ff3 | ||
|
|
8c6234879e | ||
|
|
689252c715 | ||
|
|
19cf00227b | ||
|
|
c9795dc560 | ||
|
|
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 |
7
.codeclimate.yml
Normal file
7
.codeclimate.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: "2"
|
||||
# https://docs.codeclimate.com/docs/default-analysis-configuration#sample-codeclimateyml
|
||||
checks:
|
||||
method-complexity:
|
||||
enabled: true
|
||||
config:
|
||||
threshold: 15
|
||||
@@ -14,3 +14,4 @@ Dockerfile
|
||||
*.orig
|
||||
bin/wpscan-*
|
||||
.wpscan/
|
||||
.github/
|
||||
|
||||
17
.github/dependabot.yml
vendored
Normal file
17
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "bundler"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every weekday
|
||||
interval: "daily"
|
||||
42
.github/workflows/build.yml
vendored
Normal file
42
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
ruby: [2.5, 2.6, 2.7, 3.0]
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Set up Ruby ${{ matrix.ruby }}
|
||||
uses: actions/setup-ruby@v1.1.3
|
||||
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
|
||||
continue-on-error: true
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
56
.github/workflows/docker.yml
vendored
Normal file
56
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Build Docker Images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
release:
|
||||
types: [published]
|
||||
schedule:
|
||||
- cron: "0 7 * * *"
|
||||
|
||||
jobs:
|
||||
images:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: checkout sources
|
||||
uses: actions/checkout@v2.4.0
|
||||
|
||||
- name: Set tag to latest
|
||||
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'schedule'
|
||||
run: |
|
||||
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
|
||||
|
||||
- name: Set tag to release name
|
||||
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/')
|
||||
run: |
|
||||
echo "DOCKER_TAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Check if DOCKER_TAG is set
|
||||
if: env.DOCKER_TAG == ''
|
||||
run: |
|
||||
echo DOCKER_TAG is not set!
|
||||
exit 1
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v1.10.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm/v7,linux/arm64
|
||||
push: true
|
||||
tags: wpscanteam/wpscan:${{ env.DOCKER_TAG }}
|
||||
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@v2.4.0
|
||||
- name: Set up Ruby 2.6
|
||||
uses: actions/setup-ruby@v1.1.3
|
||||
with:
|
||||
ruby-version: 2.6.x
|
||||
|
||||
#- name: Publish to GPR
|
||||
# run: |
|
||||
# mkdir -p $HOME/.gem
|
||||
# touch $HOME/.gem/credentials
|
||||
# chmod 0600 $HOME/.gem/credentials
|
||||
# printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||
# gem build *.gemspec
|
||||
# gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
|
||||
# env:
|
||||
# GEM_HOST_API_KEY: ${{secrets.GITHUB_TOKEN}}
|
||||
# OWNER: wpscanteam
|
||||
|
||||
- name: Publish to RubyGems
|
||||
run: |
|
||||
mkdir -p $HOME/.gem
|
||||
touch $HOME/.gem/credentials
|
||||
chmod 0600 $HOME/.gem/credentials
|
||||
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
|
||||
gem build *.gemspec
|
||||
gem push *.gem
|
||||
env:
|
||||
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
|
||||
5
.rspec
5
.rspec
@@ -1,3 +1,2 @@
|
||||
--color
|
||||
--fail-fast
|
||||
--require spec_helper
|
||||
--require spec_helper
|
||||
--color
|
||||
18
.rubocop.yml
18
.rubocop.yml
@@ -1,13 +1,21 @@
|
||||
require: rubocop-performance
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
NewCops: enable
|
||||
SuggestExtensions: false
|
||||
TargetRubyVersion: 2.5
|
||||
Exclude:
|
||||
- '*.gemspec'
|
||||
- 'vendor/**/*'
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
Lint/ConstantDefinitionInBlock:
|
||||
Enabled: false
|
||||
Lint/MissingSuper:
|
||||
Enabled: false
|
||||
Lint/UriEscapeUnescape:
|
||||
Enabled: false
|
||||
Metrics/AbcSize:
|
||||
Max: 25
|
||||
Max: 27
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
@@ -16,13 +24,13 @@ Metrics/ClassLength:
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 8
|
||||
Metrics/LineLength:
|
||||
Max: 120
|
||||
Max: 10
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 11
|
||||
Style/ClassVars:
|
||||
Enabled: false
|
||||
Style/Documentation:
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.2
|
||||
3.0.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
|
||||
enable_coverage :branch # Only supported for Ruby >= 2.5
|
||||
|
||||
add_filter '/spec/'
|
||||
add_filter 'helper'
|
||||
end
|
||||
end
|
||||
|
||||
16
.travis.yml
16
.travis.yml
@@ -1,16 +0,0 @@
|
||||
language: ruby
|
||||
sudo: false
|
||||
cache: bundler
|
||||
rvm:
|
||||
- 2.4.9
|
||||
- 2.5.7
|
||||
- 2.6.5
|
||||
before_install:
|
||||
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
|
||||
- gem update --system
|
||||
script:
|
||||
- bundle exec rubocop
|
||||
- bundle exec rspec
|
||||
notifications:
|
||||
email:
|
||||
- team@wpscan.org
|
||||
21
Dockerfile
21
Dockerfile
@@ -1,16 +1,16 @@
|
||||
FROM ruby:2.6.3-alpine AS builder
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
FROM ruby:3.0.2-alpine AS builder
|
||||
LABEL maintainer="WPScan Team <contact@wpscan.com>"
|
||||
|
||||
ARG BUNDLER_ARGS="--jobs=8 --without test development"
|
||||
|
||||
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
|
||||
RUN echo "install: --no-document --no-post-install-message\nupdate: --no-document --no-post-install-message" > /etc/gemrc
|
||||
|
||||
COPY . /wpscan
|
||||
|
||||
RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
|
||||
bundle install --system --clean --no-cache --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
|
||||
# temp fix for https://github.com/bundler/bundler/issues/6680
|
||||
rm -rf /usr/local/bundle/cache
|
||||
bundle config force_ruby_platform true && \
|
||||
bundle config disable_version_check 'true' && \
|
||||
bundle config without "test development" && \
|
||||
bundle config path.system 'true' && \
|
||||
bundle install --gemfile=/wpscan/Gemfile --jobs=8
|
||||
|
||||
WORKDIR /wpscan
|
||||
RUN rake install --trace
|
||||
@@ -19,8 +19,9 @@ RUN rake install --trace
|
||||
RUN chmod -R a+r /usr/local/bundle
|
||||
|
||||
|
||||
FROM ruby:2.6.3-alpine
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
FROM ruby:3.0.2-alpine
|
||||
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
|
||||
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -27,7 +27,7 @@ Example cases which do not require a commercial license, and thus fall under the
|
||||
- Using WPScan to test your own systems.
|
||||
- Any non-commercial use of WPScan.
|
||||
|
||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
||||
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;
|
||||
|
||||
|
||||
61
README.md
61
README.md
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
</a>
|
||||
</p>
|
||||
@@ -7,15 +7,16 @@
|
||||
<h3 align="center">WPScan</h3>
|
||||
|
||||
<p align="center">
|
||||
WordPress Vulnerability Scanner
|
||||
WordPress Security Scanner
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://wpscan.org/" title="homepage" target="_blank">Homepage</a> - <a href="https://wpscan.io/" title="wpscan.io" target="_blank">WPScan.io</a> - <a href="https://wpvulndb.com/" title="vulnerability database" target="_blank">Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress plugin" target="_blank">WordPress Plugin</a>
|
||||
<a href="https://wpscan.com/" title="homepage" target="_blank">WPScan WordPress Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress security plugin" target="_blank">WordPress Security Plugin</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a>
|
||||
<a href="https://travis-ci.org/wpscanteam/wpscan" target="_blank"><img src="https://travis-ci.org/wpscanteam/wpscan.svg?branch=master"></a>
|
||||
<a href="https://hub.docker.com/r/wpscanteam/wpscan/" target="_blank"><img src="https://img.shields.io/docker/pulls/wpscanteam/wpscan.svg"></a>
|
||||
<a href="https://github.com/wpscanteam/wpscan/actions?query=workflow%3ABuild" target="_blank"><img src="https://github.com/wpscanteam/wpscan/workflows/Build/badge.svg"></a>
|
||||
<a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
|
||||
</p>
|
||||
|
||||
@@ -24,14 +25,23 @@
|
||||
## Prerequisites
|
||||
|
||||
- (Optional but highly recommended: [RVM](https://rvm.io/rvm/install))
|
||||
- Ruby >= 2.3 - Recommended: latest
|
||||
- Ruby >= 2.5 - Recommended: latest
|
||||
- Ruby 2.5.0 to 2.5.3 can cause an 'undefined symbol: rmpd_util_str_to_d' error in some systems, see [#1283](https://github.com/wpscanteam/wpscan/issues/1283)
|
||||
- Curl >= 7.21 - Recommended: latest
|
||||
- Curl >= 7.72 - Recommended: latest
|
||||
- The 7.29 has a segfault
|
||||
- The < 7.72 could result in `Stream error in the HTTP/2 framing layer` in some cases
|
||||
- RubyGems - Recommended: latest
|
||||
- Nokogiri might require packages to be installed via your package manager depending on your OS, see https://nokogiri.org/tutorials/installing_nokogiri.html
|
||||
|
||||
### From RubyGems (Recommended)
|
||||
### In a Pentesting distribution
|
||||
|
||||
When using a pentesting distubution (such as Kali Linux), it is recommended to install/update wpscan via the package manager if available.
|
||||
|
||||
### In macOSX via Homebrew
|
||||
|
||||
`brew install wpscanteam/tap/wpscan`
|
||||
|
||||
### From RubyGems
|
||||
|
||||
```shell
|
||||
gem install wpscan
|
||||
@@ -39,23 +49,11 @@ gem install wpscan
|
||||
|
||||
On MacOSX, if a ```Gem::FilePermissionError``` is raised due to the Apple's System Integrity Protection (SIP), either install RVM and install wpscan again, or run ```sudo gem install -n /usr/local/bin wpscan``` (see [#1286](https://github.com/wpscanteam/wpscan/issues/1286))
|
||||
|
||||
### From sources (NOT Recommended)
|
||||
|
||||
Prerequisites: Git
|
||||
|
||||
```shell
|
||||
git clone https://github.com/wpscanteam/wpscan
|
||||
|
||||
cd wpscan/
|
||||
|
||||
bundle install && rake install
|
||||
```
|
||||
|
||||
# Updating
|
||||
|
||||
You can update the local database by using ```wpscan --update```
|
||||
|
||||
Updating WPScan itself is either done via ```gem update wpscan``` or the packages manager (this is quite important for distributions such as in Kali Linux: ```apt-get update && apt-get upgrade```) depending how WPScan was (pre)installed
|
||||
Updating WPScan itself is either done via ```gem update wpscan``` or the packages manager (this is quite important for distributions such as in Kali Linux: ```apt-get update && apt-get upgrade```) depending on how WPScan was (pre)installed
|
||||
|
||||
# Docker
|
||||
|
||||
@@ -77,6 +75,8 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
|
||||
|
||||
# Usage
|
||||
|
||||
Full user documentation can be found here; https://github.com/wpscanteam/wpscan/wiki/WPScan-User-Documentation
|
||||
|
||||
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings.
|
||||
|
||||
If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
|
||||
@@ -86,9 +86,19 @@ For more options, open a terminal and type ```wpscan --help``` (if you built wps
|
||||
|
||||
The DB is located at ~/.wpscan/db
|
||||
|
||||
## Vulnerability Database
|
||||
## Optional: WordPress Vulnerability Database API
|
||||
|
||||
The WPScan CLI tool uses the [WPVulnDB API](https://wpvulndb.com/api) to retrieve WordPress vulnerability data in real time. For WPScan to retrieve the vulnerability data an API token must be supplied via the `--api-token` option, or via a configuration file, as discussed below. An API token can be obtained by registering an account on [WPVulnDB](https://wpvulndb.com/users/sign_up). Up to 50 API requests per day are given free of charge to registered users. Once the 50 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data. Users can upgrade to paid API usage to increase their API limits within their user profile on [WPVulnDB](https://wpvulndb.com/).
|
||||
The WPScan CLI tool uses the [WordPress Vulnerability Database 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.com](https://wpscan.com/register).
|
||||
|
||||
Up to 25 API requests per day are given free of charge, that should be suitable to scan most WordPress websites at least once per day. When the daily 25 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.com](https://wpscan.com/).
|
||||
|
||||
#### The Free plan allows 25 API requests per day. View the different [available API plans](https://wpscan.com/api).
|
||||
|
||||
### How many API requests do you need?
|
||||
|
||||
- Our WordPress scanner makes one API request for the WordPress version, one request per installed plugin and one request per installed theme.
|
||||
- On average, a WordPress website has 22 installed plugins.
|
||||
- The Free plan should cover around 50% of all WordPress websites.
|
||||
|
||||
## Load CLI options from file/s
|
||||
|
||||
@@ -130,6 +140,11 @@ cli_options:
|
||||
api_token: YOUR_API_TOKEN
|
||||
```
|
||||
|
||||
## Load API Token From ENV (since v3.7.10)
|
||||
|
||||
The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
|
||||
|
||||
|
||||
## Enumerating usernames
|
||||
|
||||
```shell
|
||||
@@ -177,7 +192,7 @@ Example cases which do not require a commercial license, and thus fall under the
|
||||
- Using WPScan to test your own systems.
|
||||
- Any non-commercial use of WPScan.
|
||||
|
||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
||||
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;
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ module WPScan
|
||||
def cli_options
|
||||
[OptURL.new(['--url URL', 'The URL of the blog to scan'],
|
||||
required_unless: %i[update help hh version], default_protocol: 'http')] +
|
||||
super.drop(1) + # delete the --url from CMSScanner
|
||||
super.drop(2) + # delete the --url and --force from CMSScanner
|
||||
[
|
||||
OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'],
|
||||
choices: %w[apache iis nginx],
|
||||
normalize: %i[downcase to_sym],
|
||||
advanced: true),
|
||||
OptBoolean.new(['--force', 'Do not check if the target is running WordPress']),
|
||||
OptBoolean.new(['--force', 'Do not check if the target is running WordPress or returns a 403']),
|
||||
OptBoolean.new(['--[no-]update', 'Whether or not to update the Database'])
|
||||
]
|
||||
end
|
||||
@@ -39,7 +39,7 @@ module WPScan
|
||||
output('@notice', msg: 'It seems like you have not updated the database for some time.')
|
||||
print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
|
||||
|
||||
/^y/i.match?(Readline.readline) ? true : false
|
||||
/^y/i.match?(Readline.readline)
|
||||
end
|
||||
|
||||
def update_db
|
||||
|
||||
@@ -18,9 +18,7 @@ module WPScan
|
||||
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
|
||||
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
|
||||
|
||||
return if target.content_dir
|
||||
|
||||
raise Error::WpContentDirNotDetected
|
||||
raise Error::WpContentDirNotDetected unless target.content_dir
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ module WPScan
|
||||
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
|
||||
OptChoice.new(
|
||||
['--plugins-detection MODE',
|
||||
'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
|
||||
'Use the supplied mode to enumerate Plugins.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
|
||||
),
|
||||
OptBoolean.new(
|
||||
@@ -62,8 +62,7 @@ module WPScan
|
||||
),
|
||||
OptChoice.new(
|
||||
['--plugins-version-detection MODE',
|
||||
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
|
||||
'or --plugins-detection modes.'],
|
||||
'Use the supplied mode to check plugins\' versions.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
||||
),
|
||||
OptInteger.new(
|
||||
@@ -171,6 +170,12 @@ module WPScan
|
||||
['--users-detection MODE',
|
||||
'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
|
||||
),
|
||||
OptRegexp.new(
|
||||
[
|
||||
'--exclude-usernames REGEXP_OR_STRING',
|
||||
'Exclude usernames matching the Regexp/string (case insensitive). Regexp delimiters are not required.'
|
||||
], options: Regexp::IGNORECASE
|
||||
)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -19,31 +19,37 @@ module WPScan
|
||||
OptChoice.new(['--password-attack ATTACK',
|
||||
'Force the supplied attack to be used rather than automatically determining one.'],
|
||||
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
|
||||
|
||||
def attack_opts
|
||||
@attack_opts ||= {
|
||||
show_progression: user_interaction?,
|
||||
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
||||
}
|
||||
end
|
||||
|
||||
def run
|
||||
return unless ParsedCli.passwords
|
||||
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
||||
end
|
||||
|
||||
attack_opts = {
|
||||
show_progression: user_interaction?,
|
||||
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
||||
}
|
||||
|
||||
begin
|
||||
found = []
|
||||
|
||||
attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user|
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
|
||||
end
|
||||
|
||||
attacker.attack(users, ParsedCli.passwords, attack_opts) do |user|
|
||||
found << user
|
||||
|
||||
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
||||
end
|
||||
rescue Error::NoLoginInterfaceDetected => e
|
||||
# TODO: Maybe output that in JSON as well.
|
||||
output('@notice', msg: e.to_s) if user_interaction?
|
||||
ensure
|
||||
output('users', users: found)
|
||||
end
|
||||
@@ -65,6 +71,8 @@ module WPScan
|
||||
|
||||
case ParsedCli.password_attack
|
||||
when :wp_login
|
||||
raise Error::NoLoginInterfaceDetected unless target.login_url
|
||||
|
||||
Finders::Passwords::WpLogin.new(target)
|
||||
when :xmlrpc
|
||||
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||
@@ -81,8 +89,8 @@ module WPScan
|
||||
def xmlrpc_get_users_blogs_enabled?
|
||||
if xmlrpc&.enabled? &&
|
||||
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
|
||||
xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
||||
.run.body !~ /XML\-RPC services are disabled/
|
||||
!xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
||||
.run.body.match?(/>\s*405\s*</)
|
||||
|
||||
true
|
||||
else
|
||||
@@ -100,8 +108,10 @@ module WPScan
|
||||
else
|
||||
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
end
|
||||
else
|
||||
elsif target.login_url
|
||||
Finders::Passwords::WpLogin.new(target)
|
||||
else
|
||||
raise Error::NoLoginInterfaceDetected
|
||||
end
|
||||
end
|
||||
|
||||
@@ -113,15 +123,6 @@ module WPScan
|
||||
acc << Model::User.new(elem.chomp)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] wordlist_path
|
||||
#
|
||||
# @return [ Array<String> ]
|
||||
def passwords(wordlist_path)
|
||||
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
|
||||
acc << elem.chomp
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,20 +4,25 @@ module WPScan
|
||||
module Controller
|
||||
# Controller to handle the API token
|
||||
class VulnApi < CMSScanner::Controller::Base
|
||||
ENV_KEY = 'WPSCAN_API_TOKEN'
|
||||
|
||||
def cli_options
|
||||
[
|
||||
OptString.new(['--api-token TOKEN', 'The WPVulnDB API Token to display vulnerability data'])
|
||||
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
|
||||
return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
|
||||
|
||||
DB::VulnApi.token = ParsedCli.api_token
|
||||
DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
|
||||
|
||||
api_status = DB::VulnApi.status
|
||||
|
||||
raise Error::InvalidApiToken if api_status['error']
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@ module WPScan
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE) TABLE|INSERT INTO/.freeze
|
||||
SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE|ALTER) (?:TABLE|DATABASE)|INSERT INTO/.freeze
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
@@ -39,18 +39,57 @@ module WPScan
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def potential_urls(opts = {})
|
||||
urls = {}
|
||||
domain_name = PublicSuffix.domain(target.uri.host)[/(^[\w|-]+)/, 1]
|
||||
urls = {}
|
||||
index = 0
|
||||
|
||||
File.open(opts[:list]).each_with_index do |path, index|
|
||||
path.gsub!('{domain_name}', domain_name)
|
||||
File.open(opts[:list]).each do |path|
|
||||
path.chomp!
|
||||
|
||||
urls[target.url(path.chomp)] = index
|
||||
if path.include?('{domain_name}')
|
||||
urls[target.url(path.gsub('{domain_name}', domain_name))] = index
|
||||
|
||||
if domain_name != domain_name_with_sub
|
||||
urls[target.url(path.gsub('{domain_name}', domain_name_with_sub))] = index + 1
|
||||
|
||||
index += 1
|
||||
end
|
||||
else
|
||||
urls[target.url(path)] = index
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
|
||||
def domain_name
|
||||
@domain_name ||= if Resolv::AddressRegex.match?(target.uri.host)
|
||||
target.uri.host
|
||||
else
|
||||
(PublicSuffix.domain(target.uri.host) || target.uri.host)[/(^[\w|-]+)/, 1]
|
||||
end
|
||||
end
|
||||
|
||||
def domain_name_with_sub
|
||||
@domain_name_with_sub ||=
|
||||
if Resolv::AddressRegex.match?(target.uri.host)
|
||||
target.uri.host
|
||||
else
|
||||
parsed = PublicSuffix.parse(target.uri.host)
|
||||
|
||||
if parsed.subdomain
|
||||
parsed.subdomain.gsub(".#{parsed.tld}", '')
|
||||
elsif parsed.domain
|
||||
parsed.domain.gsub(".#{parsed.tld}", '')
|
||||
else
|
||||
target.uri.host
|
||||
end
|
||||
end
|
||||
rescue PublicSuffix::DomainNotAllowed
|
||||
@domain_name_with_sub = target.uri.host
|
||||
end
|
||||
|
||||
def create_progress_bar(opts = {})
|
||||
super(opts.merge(title: ' Checking DB Exports -'))
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ require_relative 'interesting_findings/multisite'
|
||||
require_relative 'interesting_findings/debug_log'
|
||||
require_relative 'interesting_findings/backup_db'
|
||||
require_relative 'interesting_findings/mu_plugins'
|
||||
require_relative 'interesting_findings/php_disabled'
|
||||
require_relative 'interesting_findings/registration'
|
||||
require_relative 'interesting_findings/tmm_db_migrate'
|
||||
require_relative 'interesting_findings/upload_sql_dump'
|
||||
@@ -26,7 +27,7 @@ module WPScan
|
||||
%w[
|
||||
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
|
||||
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
|
||||
UploadSQLDump EmergencyPwdResetScript WPCron
|
||||
UploadSQLDump EmergencyPwdResetScript WPCron PHPDisabled
|
||||
].each do |f|
|
||||
finders << InterestingFindings.const_get(f).new(target)
|
||||
end
|
||||
|
||||
@@ -16,8 +16,7 @@ module WPScan
|
||||
target.url(path),
|
||||
confidence: 70,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: target.directory_listing_entries(path),
|
||||
references: { url: 'https://github.com/wpscanteam/wpscan/issues/422' }
|
||||
interesting_entries: target.directory_listing_entries(path)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,11 +11,7 @@ module WPScan
|
||||
|
||||
return unless target.debug_log?(path)
|
||||
|
||||
Model::DebugLog.new(
|
||||
target.url(path),
|
||||
confidence: 100, found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
|
||||
)
|
||||
Model::DebugLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,14 +9,9 @@ module WPScan
|
||||
def aggressive(_opts = {})
|
||||
path = 'installer-log.txt'
|
||||
|
||||
return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body)
|
||||
return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.match?(target.head_and_get(path).body)
|
||||
|
||||
Model::DuplicatorInstallerLog.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
||||
)
|
||||
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,10 +15,7 @@ module WPScan
|
||||
Model::EmergencyPwdResetScript.new(
|
||||
target.url(path),
|
||||
confidence: /password/i.match?(res.body) ? 100 : 40,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: {
|
||||
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
|
||||
}
|
||||
found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,8 +16,7 @@ module WPScan
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: fpd_entries,
|
||||
references: { url: 'https://www.owasp.org/index.php/Full_Path_Disclosure' }
|
||||
interesting_entries: fpd_entries
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,22 +7,16 @@ module WPScan
|
||||
class MuPlugins < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def passive(_opts = {})
|
||||
pattern = %r{#{target.content_dir}/mu\-plugins/}i
|
||||
pattern = %r{#{target.content_dir}/mu-plugins/}i
|
||||
|
||||
target.in_scope_uris(target.homepage_res) do |uri|
|
||||
target.in_scope_uris(target.homepage_res, '(//@href|//@src)[contains(., "mu-plugins")]') do |uri|
|
||||
next unless uri.path&.match?(pattern)
|
||||
|
||||
url = target.url('wp-content/mu-plugins/')
|
||||
|
||||
target.mu_plugins = true
|
||||
|
||||
return Model::MuPlugins.new(
|
||||
url,
|
||||
confidence: 70,
|
||||
found_by: 'URLs In Homepage (Passive Detection)',
|
||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
||||
)
|
||||
return Model::MuPlugins.new(url, confidence: 70, found_by: 'URLs In Homepage (Passive Detection)')
|
||||
end
|
||||
nil
|
||||
end
|
||||
@@ -37,13 +31,7 @@ module WPScan
|
||||
|
||||
target.mu_plugins = true
|
||||
|
||||
Model::MuPlugins.new(
|
||||
url,
|
||||
confidence: 80,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "This site has 'Must Use Plugins': #{url}",
|
||||
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
|
||||
)
|
||||
Model::MuPlugins.new(url, confidence: 80, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,18 +12,12 @@ module WPScan
|
||||
location = res.headers_hash['location']
|
||||
|
||||
return unless [200, 302].include?(res.code)
|
||||
return if res.code == 302 && location =~ /wp-login\.php\?action=register/
|
||||
return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
|
||||
return if res.code == 302 && location&.include?('wp-login.php?action=register')
|
||||
return unless res.code == 200 || (res.code == 302 && location&.include?('wp-signup.php'))
|
||||
|
||||
target.multisite = true
|
||||
|
||||
Model::Multisite.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: 'This site seems to be a multisite',
|
||||
references: { url: 'http://codex.wordpress.org/Glossary#Multisite' }
|
||||
)
|
||||
Model::Multisite.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
21
app/finders/interesting_findings/php_disabled.rb
Normal file
21
app/finders/interesting_findings/php_disabled.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# See https://github.com/wpscanteam/wpscan/issues/1593
|
||||
class PHPDisabled < CMSScanner::Finders::Finder
|
||||
PATTERN = /\$wp_version =/.freeze
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-includes/version.php'
|
||||
|
||||
return unless PATTERN.match?(target.head_and_get(path).body)
|
||||
|
||||
Model::PHPDisabled.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,12 +20,7 @@ module WPScan
|
||||
|
||||
target.registration_enabled = true
|
||||
|
||||
Model::Registration.new(
|
||||
res.effective_url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "Registration is enabled: #{res.effective_url}"
|
||||
)
|
||||
Model::Registration.new(res.effective_url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,12 +13,7 @@ module WPScan
|
||||
|
||||
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
||||
|
||||
Model::TmmDbMigrate.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { packetstorm: 131_957 }
|
||||
)
|
||||
Model::TmmDbMigrate.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,12 +13,7 @@ module WPScan
|
||||
|
||||
url = target.url(path)
|
||||
|
||||
Model::UploadDirectoryListing.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
to_s: "Upload directory has listing enabled: #{url}"
|
||||
)
|
||||
Model::UploadDirectoryListing.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,11 +14,7 @@ module WPScan
|
||||
|
||||
return unless SQL_PATTERN.match?(res.body)
|
||||
|
||||
Model::UploadSQLDump.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS
|
||||
)
|
||||
Model::UploadSQLDump.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,17 +11,7 @@ module WPScan
|
||||
|
||||
return unless res.code == 200
|
||||
|
||||
Model::WPCron.new(
|
||||
wp_cron_url,
|
||||
confidence: 60,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: {
|
||||
url: [
|
||||
'https://www.iplocation.net/defend-wordpress-from-ddos',
|
||||
'https://github.com/wpscanteam/wpscan/issues/1299'
|
||||
]
|
||||
}
|
||||
)
|
||||
Model::WPCron.new(wp_cron_url, confidence: 60, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
|
||||
def wp_cron_url
|
||||
|
||||
@@ -20,8 +20,8 @@ module WPScan
|
||||
end
|
||||
|
||||
def passive_from_css_href(res, opts)
|
||||
target.in_scope_uris(res, '//style/@src|//link/@href') do |uri|
|
||||
next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
|
||||
target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri|
|
||||
next unless uri.path =~ %r{/themes/([^/]+)/style.css\z}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], uri.to_s, opts)
|
||||
end
|
||||
@@ -33,7 +33,7 @@ module WPScan
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
|
||||
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'( ]*}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
def passive(opts = {})
|
||||
found = []
|
||||
|
||||
slugs = items_from_links('themes', false) + items_from_codes('themes', false)
|
||||
slugs = items_from_links('themes', uniq: false) + items_from_codes('themes', uniq: false)
|
||||
|
||||
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
||||
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 302 &&
|
||||
[*response.headers['Set-Cookie']]&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
||||
Array(response.headers['Set-Cookie'])&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
|
||||
@@ -12,11 +12,11 @@ module WPScan
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 200 && response.body =~ /blogName/
|
||||
response.code == 200 && response.body.include?('blogName')
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
response.code != 200 && response.body !~ /login_error/i
|
||||
response.code != 200 && response.body !~ /Incorrect username or password/i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,8 +22,30 @@ module WPScan
|
||||
target.multi_call(methods, cache_ttl: 0).run
|
||||
end
|
||||
|
||||
# @param [ IO ] file
|
||||
# @param [ Integer ] passwords_size
|
||||
# @return [ Array<String> ] The passwords from the last checked position in the file until there are
|
||||
# passwords_size passwords retrieved
|
||||
def passwords_from_wordlist(file, passwords_size)
|
||||
pwds = []
|
||||
added_pwds = 0
|
||||
|
||||
return pwds if passwords_size.zero?
|
||||
|
||||
# Make sure that the main code does not call #sysseek or #count etc
|
||||
# otherwise the file descriptor will be set to somwehere else
|
||||
file.each_line(chomp: true) do |line|
|
||||
pwds << line
|
||||
added_pwds += 1
|
||||
|
||||
break if added_pwds == passwords_size
|
||||
end
|
||||
|
||||
pwds
|
||||
end
|
||||
|
||||
# @param [ Array<Model::User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
# @param [ String ] wordlist_path
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
# @option opts [ Integer ] :multicall_max_passwords
|
||||
@@ -33,18 +55,22 @@ module WPScan
|
||||
# TODO: Make rubocop happy about metrics etc
|
||||
#
|
||||
# rubocop:disable all
|
||||
def attack(users, passwords, opts = {})
|
||||
wordlist_index = 0
|
||||
def attack(users, wordlist_path, opts = {})
|
||||
checked_passwords = 0
|
||||
wordlist = File.open(wordlist_path)
|
||||
wordlist_size = wordlist.count
|
||||
max_passwords = opts[:multicall_max_passwords]
|
||||
current_passwords_size = passwords_size(max_passwords, users.size)
|
||||
|
||||
create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
|
||||
create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,
|
||||
show_progression: opts[:show_progression])
|
||||
|
||||
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
|
||||
|
||||
loop do
|
||||
current_users = users.select { |user| user.password.nil? }
|
||||
current_passwords = passwords[wordlist_index, current_passwords_size]
|
||||
wordlist_index += current_passwords_size
|
||||
current_users = users.select { |user| user.password.nil? }
|
||||
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
|
||||
checked_passwords += current_passwords_size
|
||||
|
||||
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
||||
|
||||
@@ -76,16 +102,19 @@ module WPScan
|
||||
break
|
||||
end
|
||||
|
||||
progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
|
||||
begin
|
||||
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
|
||||
rescue ProgressBar::InvalidProgressError
|
||||
end
|
||||
end
|
||||
end
|
||||
# Maybe a progress_bar.stop ?
|
||||
end
|
||||
# rubocop:disable all
|
||||
# rubocop:enable all
|
||||
|
||||
def passwords_size(max_passwords, users_size)
|
||||
return 1 if max_passwords < users_size
|
||||
return 0 if users_size == 0
|
||||
return 0 if users_size.zero?
|
||||
|
||||
max_passwords / users_size
|
||||
end
|
||||
@@ -94,9 +123,13 @@ module WPScan
|
||||
def check_and_output_errors(res)
|
||||
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
|
||||
|
||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
|
||||
if /parse error. not well formed/i.match?(res.body)
|
||||
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
|
||||
end
|
||||
|
||||
progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
|
||||
return unless /requested method [^ ]+ does not exist/i.match?(res.body)
|
||||
|
||||
progress_bar.log('The requested method is not supported')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,7 +48,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String, nil ] The version number detected from the stable tag
|
||||
def from_stable_tag(body)
|
||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
|
||||
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i
|
||||
|
||||
number = Regexp.last_match[1]
|
||||
|
||||
@@ -59,20 +59,18 @@ module WPScan
|
||||
#
|
||||
# @return [ String, nil ] The best version number detected from the changelog section
|
||||
def from_changelog_section(body)
|
||||
extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
|
||||
extracted_versions = body.scan(%r{=+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[ \ta-z0-9().\-/]*=+}i)
|
||||
|
||||
return if extracted_versions.nil? || extracted_versions.empty?
|
||||
|
||||
extracted_versions.flatten!
|
||||
# must contain at least one number
|
||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||
extracted_versions = extracted_versions.grep(/[0-9]+/)
|
||||
|
||||
sorted = extracted_versions.sort do |x, y|
|
||||
begin
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue StandardError
|
||||
0
|
||||
end
|
||||
|
||||
sorted.last
|
||||
|
||||
@@ -19,8 +19,12 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
|
||||
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
|
||||
finding_opts = opts.merge(found_by: found_by,
|
||||
confidence: 80,
|
||||
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
|
||||
|
||||
found << Model::Plugin.new(slug, target, finding_opts)
|
||||
|
||||
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ module WPScan
|
||||
|
||||
# @return [ Version ]
|
||||
def style_version
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z.-]+)/i
|
||||
|
||||
Model::Version.new(
|
||||
Regexp.last_match[1],
|
||||
|
||||
@@ -19,8 +19,12 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
|
||||
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug|
|
||||
finding_opts = opts.merge(found_by: found_by,
|
||||
confidence: 80,
|
||||
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
|
||||
|
||||
found << Model::Theme.new(slug, target, finding_opts)
|
||||
|
||||
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||
end
|
||||
|
||||
@@ -6,10 +6,21 @@ require_relative 'users/oembed_api'
|
||||
require_relative 'users/rss_generator'
|
||||
require_relative 'users/author_id_brute_forcing'
|
||||
require_relative 'users/login_error_messages'
|
||||
require_relative 'users/yoast_seo_author_sitemap.rb'
|
||||
require_relative 'users/author_sitemap'
|
||||
require_relative 'users/yoast_seo_author_sitemap'
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
# Specific Finders container to filter the usernames found
|
||||
# and remove the ones matching ParsedCli.exclude_username if supplied
|
||||
class UsersFinders < SameTypeFinders
|
||||
def filter_findings
|
||||
findings.delete_if { |user| ParsedCli.exclude_usernames.match?(user.username) } if ParsedCli.exclude_usernames
|
||||
|
||||
findings
|
||||
end
|
||||
end
|
||||
|
||||
module Users
|
||||
# Users Finder
|
||||
class Base
|
||||
@@ -22,10 +33,15 @@ module WPScan
|
||||
Users::WpJsonApi.new(target) <<
|
||||
Users::OembedApi.new(target) <<
|
||||
Users::RSSGenerator.new(target) <<
|
||||
Users::AuthorSitemap.new(target) <<
|
||||
Users::YoastSeoAuthorSitemap.new(target) <<
|
||||
Users::AuthorIdBruteForcing.new(target) <<
|
||||
Users::LoginErrorMessages.new(target)
|
||||
end
|
||||
|
||||
def finders
|
||||
@finders ||= Finders::UsersFinders.new
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -71,11 +71,13 @@ module WPScan
|
||||
return username, 'Display Name', 50 if username
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ String, Addressable::URI ] uri
|
||||
#
|
||||
# @return [ String, nil ]
|
||||
def username_from_author_url(url)
|
||||
url[%r{/author/([^/\b]+)/?}i, 1]
|
||||
def username_from_author_url(uri)
|
||||
uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
|
||||
|
||||
uri.path[%r{/author/([^/\b]+)/?}i, 1]
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] res
|
||||
@@ -83,12 +85,12 @@ module WPScan
|
||||
# @return [ String, nil ] The username found
|
||||
def username_from_response(res)
|
||||
# Permalink enabled
|
||||
target.in_scope_uris(res, '//link/@href|//a/@href') do |uri|
|
||||
username = username_from_author_url(uri.to_s)
|
||||
target.in_scope_uris(res, '//@href[contains(., "author/")]') do |uri|
|
||||
username = username_from_author_url(uri)
|
||||
return username if username
|
||||
end
|
||||
|
||||
# No permalink
|
||||
# No permalink, TODO Maybe use xpath to extract the classes ?
|
||||
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
|
||||
end
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ module WPScan
|
||||
def potential_usernames(res)
|
||||
usernames = []
|
||||
|
||||
target.in_scope_uris(res, '//a/@href') do |uri, node|
|
||||
target.in_scope_uris(res, '//a/@href[contains(., "author")]') do |uri, node|
|
||||
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
||||
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
||||
elsif /author=[0-9]+/.match?(uri.query)
|
||||
|
||||
36
app/finders/users/author_sitemap.rb
Normal file
36
app/finders/users/author_sitemap.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Since WP 5.5, /wp-sitemap-users-1.xml is generated and contains
|
||||
# the usernames of accounts who made a post
|
||||
class AuthorSitemap < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
|
||||
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
||||
username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
|
||||
|
||||
next unless username && !username.strip.empty?
|
||||
|
||||
found << Model::User.new(username,
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [sitemap_url])
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ String ] The URL of the sitemap
|
||||
def sitemap_url
|
||||
@sitemap_url ||= target.url('wp-sitemap-users-1.xml')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -37,7 +37,7 @@ module WPScan
|
||||
# usernames from the potential Users found
|
||||
unames = opts[:found].map(&:username)
|
||||
|
||||
[*opts[:list]].each { |uname| unames << uname.chomp }
|
||||
Array(opts[:list]).each { |uname| unames << uname.chomp }
|
||||
|
||||
unames.uniq
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ module WPScan
|
||||
urls.each do |url|
|
||||
res = Browser.get_and_follow_location(url)
|
||||
|
||||
next unless res.code == 200 && res.body =~ /<dc\:creator>/i
|
||||
next unless res.code == 200 && res.body =~ /<dc:creator>/i
|
||||
|
||||
potential_usernames = []
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ module WPScan
|
||||
loop do
|
||||
current_page += 1
|
||||
|
||||
res = Typhoeus.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
|
||||
res = Browser.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
|
||||
|
||||
total_pages ||= res.headers['X-WP-TotalPages'].to_i
|
||||
|
||||
|
||||
@@ -5,27 +5,7 @@ module WPScan
|
||||
module Users
|
||||
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
|
||||
# See https://github.com/wpscanteam/wpscan/issues/1228
|
||||
class YoastSeoAuthorSitemap < CMSScanner::Finders::Finder
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Array<User> ]
|
||||
def aggressive(_opts = {})
|
||||
found = []
|
||||
|
||||
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
|
||||
username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
|
||||
|
||||
next unless username && !username.strip.empty?
|
||||
|
||||
found << Model::User.new(username,
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [sitemap_url])
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
class YoastSeoAuthorSitemap < AuthorSitemap
|
||||
# @return [ String ] The URL of the author-sitemap
|
||||
def sitemap_url
|
||||
@sitemap_url ||= target.url('author-sitemap.xml')
|
||||
|
||||
@@ -8,11 +8,15 @@ module WPScan
|
||||
# @param [ String ] type plugins / themes
|
||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||
#
|
||||
# @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
|
||||
def items_from_links(type, uniq = true)
|
||||
# @return [ Array<String> ] The plugins/themes detected in the href, src attributes of the page
|
||||
def items_from_links(type, uniq: true)
|
||||
found = []
|
||||
xpath = format(
|
||||
'(//@href|//@src|//@data-src)[contains(., "%s")]',
|
||||
type == 'plugins' ? target.plugins_dir : target.content_dir
|
||||
)
|
||||
|
||||
target.in_scope_uris(page_res) do |uri|
|
||||
target.in_scope_uris(page_res, xpath) do |uri|
|
||||
next unless uri.to_s =~ item_attribute_pattern(type)
|
||||
|
||||
slug = Regexp.last_match[1]&.strip
|
||||
@@ -27,7 +31,7 @@ module WPScan
|
||||
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
|
||||
#
|
||||
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
|
||||
def items_from_codes(type, uniq = true)
|
||||
def items_from_codes(type, uniq: true)
|
||||
found = []
|
||||
|
||||
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
|
||||
@@ -51,7 +55,7 @@ module WPScan
|
||||
#
|
||||
# @return [ Regexp ]
|
||||
def item_code_pattern(type)
|
||||
@item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
|
||||
@item_code_pattern ||= %r{["'( ]#{item_url_pattern(type)}([^\\/)"']+)}i
|
||||
end
|
||||
|
||||
# @param [ String ] type
|
||||
@@ -62,9 +66,9 @@ module WPScan
|
||||
item_url = type == 'plugins' ? target.plugins_url : target.content_url
|
||||
|
||||
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
|
||||
item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
||||
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
|
||||
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?/}i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ module WPScan
|
||||
module Finders
|
||||
# Specific Finders container to filter the version detected
|
||||
# and remove the one with low confidence to avoid false
|
||||
# positive when there is not enought information to accurately
|
||||
# positive when there is not enough information to accurately
|
||||
# determine it.
|
||||
class WpVersionFinders < UniqueFinders
|
||||
def filter_findings
|
||||
|
||||
@@ -28,7 +28,7 @@ module WPScan
|
||||
end
|
||||
|
||||
def passive_urls_xpath
|
||||
'//a[contains(@href, "rdf")]/@href'
|
||||
'//a[contains(@href, "/rdf")]/@href'
|
||||
end
|
||||
|
||||
def aggressive_urls(_opts = {})
|
||||
|
||||
@@ -7,46 +7,144 @@ module WPScan
|
||||
include References
|
||||
end
|
||||
|
||||
#
|
||||
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
|
||||
#
|
||||
class BackupDB < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "A backup directory has been found: #{url}"
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['https://github.com/wpscanteam/wpscan/issues/422'] }
|
||||
end
|
||||
end
|
||||
|
||||
class DebugLog < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "Debug Log found: #{url}"
|
||||
end
|
||||
|
||||
# @ return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['https://codex.wordpress.org/Debugging_in_WordPress'] }
|
||||
end
|
||||
end
|
||||
|
||||
class DuplicatorInstallerLog < InterestingFinding
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['https://www.exploit-db.com/ghdb/3981/'] }
|
||||
end
|
||||
end
|
||||
|
||||
class EmergencyPwdResetScript < InterestingFinding
|
||||
def references
|
||||
@references ||= {
|
||||
url: ['https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script']
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class FullPathDisclosure < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "Full Path Disclosure found: #{url}"
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['https://www.owasp.org/index.php/Full_Path_Disclosure'] }
|
||||
end
|
||||
end
|
||||
|
||||
class MuPlugins < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= "This site has 'Must Use Plugins': #{url}"
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['http://codex.wordpress.org/Must_Use_Plugins'] }
|
||||
end
|
||||
end
|
||||
|
||||
class Multisite < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= 'This site seems to be a multisite'
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { url: ['http://codex.wordpress.org/Glossary#Multisite'] }
|
||||
end
|
||||
end
|
||||
|
||||
class Readme < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "WordPress readme found: #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
class Registration < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= "Registration is enabled: #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
class TmmDbMigrate < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "ThemeMakers migration file found: #{url}"
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= { packetstorm: [131_957] }
|
||||
end
|
||||
end
|
||||
|
||||
class UploadDirectoryListing < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= "Upload directory has listing enabled: #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
class UploadSQLDump < InterestingFinding
|
||||
def to_s
|
||||
@to_s ||= "SQL Dump found: #{url}"
|
||||
end
|
||||
end
|
||||
|
||||
class WPCron < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= "The external WP-Cron seems to be enabled: #{url}"
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= {
|
||||
url: [
|
||||
'https://www.iplocation.net/defend-wordpress-from-ddos',
|
||||
'https://github.com/wpscanteam/wpscan/issues/1299'
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
class PHPDisabled < InterestingFinding
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
@to_s ||= 'PHP seems to be disabled'
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
@references ||= {
|
||||
url: ['https://github.com/wpscanteam/wpscan/issues/1593']
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,7 +38,7 @@ module WPScan
|
||||
|
||||
# @return [ Array<String> ]
|
||||
def potential_readme_filenames
|
||||
@potential_readme_filenames ||= [*(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)]
|
||||
@potential_readme_filenames ||= Array((DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ module WPScan
|
||||
# @return [ Theme ]
|
||||
def parent_theme
|
||||
return unless template
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"')]+)["']?\);\s*$/i
|
||||
|
||||
opts = detection_opts.merge(
|
||||
style_url: url(Regexp.last_match[1]),
|
||||
@@ -101,7 +101,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String ]
|
||||
def parse_style_tag(body, tag)
|
||||
value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n\*]+)/i, 1]
|
||||
value = body[/\b#{Regexp.escape(tag)}:[\t ]*([^\r\n*]+)/, 1]
|
||||
|
||||
value && !value.strip.empty? ? value.strip : nil
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@ module WPScan
|
||||
def vulnerabilities
|
||||
vulns = []
|
||||
|
||||
vulns << rce_webshot_vuln if version == false || version > '1.35' && version < '2.8.14' && webshot_enabled?
|
||||
vulns << rce_webshot_vuln if version == false || (version > '1.35' && version < '2.8.14' && webshot_enabled?)
|
||||
vulns << rce_132_vuln if version == false || version < '1.33'
|
||||
|
||||
vulns
|
||||
@@ -40,9 +40,9 @@ module WPScan
|
||||
def rce_132_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 1.32 Remote Code Execution',
|
||||
{ exploitdb: ['17602'] },
|
||||
'RCE',
|
||||
'1.33'
|
||||
references: { exploitdb: ['17602'] },
|
||||
type: 'RCE',
|
||||
fixed_in: '1.33'
|
||||
)
|
||||
end
|
||||
|
||||
@@ -50,12 +50,12 @@ module WPScan
|
||||
def rce_webshot_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||
{
|
||||
references: {
|
||||
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
|
||||
cve: '2014-4663'
|
||||
},
|
||||
'RCE',
|
||||
'2.8.14'
|
||||
type: 'RCE',
|
||||
fixed_in: '2.8.14'
|
||||
)
|
||||
end
|
||||
|
||||
@@ -63,7 +63,7 @@ module WPScan
|
||||
def webshot_enabled?
|
||||
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
|
||||
|
||||
/WEBSHOT_ENABLED == true/.match?(res.body) ? false : true
|
||||
!/WEBSHOT_ENABLED == true/.match?(res.body)
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
||||
|
||||
@@ -23,7 +23,7 @@ module WPScan
|
||||
# @option opts [ Hash ] :version_detection The options to use when looking for the version
|
||||
# @option opts [ String ] :url The URL of the item
|
||||
def initialize(slug, blog, opts = {})
|
||||
@slug = URI.decode(slug)
|
||||
@slug = Addressable::URI.unencode(slug)
|
||||
@blog = blog
|
||||
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
||||
|
||||
@@ -39,7 +39,7 @@ module WPScan
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
Array(db_data['vulnerabilities']).each do |json_vuln|
|
||||
vulnerability = Vulnerability.load_from_json(json_vuln)
|
||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||
end
|
||||
@@ -53,7 +53,9 @@ module WPScan
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def vulnerable_to?(vuln)
|
||||
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
|
||||
return false if version && vuln&.introduced_in && version < vuln.introduced_in
|
||||
|
||||
return true unless version && vuln&.fixed_in && !vuln.fixed_in.empty?
|
||||
|
||||
version < vuln.fixed_in
|
||||
end
|
||||
@@ -83,11 +85,6 @@ module WPScan
|
||||
end
|
||||
end
|
||||
|
||||
# URI.encode is preferered over Addressable::URI.encode as it will encode
|
||||
# leading # character:
|
||||
# URI.encode('#t#') => %23t%23
|
||||
# Addressable::URI.encode('#t#') => #t%23
|
||||
#
|
||||
# @param [ String ] path Optional path to merge with the uri
|
||||
#
|
||||
# @return [ String ]
|
||||
@@ -95,7 +92,7 @@ module WPScan
|
||||
return unless @uri
|
||||
return @uri.to_s unless path
|
||||
|
||||
@uri.join(URI.encode(path)).to_s
|
||||
@uri.join(Addressable::URI.encode(path)).to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
@@ -165,8 +162,8 @@ module WPScan
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def head_and_get(path, codes = [200], params = {})
|
||||
final_path = +@path_from_blog
|
||||
final_path << URI.encode(path) unless path.nil?
|
||||
final_path = @path_from_blog.dup # @path_from_blog is set in the plugin/theme
|
||||
final_path << path unless path.nil?
|
||||
|
||||
blog.head_and_get(final_path, codes, params)
|
||||
end
|
||||
|
||||
@@ -53,7 +53,7 @@ module WPScan
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
Array(db_data['vulnerabilities']).each do |json_vuln|
|
||||
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
||||
end
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ module WPScan
|
||||
|
||||
# @return [ Hash ]
|
||||
def references
|
||||
{
|
||||
@references ||= {
|
||||
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
|
||||
metasploit: [
|
||||
'auxiliary/scanner/http/wordpress_ghost_scanner',
|
||||
|
||||
@@ -9,6 +9,6 @@ _______________________________________________________________
|
||||
WordPress Security Scanner by the WPScan Team
|
||||
Version <%= WPScan::VERSION %>
|
||||
<%= ' ' * ((63 - WPScan::DB::Sponsor.text.length)/2) + WPScan::DB::Sponsor.text %>
|
||||
@_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
|
||||
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart
|
||||
_______________________________________________________________
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<% if @users.empty? -%>
|
||||
<%= notice_icon %> No Valid Passwords Found.
|
||||
<% else -%>
|
||||
<%= notice_icon %> Valid Combinations Found:
|
||||
<%= critical_icon %> Valid Combinations Found:
|
||||
<% @users.each do |user| -%>
|
||||
| Username: <%= user.username %>, Password: <%= user.password %>
|
||||
<% end -%>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<% unless @status.empty? -%>
|
||||
<% if @status['http_error'] -%>
|
||||
<%= critical_icon %> WPVulnDB API, <%= @status['http_error'].to_s %>
|
||||
<%= critical_icon %> WPScan DB API, <%= @status['http_error'].to_s %>
|
||||
<% else -%>
|
||||
<%= info_icon %> WPVulnDB API OK
|
||||
<%= 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 WPVulnDB API Token given, as a result vulnerability data has not been output.
|
||||
<%= warning_icon %> You can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up.
|
||||
<%= warning_icon %> No WPScan API Token given, as a result vulnerability data has not been output.
|
||||
<%= warning_icon %> You can get a free API token with 25 daily requests by registering at https://wpscan.com/register
|
||||
<% end -%>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
| <%= critical_icon %> Title: <%= @v.title %>
|
||||
<% if @v.cvss -%>
|
||||
| CVSS: <%= @v.cvss[:score] %> (<%= @v.cvss[:vector] %>)
|
||||
<% end -%>
|
||||
<% if @v.fixed_in -%>
|
||||
| Fixed in: <%= @v.fixed_in %>
|
||||
<% end -%>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if @version -%>
|
||||
<%= info_icon %> WordPress version <%= @version.number %> identified (<%= @version.status.capitalize %>, released on <%= @version.release_date %>).
|
||||
<%= info_icon %> WordPress version <%= @version.number %> identified (<%= @version.status.tr('-', '_').humanize %>, released on <%= @version.release_date %>).
|
||||
<%= render('@finding', item: @version) -%>
|
||||
<% else -%>
|
||||
<%= notice_icon %> The WordPress version could not be detected.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"@_WPScan_",
|
||||
"@ethicalhack3r",
|
||||
"@erwan_lr",
|
||||
"@_FireFart_"
|
||||
"@firefart"
|
||||
],
|
||||
"sponsor": <%= WPScan::DB::Sponsor.text.to_json %>
|
||||
},
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<% vulns.each_with_index do |v, index| -%>
|
||||
{
|
||||
"title": <%= v.title.to_json %>,
|
||||
<% if v.cvss -%>
|
||||
"cvss": <%= v.cvss.to_json %>,
|
||||
<% end -%>
|
||||
"fixed_in": <%= v.fixed_in.to_json %>,
|
||||
"references": <%= v.references.to_json %>
|
||||
}<% unless index == last_index -%>,<% end -%>
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"requests_remaining": <%= @status['requests_remaining'].to_json %>
|
||||
<% end -%>
|
||||
<% else -%>
|
||||
"error": "No WPVulnDB API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 50 daily requests by registering at https://wpvulndb.com/users/sign_up."
|
||||
"error": "No WPScan API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 25 daily requests by registering at https://wpscan.com/register"
|
||||
<% end -%>
|
||||
},
|
||||
@@ -13,6 +13,7 @@ require 'uri'
|
||||
require 'time'
|
||||
require 'readline'
|
||||
require 'securerandom'
|
||||
require 'resolv'
|
||||
# Monkey Patches/Fixes/Override
|
||||
require 'wpscan/typhoeus/response' # Adds a from_vuln_api? method
|
||||
# Custom Libs
|
||||
|
||||
@@ -7,7 +7,7 @@ module WPScan
|
||||
|
||||
# @return [ String ]
|
||||
def default_user_agent
|
||||
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.org/)"
|
||||
@default_user_agent ||= "WPScan v#{VERSION} (https://wpscan.com/wordpress-security-scanner)"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,11 @@ module WPScan
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.all_df_data
|
||||
@all_df_data ||= YAML.safe_load(File.read(df_file), [Regexp])
|
||||
@all_df_data ||= if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('4.0.0')
|
||||
YAML.safe_load(File.read(df_file), permitted_classes: [Regexp])
|
||||
else
|
||||
YAML.safe_load(File.read(df_file), [Regexp])
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Array<Symbol> ]
|
||||
@@ -31,7 +35,7 @@ module WPScan
|
||||
|
||||
finder_configs(
|
||||
finder_class,
|
||||
Regexp.last_match[1] == 'aggressive'
|
||||
aggressive: Regexp.last_match[1] == 'aggressive'
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ module WPScan
|
||||
# @param [ Symbol ] finder_class
|
||||
# @param [ Boolean ] aggressive
|
||||
# @return [ Hash ]
|
||||
def self.finder_configs(finder_class, aggressive = false)
|
||||
def self.finder_configs(finder_class, aggressive: false)
|
||||
configs = {}
|
||||
|
||||
return configs unless allowed_classes.include?(finder_class)
|
||||
|
||||
@@ -24,7 +24,7 @@ module WPScan
|
||||
# @param [ Symbol ] finder_class
|
||||
# @param [ Boolean ] aggressive
|
||||
# @return [ Hash ]
|
||||
def self.finder_configs(finder_class, aggressive = false)
|
||||
def self.finder_configs(finder_class, aggressive: false)
|
||||
configs = {}
|
||||
|
||||
return configs unless allowed_classes.include?(finder_class)
|
||||
|
||||
@@ -24,7 +24,13 @@ module WPScan
|
||||
|
||||
FileUtils.mkdir_p(repo_directory.to_s) unless Dir.exist?(repo_directory.to_s)
|
||||
|
||||
raise "#{repo_directory} is not writable" unless repo_directory.writable?
|
||||
# When --no-update is passed, return to avoid raising an error if the directory is not writable
|
||||
# Mainly there for Homebrew: https://github.com/wpscanteam/wpscan/pull/1455
|
||||
return if ParsedCli.update == false
|
||||
|
||||
unless repo_directory.writable?
|
||||
raise "#{repo_directory} is not writable (uid: #{Process.uid}, gid: #{Process.gid})"
|
||||
end
|
||||
|
||||
delete_old_files
|
||||
end
|
||||
@@ -67,13 +73,13 @@ module WPScan
|
||||
# @return [ Hash ] The params for Typhoeus::Request
|
||||
# @note Those params can't be overriden by CLI options
|
||||
def request_params
|
||||
@request_params ||= {
|
||||
@request_params ||= Browser.instance.default_connect_request_params.merge(
|
||||
timeout: 600,
|
||||
connecttimeout: 300,
|
||||
accept_encoding: 'gzip, deflate',
|
||||
cache_ttl: 0,
|
||||
headers: { 'User-Agent' => Browser.instance.default_user_agent, 'Referer' => nil }
|
||||
}
|
||||
headers: { 'User-Agent' => Browser.instance.default_user_agent }
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ String ] The raw file URL associated with the given filename
|
||||
@@ -85,7 +91,7 @@ module WPScan
|
||||
def remote_file_checksum(filename)
|
||||
url = "#{remote_file_url(filename)}.sha512"
|
||||
|
||||
res = Browser.get(url, request_params)
|
||||
res = Typhoeus.get(url, request_params)
|
||||
raise Error::Download, res if res.timed_out? || res.code != 200
|
||||
|
||||
res.body.chomp
|
||||
@@ -126,7 +132,7 @@ module WPScan
|
||||
file_path = local_file_path(filename)
|
||||
file_url = remote_file_url(filename)
|
||||
|
||||
res = Browser.get(file_url, request_params)
|
||||
res = Typhoeus.get(file_url, request_params)
|
||||
raise Error::Download, res if res.timed_out? || res.code != 200
|
||||
|
||||
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
||||
@@ -139,24 +145,22 @@ module WPScan
|
||||
updated = []
|
||||
|
||||
FILES.each do |filename|
|
||||
begin
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
|
||||
# Checking if the file needs to be updated
|
||||
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
# Checking if the file needs to be updated
|
||||
next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
|
||||
create_backup(filename)
|
||||
dl_checksum = download(filename)
|
||||
create_backup(filename)
|
||||
dl_checksum = download(filename)
|
||||
|
||||
raise "#{filename}: checksums do not match" unless dl_checksum == db_checksum
|
||||
raise Error::ChecksumsMismatch, filename unless dl_checksum == db_checksum
|
||||
|
||||
updated << filename
|
||||
rescue StandardError => e
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
||||
end
|
||||
updated << filename
|
||||
rescue StandardError => e
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
delete_backup(filename) if File.exist?(backup_file_path(filename))
|
||||
end
|
||||
|
||||
File.write(last_update_file, Time.now)
|
||||
|
||||
@@ -4,7 +4,7 @@ module WPScan
|
||||
module DB
|
||||
# WPVulnDB API
|
||||
class VulnApi
|
||||
NON_ERROR_CODES = [200, 401].freeze
|
||||
NON_ERROR_CODES = [200, 403].freeze
|
||||
|
||||
class << self
|
||||
attr_accessor :token
|
||||
@@ -12,7 +12,7 @@ module WPScan
|
||||
|
||||
# @return [ Addressable::URI ]
|
||||
def self.uri
|
||||
@uri ||= Addressable::URI.parse('https://wpvulndb.com/api/v3/')
|
||||
@uri ||= Addressable::URI.parse('https://wpscan.com/api/v3/')
|
||||
end
|
||||
|
||||
# @param [ String ] path
|
||||
@@ -21,10 +21,12 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def self.get(path, params = {})
|
||||
return {} unless token
|
||||
return {} if path.end_with?('/latest') # Remove this when api/v4 is up
|
||||
|
||||
res = Browser.get(uri.join(path), params.merge(request_params))
|
||||
# Typhoeus.get is used rather than Browser.get to avoid merging irrelevant params from the CLI
|
||||
res = Typhoeus.get(uri.join(path), default_request_params.merge(params))
|
||||
|
||||
return {} if res.code == 404 # This is for API inconsistencies when dots in path
|
||||
return {} if res.code == 404 || res.code == 429
|
||||
return JSON.parse(res.body) if NON_ERROR_CODES.include?(res.code)
|
||||
|
||||
raise Error::HTTP, res
|
||||
@@ -32,6 +34,8 @@ module WPScan
|
||||
retries ||= 0
|
||||
|
||||
if (retries += 1) <= 3
|
||||
@default_request_params[:headers]['X-Retry'] = retries
|
||||
|
||||
sleep(1)
|
||||
retry
|
||||
end
|
||||
@@ -64,15 +68,14 @@ module WPScan
|
||||
end
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.request_params
|
||||
{
|
||||
# @note Those params can not be overriden by CLI options
|
||||
def self.default_request_params
|
||||
@default_request_params ||= Browser.instance.default_connect_request_params.merge(
|
||||
headers: {
|
||||
'Host' => uri.host, # Reset in case user provided a --vhost for the target
|
||||
'Referer' => nil, # Removes referer set by the cmsscanner to the target url
|
||||
'User-Agent' => Browser.instance.default_user_agent,
|
||||
'Authorization' => "Token token=#{token}"
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,16 +5,16 @@ module WPScan
|
||||
class PluginsThresholdReached < Standard
|
||||
def to_s
|
||||
"The number of plugins detected reached the threshold of #{ParsedCli.plugins_threshold} " \
|
||||
'which might indicate False Positive. It would be recommended to use the --exclude-content-based ' \
|
||||
'option to ignore the bad responses.'
|
||||
'which might indicate False Positive. It would be recommended to use the --exclude-content-based ' \
|
||||
'option to ignore the bad responses.'
|
||||
end
|
||||
end
|
||||
|
||||
class ThemesThresholdReached < Standard
|
||||
def to_s
|
||||
"The number of themes detected reached the threshold of #{ParsedCli.themes_threshold} " \
|
||||
'which might indicate False Positive. It would be recommended to use the --exclude-content-based ' \
|
||||
'option to ignore the bad responses.'
|
||||
'which might indicate False Positive. It would be recommended to use the --exclude-content-based ' \
|
||||
'option to ignore the bad responses.'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,5 +8,17 @@ module WPScan
|
||||
'Update required, you can not run a scan if a database file is missing.'
|
||||
end
|
||||
end
|
||||
|
||||
class ChecksumsMismatch < Standard
|
||||
attr_reader :db_file
|
||||
|
||||
def initialize(db_file)
|
||||
@db_file = db_file
|
||||
end
|
||||
|
||||
def to_s
|
||||
"#{db_file}: checksums do not match. Please try again in a few minutes."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -26,7 +26,13 @@ module WPScan
|
||||
class WpContentDirNotDetected < Standard
|
||||
def to_s
|
||||
'Unable to identify the wp-content dir, please supply it with --wp-content-dir,' \
|
||||
' use the --scope option or make sure the --url value given is the correct one'
|
||||
' use the --scope option or make sure the --url value given is the correct one'
|
||||
end
|
||||
end
|
||||
|
||||
class NoLoginInterfaceDetected < Standard
|
||||
def to_s
|
||||
'Could not find a login interface to perform the password attack against'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ module WPScan
|
||||
end
|
||||
|
||||
# Needed to have inheritance of the @child_class_constants
|
||||
# If inheritance is not needed, then the #child_class_constant can be used in the classe definition, ie
|
||||
# If inheritance is not needed, then the #child_class_constant can be used in the class definition, ie
|
||||
# child_class_constant :FILES, PATTERN: /aaa/i
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@@ -56,8 +56,8 @@ module WPScan
|
||||
|
||||
homepage_result = find(target.homepage_res, opts)
|
||||
|
||||
if homepage_result
|
||||
return homepage_result unless homepage_result.is_a?(Array) && homepage_result.empty?
|
||||
unless homepage_result.nil? || (homepage_result.is_a?(Array) && homepage_result&.empty?)
|
||||
return homepage_result
|
||||
end
|
||||
|
||||
find(target.error_404_res, opts)
|
||||
|
||||
@@ -11,7 +11,7 @@ module WPScan
|
||||
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super.merge(
|
||||
PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 70
|
||||
PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[.\d]+)/, CONFIDENCE: 70
|
||||
)
|
||||
end
|
||||
|
||||
@@ -21,13 +21,11 @@ module WPScan
|
||||
parsers = ALLOWED_PARSERS.include?(self.class::PARSER) ? [self.class::PARSER] : ALLOWED_PARSERS
|
||||
|
||||
parsers.each do |parser|
|
||||
begin
|
||||
parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
|
||||
parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
|
||||
|
||||
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
|
||||
rescue StandardError
|
||||
next
|
||||
end
|
||||
|
||||
nil # Make sure nil is returned in case none of the parsers managed to parse the body correctly
|
||||
|
||||
@@ -9,7 +9,7 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)\=(?<v>\d+\.[\.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
|
||||
XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)=(?<v>\d+\.[.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ module WPScan
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(
|
||||
XPATH: nil, PATTERN: /\A(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 60
|
||||
XPATH: nil, PATTERN: /\A(?<v>\d+\.[.\d]+)/, CONFIDENCE: 60
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -33,10 +33,12 @@ module WPScan
|
||||
|
||||
# @return [ Hash ]
|
||||
def self.child_class_constants
|
||||
@child_class_constants ||= super().merge(PATTERN: /ver\=(?<v>\d+\.[\.\d]+)/i)
|
||||
@child_class_constants ||= super().merge(PATTERN: /ver=(?<v>\d+\.[.\d]+)/i)
|
||||
end
|
||||
end
|
||||
|
||||
# This one has been disabled from the DF.yml as it was causing FPs when a plugin had numerous
|
||||
# files matching a known WP version.
|
||||
class WpItemQueryParameter < QueryParameter
|
||||
def xpath
|
||||
@xpath ||=
|
||||
|
||||
@@ -13,7 +13,7 @@ end
|
||||
#
|
||||
# @return [ Symbol ]
|
||||
def classify_slug(slug)
|
||||
classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/\-{1,}/, '_').camelize.to_s
|
||||
classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/-{1,}/, '_').camelize.to_s
|
||||
classified = "D_#{classified}" if /\d/.match?(classified[0])
|
||||
|
||||
classified.to_sym
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
module WPScan
|
||||
# References module (which should be included along with the CMSScanner::References)
|
||||
# to allow the use of the wpvulndb reference
|
||||
# to allow the use of the wpvulndb reference.
|
||||
module References
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@@ -27,7 +27,7 @@ module WPScan
|
||||
end
|
||||
|
||||
def wpvulndb_url(id)
|
||||
"https://wpvulndb.com/vulnerabilities/#{id}"
|
||||
"https://wpscan.com/vulnerability/#{id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,13 +19,13 @@ module WPScan
|
||||
# @return [ Boolean ]
|
||||
def vulnerable?
|
||||
[@wp_version, @main_theme, @plugins, @themes, @timthumbs].each do |e|
|
||||
[*e].each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
|
||||
Array(e).each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
|
||||
end
|
||||
|
||||
return true unless [*@config_backups].empty?
|
||||
return true unless [*@db_exports].empty?
|
||||
return true unless Array(@config_backups).empty?
|
||||
return true unless Array(@db_exports).empty?
|
||||
|
||||
[*@users].each { |u| return true if u.password }
|
||||
Array(@users).each { |u| return true if u.password }
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
@@ -11,9 +11,10 @@ module WPScan
|
||||
module WordPress
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
|
||||
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu\-)?plugins|uploads))|wp-includes)/}i.freeze
|
||||
WP_JSON_OEMBED_PATTERN = %r{/wp\-json/oembed/}i.freeze
|
||||
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp\-admin\\?/admin\-ajax\.php}i.freeze
|
||||
WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i.freeze
|
||||
WORDPRESS_HOSTED_PATTERN = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
|
||||
WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i.freeze
|
||||
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i.freeze
|
||||
|
||||
# These methods are used in the associated interesting_findings finders
|
||||
# to keep the boolean state of the finding rather than re-check the whole thing again
|
||||
@@ -32,8 +33,12 @@ module WPScan
|
||||
|
||||
if %i[mixed aggressive].include?(detection_mode)
|
||||
%w[wp-admin/install.php wp-login.php].each do |path|
|
||||
return true if in_scope_uris(Browser.get_and_follow_location(url(path))).any? do |uri|
|
||||
WORDPRESS_PATTERN.match?(uri.path)
|
||||
res = Browser.get_and_follow_location(url(path))
|
||||
|
||||
next unless res.code == 200
|
||||
|
||||
in_scope_uris(res, '//link/@href|//script/@src') do |uri|
|
||||
return true if WORDPRESS_PATTERN.match?(uri.path)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -44,7 +49,7 @@ module WPScan
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @return [ Boolean ]
|
||||
def wordpress_from_meta_comments_or_scripts?(response)
|
||||
in_scope_uris(response) do |uri|
|
||||
in_scope_uris(response, '//link/@href|//script/@src') do |uri|
|
||||
return true if WORDPRESS_PATTERN.match?(uri.path) || WP_JSON_OEMBED_PATTERN.match?(uri.path)
|
||||
end
|
||||
|
||||
@@ -99,10 +104,8 @@ module WPScan
|
||||
return true if /\.wordpress\.com$/i.match?(uri.host)
|
||||
|
||||
unless content_dir
|
||||
pattern = %r{https?://s\d\.wp\.com#{WORDPRESS_PATTERN}}i.freeze
|
||||
|
||||
uris_from_page(homepage_res) do |uri|
|
||||
return true if uri.to_s.match?(pattern)
|
||||
uris_from_page(homepage_res, '(//@href|//@src)[contains(., "wp.com")]') do |uri|
|
||||
return true if uri.to_s.match?(WORDPRESS_HOSTED_PATTERN)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -134,15 +137,19 @@ module WPScan
|
||||
# the first time the method is called, and the effective_url is then used
|
||||
# if suitable, otherwise the default wp-login will be.
|
||||
#
|
||||
# @return [ String ] The URL to the login page
|
||||
# If the login_uri CLI option has been provided, it will be returne w/o redirection check.
|
||||
#
|
||||
# @return [ String, false ] The URL to the login page or false if not detected
|
||||
def login_url
|
||||
return @login_url if @login_url
|
||||
return @login_url unless @login_url.nil?
|
||||
return @login_url = url(ParsedCli.login_uri) if ParsedCli.login_uri
|
||||
|
||||
@login_url = url('wp-login.php')
|
||||
|
||||
res = Browser.get_and_follow_location(@login_url)
|
||||
|
||||
@login_url = res.effective_url if res.effective_url =~ /wp\-login\.php\z/i && in_scope?(res.effective_url)
|
||||
@login_url = res.effective_url if res.effective_url =~ /wp-login\.php\z/i && in_scope?(res.effective_url)
|
||||
@login_url = false if res.code == 404
|
||||
|
||||
@login_url
|
||||
end
|
||||
|
||||
@@ -71,7 +71,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String ]
|
||||
def plugin_url(slug)
|
||||
plugins_uri.join("#{URI.encode(slug)}/").to_s
|
||||
plugins_uri.join("#{Addressable::URI.encode(slug)}/").to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
@@ -93,7 +93,7 @@ module WPScan
|
||||
#
|
||||
# @return [ String ]
|
||||
def theme_url(slug)
|
||||
themes_uri.join("#{URI.encode(slug)}/").to_s
|
||||
themes_uri.join("#{Addressable::URI.encode(slug)}/").to_s
|
||||
end
|
||||
|
||||
# @return [ String, False ] String of the sub_dir found, false otherwise
|
||||
@@ -104,10 +104,11 @@ module WPScan
|
||||
return @sub_dir unless @sub_dir.nil?
|
||||
|
||||
# url_pattern is from CMSScanner::Target
|
||||
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i
|
||||
pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp-includes/)}i
|
||||
xpath = '(//@src|//@href|//@data-src)[contains(., "xmlrpc.php") or contains(., "wp-includes/")]'
|
||||
|
||||
[homepage_res, error_404_res].each do |page_res|
|
||||
in_scope_uris(page_res) do |uri|
|
||||
in_scope_uris(page_res, xpath) do |uri|
|
||||
return @sub_dir = Regexp.last_match[1] if uri.to_s.match(pattern)
|
||||
end
|
||||
end
|
||||
@@ -123,15 +124,15 @@ module WPScan
|
||||
def url(path = nil)
|
||||
return @uri.to_s unless path
|
||||
|
||||
if %r{wp\-content/plugins}i.match?(path)
|
||||
path = +path.gsub('wp-content/plugins', plugins_dir)
|
||||
elsif /wp\-content/i.match?(path)
|
||||
path = +path.gsub('wp-content', content_dir)
|
||||
if %r{wp-content/plugins}i.match?(path)
|
||||
new_path = path.gsub('wp-content/plugins', plugins_dir)
|
||||
elsif /wp-content/i.match?(path)
|
||||
new_path = path.gsub('wp-content', content_dir)
|
||||
elsif path[0] != '/' && sub_dir
|
||||
path = "#{sub_dir}/#{path}"
|
||||
new_path = "#{sub_dir}/#{path}"
|
||||
end
|
||||
|
||||
super(path)
|
||||
super(new_path || path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ module Typhoeus
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def from_vuln_api?
|
||||
effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) && !effective_url.include?('/status')
|
||||
effective_url.start_with?(WPScan::DB::VulnApi.uri.to_s) &&
|
||||
!effective_url.start_with?(WPScan::DB::VulnApi.uri.join('status').to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
|
||||
# Version
|
||||
module WPScan
|
||||
VERSION = '3.7.5'
|
||||
VERSION = '3.8.20'
|
||||
end
|
||||
|
||||
@@ -18,9 +18,11 @@ module WPScan
|
||||
|
||||
new(
|
||||
json_data['title'],
|
||||
references,
|
||||
json_data['vuln_type'],
|
||||
json_data['fixed_in']
|
||||
references: references,
|
||||
type: json_data['vuln_type'],
|
||||
fixed_in: json_data['fixed_in'],
|
||||
introduced_in: json_data['introduced_in'],
|
||||
cvss: json_data['cvss']&.symbolize_keys
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,9 +52,10 @@ describe WPScan::Controller::Core do
|
||||
%i[apache iis nginx].each do |server|
|
||||
context "when #{server}" do
|
||||
let(:cli_args) { "#{super()} --server #{server}" }
|
||||
let(:servers) { [:Apache, nil, :IIS, :Nginx] }
|
||||
|
||||
it "loads the #{server.capitalize} module and returns :#{server}" do
|
||||
@stubbed_server = [:Apache, nil, :IIS, :Nginx].sample
|
||||
@stubbed_server = servers.sample
|
||||
@expected = server == :iis ? :IIS : server.to_s.camelize.to_sym
|
||||
end
|
||||
end
|
||||
@@ -70,7 +71,7 @@ describe WPScan::Controller::Core do
|
||||
let(:cli_args) { "#{super()} --no-update" }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { core.update_db_required? }. to raise_error(WPScan::Error::MissingDatabaseFile)
|
||||
expect { core.update_db_required? }.to raise_error(WPScan::Error::MissingDatabaseFile)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ describe WPScan::Controller::Enumeration do
|
||||
config_backups_list config_backups_detection
|
||||
db_exports_list db_exports_detection
|
||||
medias_detection
|
||||
users_list users_detection]
|
||||
users_list users_detection exclude_usernames]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
XMLRPC_FAILED_BODY = '
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<methodResponse>
|
||||
<fault>
|
||||
<value>
|
||||
<struct>
|
||||
<member>
|
||||
<name>faultCode</name>
|
||||
<value><int>405</int></value>
|
||||
</member>
|
||||
<member>
|
||||
<name>faultString</name>
|
||||
<value><string>%s</string></value>
|
||||
</member>
|
||||
</struct>
|
||||
</value>
|
||||
</fault>
|
||||
</methodResponse>'
|
||||
|
||||
describe WPScan::Controller::PasswordAttack do
|
||||
subject(:controller) { described_class.new }
|
||||
let(:target_url) { 'http://ex.lo/' }
|
||||
@@ -15,13 +34,13 @@ describe WPScan::Controller::PasswordAttack do
|
||||
|
||||
it 'contains to correct options' do
|
||||
expect(controller.cli_options.map(&:to_sym))
|
||||
.to eq(%i[passwords usernames multicall_max_passwords password_attack])
|
||||
.to eq(%i[passwords usernames multicall_max_passwords password_attack login_uri])
|
||||
end
|
||||
end
|
||||
|
||||
describe '#users' do
|
||||
context 'when no --usernames' do
|
||||
it 'calles target.users' do
|
||||
it 'calls target.users' do
|
||||
expect(controller.target).to receive(:users)
|
||||
controller.users
|
||||
end
|
||||
@@ -40,10 +59,6 @@ describe WPScan::Controller::PasswordAttack do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#passwords' do
|
||||
xit
|
||||
end
|
||||
|
||||
describe '#run' do
|
||||
context 'when no --passwords is supplied' do
|
||||
it 'does not run the attacker' do
|
||||
@@ -85,20 +100,34 @@ describe WPScan::Controller::PasswordAttack do
|
||||
end
|
||||
|
||||
context 'when wp.getUsersBlogs method listed' do
|
||||
before { expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2]) }
|
||||
before do
|
||||
expect(xmlrpc).to receive(:available_methods).and_return(%w[wp.getUsersBlogs m2])
|
||||
|
||||
stub_request(:post, xmlrpc.url).to_return(body: body)
|
||||
end
|
||||
|
||||
context 'when wp.getUsersBlogs method disabled' do
|
||||
it 'returns false' do
|
||||
stub_request(:post, xmlrpc.url).to_return(body: 'XML-RPC services are disabled on this site.')
|
||||
context 'when blog is in EN' do
|
||||
let(:body) { format(XMLRPC_FAILED_BODY, 'XML-RPC services are disabled on this site.') }
|
||||
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
|
||||
it 'returns false' do
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context 'when blog is in FR' do
|
||||
let(:body) { format(XMLRPC_FAILED_BODY, 'Les services XML-RPC sont désactivés sur ce site.') }
|
||||
|
||||
it 'returns false' do
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wp.getUsersBlogs method enabled' do
|
||||
it 'returns true' do
|
||||
stub_request(:post, xmlrpc.url).to_return(body: 'Incorrect username or password.')
|
||||
let(:body) { 'Incorrect username or password.' }
|
||||
|
||||
it 'returns true' do
|
||||
expect(controller.xmlrpc_get_users_blogs_enabled?).to be true
|
||||
end
|
||||
end
|
||||
@@ -107,15 +136,34 @@ describe WPScan::Controller::PasswordAttack do
|
||||
end
|
||||
|
||||
describe '#attacker' do
|
||||
before do
|
||||
allow(controller.target).to receive(:sub_dir)
|
||||
controller.target.instance_variable_set(:@login_url, nil)
|
||||
end
|
||||
|
||||
context 'when --password-attack provided' do
|
||||
let(:cli_args) { "#{super()} --password-attack #{attack}" }
|
||||
|
||||
context 'when wp-login' do
|
||||
before { stub_request(:get, controller.target.url('wp-login.php')).to_return(status: status) }
|
||||
|
||||
let(:attack) { 'wp-login' }
|
||||
|
||||
it 'returns the correct object' do
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
context 'when available' do
|
||||
let(:status) { 200 }
|
||||
|
||||
it 'returns the correct object' do
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not available (404)' do
|
||||
let(:status) { 404 }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { controller.attacker }.to raise_error(WPScan::Error::NoLoginInterfaceDetected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -172,11 +220,26 @@ describe WPScan::Controller::PasswordAttack do
|
||||
|
||||
context 'when automatic detection' do
|
||||
context 'when xmlrpc_get_users_blogs_enabled? is false' do
|
||||
it 'returns the WpLogin' do
|
||||
before do
|
||||
expect(controller).to receive(:xmlrpc_get_users_blogs_enabled?).and_return(false)
|
||||
stub_request(:get, controller.target.url('wp-login.php')).to_return(status: status)
|
||||
end
|
||||
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
context 'when wp-login available' do
|
||||
let(:status) { 200 }
|
||||
|
||||
it 'returns the WpLogin' do
|
||||
expect(controller.attacker).to be_a WPScan::Finders::Passwords::WpLogin
|
||||
expect(controller.attacker.target).to be_a WPScan::Target
|
||||
end
|
||||
end
|
||||
|
||||
context 'when wp-login.php not available' do
|
||||
let(:status) { 404 }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { controller.attacker }.to raise_error(WPScan::Error::NoLoginInterfaceDetected)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ describe WPScan::Controller::VulnApi do
|
||||
|
||||
before do
|
||||
WPScan::ParsedCli.options = rspec_parsed_options(cli_args)
|
||||
WPScan::DB::VulnApi.instance_variable_set(:@default_request_params, nil)
|
||||
end
|
||||
|
||||
describe '#cli_options' do
|
||||
@@ -27,7 +28,7 @@ describe WPScan::Controller::VulnApi do
|
||||
let(:cli_args) { "#{super()} --api-token token" }
|
||||
|
||||
context 'when the token is invalid' do
|
||||
before { expect(WPScan::DB::VulnApi).to receive(:status).and_return('error' => 'HTTP Token: Access denied.') }
|
||||
before { expect(WPScan::DB::VulnApi).to receive(:status).and_return('status' => 'forbidden') }
|
||||
|
||||
it 'raise an InvalidApiToken error' do
|
||||
expect { controller.before_scan }.to raise_error(WPScan::Error::InvalidApiToken)
|
||||
@@ -74,20 +75,40 @@ describe WPScan::Controller::VulnApi do
|
||||
context 'when limited requests' do
|
||||
let(:requests) { 100 }
|
||||
|
||||
it 'does not raise an error' do
|
||||
it 'sets the token and does not raise an error' do
|
||||
expect { controller.before_scan }.to_not raise_error
|
||||
|
||||
expect(WPScan::DB::VulnApi.token).to eql 'token'
|
||||
end
|
||||
|
||||
context 'when unlimited requests' do
|
||||
let(:requests) { 'Unlimited' }
|
||||
|
||||
it 'does not raise an error' do
|
||||
it 'sets the token and does not raise an error' do
|
||||
expect { controller.before_scan }.to_not raise_error
|
||||
|
||||
expect(WPScan::DB::VulnApi.token).to eql 'token'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when token in ENV' do
|
||||
before do
|
||||
ENV[described_class::ENV_KEY] = 'token-from-env'
|
||||
|
||||
expect(WPScan::DB::VulnApi)
|
||||
.to receive(:status)
|
||||
.and_return('success' => true, 'plan' => 'free', 'requests_remaining' => 'Unlimited')
|
||||
end
|
||||
|
||||
it 'sets the token and does not raise an error' do
|
||||
expect { controller.before_scan }.to_not raise_error
|
||||
|
||||
expect(WPScan::DB::VulnApi.token).to eql 'token-from-env'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
allow(target).to receive(:sub_dir).and_return(false)
|
||||
end
|
||||
|
||||
it 'replace {domain_name} by its value' do
|
||||
it 'replaces {domain_name} by its values' do
|
||||
expect(finder.potential_urls(opts).keys).to eql %w[
|
||||
http://ex.lo/aa/ex.sql
|
||||
http://ex.lo/aa/wordpress.sql
|
||||
@@ -27,8 +27,8 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
context "when #{sub_domain} sub-domain" do
|
||||
let(:url) { "https://#{sub_domain}.domain.tld" }
|
||||
|
||||
it 'replace {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/domain.sql"
|
||||
it 'replaces {domain_name} by its correct values' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/domain.sql", "#{url}/#{sub_domain}.domain.sql"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,7 +36,7 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
context 'when multi-level tlds' do
|
||||
let(:url) { 'https://something.com.tr' }
|
||||
|
||||
it 'replace {domain_name} by its correct value' do
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include 'https://something.com.tr/something.sql'
|
||||
end
|
||||
end
|
||||
@@ -44,16 +44,38 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
context 'when multi-level tlds and sub-domain' do
|
||||
let(:url) { 'https://dev.something.com.tr' }
|
||||
|
||||
it 'replace {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include 'https://dev.something.com.tr/something.sql'
|
||||
it 'replaces {domain_name} by its correct values' do
|
||||
expect(finder.potential_urls(opts).keys).to include(
|
||||
'https://dev.something.com.tr/something.sql',
|
||||
'https://dev.something.com.tr/dev.something.sql'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when some weird stuff' do
|
||||
let(:url) { 'https://098f6bcd4621d373cade4e832627b4f6.aa-bb-ccc-dd.domain-test.com' }
|
||||
|
||||
it 'replace {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/domain-test.sql"
|
||||
it 'replaces {domain_name} by its correct values' do
|
||||
expect(finder.potential_urls(opts).keys).to include(
|
||||
"#{url}/domain-test.sql",
|
||||
"#{url}/098f6bcd4621d373cade4e832627b4f6.aa-bb-ccc-dd.domain-test.sql"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a non standard URL' do
|
||||
let(:url) { 'http://dc-2' }
|
||||
|
||||
it 'replaces {domain_name} by its correct value' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/dc-2.sql"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an IP address' do
|
||||
let(:url) { 'http://192.168.1.12' }
|
||||
|
||||
it 'replaces {domain_name} by the IP address' do
|
||||
expect(finder.potential_urls(opts).keys).to include "#{url}/192.168.1.12.sql"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -94,19 +116,29 @@ describe WPScan::Finders::DbExports::KnownLocations do
|
||||
expect(target).to receive(:homepage_or_404?).twice.and_return(false)
|
||||
end
|
||||
|
||||
it 'returns the expected Array<DbExport>' do
|
||||
expected = []
|
||||
context 'when matching the pattern' do
|
||||
it 'returns the expected Array<DbExport>' do
|
||||
expected = []
|
||||
|
||||
found_files.each do |file|
|
||||
url = "#{target.url}#{file}"
|
||||
expected << WPScan::Model::DbExport.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: described_class::DIRECT_ACCESS
|
||||
)
|
||||
found_files.each do |file|
|
||||
url = "#{target.url}#{file}"
|
||||
expected << WPScan::Model::DbExport.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: described_class::DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
|
||||
expect(finder.aggressive(opts)).to eql expected
|
||||
end
|
||||
end
|
||||
|
||||
expect(finder.aggressive(opts)).to eql expected
|
||||
context 'when not matching the pattern' do
|
||||
let(:db_export) { '' }
|
||||
|
||||
it 'returns an empty array' do
|
||||
expect(finder.aggressive(opts)).to eql []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,15 +35,47 @@ describe WPScan::Finders::InterestingFindings::DuplicatorInstallerLog do
|
||||
end
|
||||
|
||||
context 'when the body matches' do
|
||||
let(:body) { File.read(fixtures.join(filename)) }
|
||||
|
||||
it 'returns the InterestingFinding' do
|
||||
after do
|
||||
expect(finder.aggressive).to eql WPScan::Model::DuplicatorInstallerLog.new(
|
||||
log_url,
|
||||
confidence: 100,
|
||||
found_by: described_class::DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
|
||||
context 'when old versions of the file' do
|
||||
let(:body) { File.read(fixtures.join('old.txt')) }
|
||||
|
||||
it 'returns the InterestingFinding' do
|
||||
# handled in after loop above
|
||||
end
|
||||
end
|
||||
|
||||
context 'when newest versions of the file' do
|
||||
context 'when PRO format 1' do
|
||||
let(:body) { File.read(fixtures.join('pro.txt')) }
|
||||
|
||||
it 'returns the InterestingFinding' do
|
||||
# handled in after loop above
|
||||
end
|
||||
end
|
||||
|
||||
context 'when PRO format 2' do
|
||||
let(:body) { File.read(fixtures.join('pro2.txt')) }
|
||||
|
||||
it 'returns the InterestingFinding' do
|
||||
# handled in after loop above
|
||||
end
|
||||
end
|
||||
|
||||
context 'when LITE' do
|
||||
let(:body) { File.read(fixtures.join('lite.txt')) }
|
||||
|
||||
it 'returns the InterestingFinding' do
|
||||
# handled in after loop above
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ describe WPScan::Finders::InterestingFindings::EmergencyPwdResetScript do
|
||||
subject(:finder) { described_class.new(target) }
|
||||
let(:target) { WPScan::Target.new(url).extend(CMSScanner::Target::Server::Apache) }
|
||||
let(:url) { 'http://ex.lo/' }
|
||||
let(:file_url) { url + 'emergency.php' }
|
||||
let(:file_url) { "#{url}emergency.php" }
|
||||
let(:fixtures) { FINDERS_FIXTURES.join('interesting_findings', 'emergency_pwd_reset_script') }
|
||||
|
||||
before do
|
||||
|
||||
@@ -6,8 +6,55 @@ describe WPScan::Finders::InterestingFindings::MuPlugins do
|
||||
let(:url) { 'http://ex.lo/' }
|
||||
let(:fixtures) { FINDERS_FIXTURES.join('interesting_findings', 'mu_plugins') }
|
||||
|
||||
before do
|
||||
expect(target).to receive(:content_dir).at_least(1).and_return('wp-content')
|
||||
end
|
||||
|
||||
describe '#passive' do
|
||||
xit
|
||||
before { stub_request(:get, url).to_return(body: body) }
|
||||
|
||||
context 'when no uris' do
|
||||
let(:body) { '' }
|
||||
|
||||
its(:passive) { should be nil }
|
||||
end
|
||||
|
||||
context 'when a large amount of unrelated uris' do
|
||||
let(:body) do
|
||||
Array.new(250) { |i| "<a href='#{url}#{i}.html'>Some Link</a><img src='#{url}img-#{i}.png'/>" }.join("\n")
|
||||
end
|
||||
|
||||
it 'should not take a while to process the page' do
|
||||
time_start = Time.now
|
||||
result = finder.passive
|
||||
time_end = Time.now
|
||||
|
||||
expect(result).to be nil
|
||||
expect(time_end - time_start).to be < 1
|
||||
end
|
||||
end
|
||||
|
||||
context 'when uris' do
|
||||
let(:body) { File.read(fixtures.join(fixture)) }
|
||||
|
||||
context 'when none matching' do
|
||||
let(:fixture) { 'no_match.html' }
|
||||
|
||||
its(:passive) { should be nil }
|
||||
end
|
||||
|
||||
context 'when matching via href' do
|
||||
let(:fixture) { 'match_href.html' }
|
||||
|
||||
its(:passive) { should be_a WPScan::Model::MuPlugins }
|
||||
end
|
||||
|
||||
context 'when matching from src' do
|
||||
let(:fixture) { 'match_src.html' }
|
||||
|
||||
its(:passive) { should be_a WPScan::Model::MuPlugins }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#aggressive' do
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user