Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a94b8362a | ||
|
|
a25b493064 | ||
|
|
2acf88d83e | ||
|
|
baf3b4bc2b | ||
|
|
750411d9e1 | ||
|
|
aa7b922d30 | ||
|
|
fd660632e0 | ||
|
|
c7df7265ab | ||
|
|
42685a45b3 | ||
|
|
ce5d26a220 | ||
|
|
0e73774bd9 | ||
|
|
85b491472a | ||
|
|
4b382acbad | ||
|
|
12d15bfc7e | ||
|
|
ea1b6b9c17 | ||
|
|
5cb2d16601 | ||
|
|
913717bcf7 | ||
|
|
99fe1855d9 | ||
|
|
e2eb94be22 | ||
|
|
aca1b487ba | ||
|
|
5820c53d0f | ||
|
|
9298758acd | ||
|
|
a981c2b17b | ||
|
|
a783b53107 | ||
|
|
cf2881fda6 | ||
|
|
59368a72bd | ||
|
|
439900a1ea | ||
|
|
44557797b0 | ||
|
|
ba065d5974 | ||
|
|
105e9cbcac | ||
|
|
fe277c1e89 | ||
|
|
b5e3e6280e | ||
|
|
f90a64ce81 | ||
|
|
b9fa1e3587 | ||
|
|
4333ecb989 | ||
|
|
715d3d4ad6 | ||
|
|
38f70a88ae | ||
|
|
4b4b968710 | ||
|
|
3b94fc49a7 | ||
|
|
e41aab3a80 | ||
|
|
9450ba6cc5 | ||
|
|
ae3c164350 | ||
|
|
24e6820a90 | ||
|
|
0e05f77fb7 | ||
|
|
de960ff9db | ||
|
|
1d0128af72 | ||
|
|
285b1a1733 | ||
|
|
ab67816dd9 | ||
|
|
fea6665876 | ||
|
|
6cbc8c9924 | ||
|
|
f542a50213 | ||
|
|
fa430606ce | ||
|
|
05d27c64be | ||
|
|
0cd680bb29 | ||
|
|
ced94a7338 | ||
|
|
b65a4d0a60 | ||
|
|
2b85b44bd1 | ||
|
|
991c87a89e | ||
|
|
37a72f0c72 | ||
|
|
6c0a21c80d | ||
|
|
dc48008d43 | ||
|
|
5720d29492 | ||
|
|
358f3d59d8 | ||
|
|
b6c6a46d25 | ||
|
|
25c393d557 | ||
|
|
435fb34233 | ||
|
|
2c40913a64 | ||
|
|
e437b952da | ||
|
|
282c595b38 | ||
|
|
c2c8d63e75 | ||
|
|
ad21d97d11 | ||
|
|
5c27c78ed0 | ||
|
|
a53e9a5e12 | ||
|
|
c8036692ee | ||
|
|
b9535a3648 | ||
|
|
651c364fa9 | ||
|
|
958410d4c9 | ||
|
|
e9fba126d2 | ||
|
|
95d39cce5a | ||
|
|
32d9afdf9b | ||
|
|
7e9a4168ff | ||
|
|
9d6415a89b | ||
|
|
1499b07176 | ||
|
|
9c7188a312 | ||
|
|
b63e28c150 | ||
|
|
50d48902cf | ||
|
|
aa6899cbc5 | ||
|
|
94e6b2eab6 | ||
|
|
54c0e79c58 | ||
|
|
859d7f1c60 | ||
|
|
166112209e | ||
|
|
952395d0c1 | ||
|
|
c7061f8a51 | ||
|
|
0c71bce221 | ||
|
|
b2b4eebd78 | ||
|
|
5257a8b997 | ||
|
|
9844f9d8ab | ||
|
|
000f275263 | ||
|
|
e5077c490a | ||
|
|
d76968c15f | ||
|
|
289ef5b0dd | ||
|
|
7ec227873c | ||
|
|
1deccfd477 | ||
|
|
286e6bd51a | ||
|
|
8167fa2e17 | ||
|
|
c960df0bb1 | ||
|
|
ebf8d31c6c | ||
|
|
082ae650fc | ||
|
|
2f5599c863 | ||
|
|
a764bdd993 | ||
|
|
ef46d2c956 | ||
|
|
d2c2c1defb | ||
|
|
dede023ec8 | ||
|
|
d8a9b3aa77 | ||
|
|
ad364e6a2e | ||
|
|
523954e507 | ||
|
|
872bbdb8e0 | ||
|
|
3ca8727b64 | ||
|
|
1d3ca87772 | ||
|
|
90c42f42a1 |
@@ -1,21 +1,21 @@
|
||||
git/
|
||||
bundle/
|
||||
.idea/
|
||||
.yardoc/
|
||||
cache/
|
||||
coverage/
|
||||
spec/
|
||||
dev/
|
||||
.*
|
||||
**/*.md
|
||||
bin/
|
||||
dev/
|
||||
spec/
|
||||
*.md
|
||||
Dockerfile
|
||||
|
||||
## TEMP
|
||||
.idea/
|
||||
.yardoc/
|
||||
bundle/
|
||||
cache/
|
||||
coverage/
|
||||
git/
|
||||
**/*.md
|
||||
**/*.orig
|
||||
*.orig
|
||||
CREDITS
|
||||
data.zip
|
||||
DISCLAIMER.txt
|
||||
example.conf.json
|
||||
bin/
|
||||
log.txt
|
||||
|
||||
|
||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -1,16 +1,21 @@
|
||||
# WPScan (If not using ~/.wpscan/)
|
||||
cache/
|
||||
data/
|
||||
log.txt
|
||||
output.txt
|
||||
|
||||
# WPScan (Deployment)
|
||||
debug.log
|
||||
rspec_results.html
|
||||
wordlist.txt
|
||||
|
||||
# OS/IDE Rubbish
|
||||
coverage/
|
||||
.yardoc/
|
||||
.idea/
|
||||
*.sublime-*
|
||||
.*.swp
|
||||
.ash_history
|
||||
cache
|
||||
coverage
|
||||
.bundle
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
*.sublime-*
|
||||
.idea
|
||||
.*.swp
|
||||
log.txt
|
||||
.yardoc
|
||||
debug.log
|
||||
wordlist.txt
|
||||
rspec_results.html
|
||||
data/
|
||||
vendor/
|
||||
.DS_Store?
|
||||
@@ -1 +1 @@
|
||||
2.4.1
|
||||
2.5.1
|
||||
|
||||
@@ -13,13 +13,18 @@ rvm:
|
||||
- 2.3.2
|
||||
- 2.3.3
|
||||
- 2.4.1
|
||||
- 2.4.2
|
||||
- 2.5.0
|
||||
- 2.5.1
|
||||
- ruby-head
|
||||
before_install:
|
||||
- "env"
|
||||
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
|
||||
- "gem install bundler"
|
||||
- "bundler --version"
|
||||
- "gem regenerate_binstubs"
|
||||
- "bundle --version"
|
||||
before_script:
|
||||
- "unzip -o $TRAVIS_BUILD_DIR/data.zip -d $TRAVIS_BUILD_DIR"
|
||||
- "unzip -o $TRAVIS_BUILD_DIR/data.zip -d $HOME/.wpscan/"
|
||||
script:
|
||||
- "bundle exec rspec"
|
||||
notifications:
|
||||
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,6 +1,35 @@
|
||||
# Changelog
|
||||
## Master
|
||||
[Work in progress](https://github.com/wpscanteam/wpscan/compare/2.9.3...master)
|
||||
[Work in progress](https://github.com/wpscanteam/wpscan/compare/2.9.4...master)
|
||||
|
||||
## Version 2.9.4
|
||||
Released: 2018-06-15
|
||||
|
||||
* Updated dependencies and required ruby version
|
||||
* Improved CLI output
|
||||
* Only show readme.html output when wp <= 4.8 #1127
|
||||
* Cleanup README.md
|
||||
* Fix bug "undefined method 'identifier' for nil:NilClass" #1149
|
||||
* Since WP 4.7 readme.html only shows major version #1152
|
||||
* Add checks for humans.txt and security.text (Thank you @g0tmi1k!)
|
||||
* Add offline database update support (Thank you @g0tmi1k!)
|
||||
* Check for API access and /wp-json/'s users output (Thank you @g0tmi1k!)
|
||||
* Add RSS author information (Thank you @g0tmi1k!)
|
||||
* Check HTTP status of each value in /robots.txt (Thank you @g0tmi1k!)
|
||||
* Follow any redirections (e.g. http -> https) (Thank you @g0tmi1k!)
|
||||
* Lots of other enhancements by @g0tmi1k & WPScan Team
|
||||
* Database export file enumeration.
|
||||
|
||||
WPScan Database Statistics:
|
||||
* Total tracked wordpresses: 319
|
||||
* Total tracked plugins: 74896
|
||||
* Total tracked themes: 16666
|
||||
* Total vulnerable wordpresses: 305
|
||||
* Total vulnerable plugins: 1645
|
||||
* Total vulnerable themes: 286
|
||||
* Total wordpress vulnerabilities: 8327
|
||||
* Total plugin vulnerabilities: 2603
|
||||
* Total theme vulnerabilities: 352
|
||||
|
||||
## Version 2.9.3
|
||||
Released: 2017-07-19
|
||||
|
||||
21
CREDITS
21
CREDITS
@@ -1,21 +0,0 @@
|
||||
**CREDITS**
|
||||
|
||||
This file is used to state the individual WPScan Team members (core developers) and give credit to WPScan's other contributors. If you feel your name should be in here email team@wpscan.org.
|
||||
|
||||
*WPScan Team*
|
||||
|
||||
Erwan.LR - @erwan_lr - (Project Developer)
|
||||
Christian Mehlmauer - @_FireFart_ - (Project Developer)
|
||||
Peter van der Laan - pvdl - (Project Developer)
|
||||
Ryan Dewhurst - @ethicalhack3r (Project Lead)
|
||||
|
||||
*Other Contributors*
|
||||
|
||||
Henri Salo AKA fgeek - Reported lots of vulnerabilities
|
||||
Alip AKA Undead - alip.aswalid at gmail.com
|
||||
michee08 - Reported and gave potential solutions to bugs
|
||||
Callum Pember - Implemented proxy support - callumpember at gmail.com
|
||||
g0tmi1k - Additional timthumb checks + bug reports
|
||||
Melvin Lammerts - Reported a couple of fake vulnerabilities - melvin at 12k.nl
|
||||
Paolo Perego - @thesp0nge - Basic authentication
|
||||
Gianluca Brindisi - @gbrindisi - Ex Project Developer
|
||||
22
Dockerfile
22
Dockerfile
@@ -1,29 +1,37 @@
|
||||
FROM ruby:2.4-alpine
|
||||
MAINTAINER WPScan Team <team@wpscan.org>
|
||||
FROM ruby:2.5-alpine
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
|
||||
ARG BUNDLER_ARGS="--jobs=8 --without test"
|
||||
|
||||
# Add a new user
|
||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||
|
||||
# Setup gems
|
||||
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
|
||||
|
||||
COPY Gemfile /wpscan
|
||||
COPY Gemfile.lock /wpscan
|
||||
|
||||
# runtime dependencies
|
||||
# Runtime dependencies
|
||||
RUN apk add --no-cache libcurl procps && \
|
||||
# build dependencies
|
||||
apk add --no-cache --virtual build-deps alpine-sdk ruby-dev libffi-dev zlib-dev && \
|
||||
bundle install --system --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
|
||||
apk del --no-cache build-deps
|
||||
|
||||
# Copy over data & set permissions
|
||||
COPY . /wpscan
|
||||
RUN chown -R wpscan:wpscan /wpscan
|
||||
|
||||
USER wpscan
|
||||
|
||||
RUN /wpscan/wpscan.rb --update --verbose --no-color
|
||||
|
||||
# Switch directory
|
||||
WORKDIR /wpscan
|
||||
|
||||
# Switch users
|
||||
USER wpscan
|
||||
|
||||
# Update WPScan
|
||||
RUN /wpscan/wpscan.rb --update --verbose --no-color
|
||||
|
||||
# Run WPScan
|
||||
ENTRYPOINT ["/wpscan/wpscan.rb"]
|
||||
CMD ["--help"]
|
||||
|
||||
9
Gemfile
9
Gemfile
@@ -1,11 +1,12 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'typhoeus', '>=1.1.2'
|
||||
gem 'nokogiri', '>=1.7.0.1'
|
||||
gem 'addressable', '>=2.5.0'
|
||||
gem 'yajl-ruby', '>=1.3.0' # Better JSON parser regarding memory usage
|
||||
gem 'terminal-table', '>=1.6.0'
|
||||
gem 'nokogiri', '>=1.7.0.1'
|
||||
gem 'ruby-progressbar', '>=1.8.1'
|
||||
gem 'rubyzip', '>=1.2.1'
|
||||
gem 'terminal-table', '>=1.6.0'
|
||||
gem 'typhoeus', '>=1.1.2'
|
||||
gem 'yajl-ruby', '>=1.3.0' # Better JSON parser regarding memory usage
|
||||
|
||||
group :test do
|
||||
gem 'webmock', '>=2.3.2'
|
||||
|
||||
69
Gemfile.lock
69
Gemfile.lock
@@ -1,69 +0,0 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
addressable (2.5.1)
|
||||
public_suffix (~> 2.0, >= 2.0.2)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
diff-lcs (1.3)
|
||||
docile (1.1.5)
|
||||
ethon (0.10.1)
|
||||
ffi (>= 1.3.0)
|
||||
ffi (1.9.18)
|
||||
hashdiff (0.3.4)
|
||||
json (2.1.0)
|
||||
mini_portile2 (2.2.0)
|
||||
nokogiri (1.8.0)
|
||||
mini_portile2 (~> 2.2.0)
|
||||
public_suffix (2.0.5)
|
||||
rspec (3.6.0)
|
||||
rspec-core (~> 3.6.0)
|
||||
rspec-expectations (~> 3.6.0)
|
||||
rspec-mocks (~> 3.6.0)
|
||||
rspec-core (3.6.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-expectations (3.6.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-its (1.2.0)
|
||||
rspec-core (>= 3.0.0)
|
||||
rspec-expectations (>= 3.0.0)
|
||||
rspec-mocks (3.6.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.6.0)
|
||||
rspec-support (3.6.0)
|
||||
ruby-progressbar (1.8.1)
|
||||
safe_yaml (1.0.4)
|
||||
simplecov (0.14.1)
|
||||
docile (~> 1.1.0)
|
||||
json (>= 1.8, < 3)
|
||||
simplecov-html (~> 0.10.0)
|
||||
simplecov-html (0.10.1)
|
||||
terminal-table (1.8.0)
|
||||
unicode-display_width (~> 1.1, >= 1.1.1)
|
||||
typhoeus (1.1.2)
|
||||
ethon (>= 0.9.0)
|
||||
unicode-display_width (1.3.0)
|
||||
webmock (3.0.1)
|
||||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
yajl-ruby (1.3.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
addressable (>= 2.5.0)
|
||||
nokogiri (>= 1.7.0.1)
|
||||
rspec (>= 3.5.0)
|
||||
rspec-its (>= 1.2.0)
|
||||
ruby-progressbar (>= 1.8.1)
|
||||
simplecov (>= 0.13.0)
|
||||
terminal-table (>= 1.6.0)
|
||||
typhoeus (>= 1.1.2)
|
||||
webmock (>= 2.3.2)
|
||||
yajl-ruby (>= 1.3.0)
|
||||
|
||||
BUNDLED WITH
|
||||
1.14.6
|
||||
4
LICENSE
4
LICENSE
@@ -1,6 +1,6 @@
|
||||
WPScan Public Source License
|
||||
|
||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2016 WPScan Team.
|
||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
|
||||
|
||||
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
||||
|
||||
@@ -8,7 +8,7 @@ Cases that include commercialization of WPScan require a commercial, non-free li
|
||||
|
||||
1.1 “License” means this document.
|
||||
1.2 “Contributor” means each individual or legal entity that creates, contributes to the creation of, or owns WPScan.
|
||||
1.3 “WPScan Team” means WPScan’s core developers, an updated list of whom can be found within the CREDITS file.
|
||||
1.3 “WPScan Team” means WPScan’s core developers.
|
||||
|
||||
2. Commercialization
|
||||
|
||||
|
||||
55
README.md
55
README.md
@@ -1,16 +1,17 @@
|
||||

|
||||
|
||||
|
||||
[](https://travis-ci.org/wpscanteam/wpscan)
|
||||
[](https://codeclimate.com/github/wpscanteam/wpscan)
|
||||
[](https://gemnasium.com/wpscanteam/wpscan)
|
||||
[](https://hub.docker.com/r/wpscanteam/wpscan/)
|
||||
[](https://www.patreon.com/wpscan)
|
||||
|
||||

|
||||
|
||||
# LICENSE
|
||||
|
||||
## WPScan Public Source License
|
||||
|
||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2016 WPScan Team.
|
||||
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2018 WPScan Team.
|
||||
|
||||
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
|
||||
|
||||
@@ -102,6 +103,10 @@ Windows is not supported
|
||||
We suggest you use our official Docker image from https://hub.docker.com/r/wpscanteam/wpscan/ to avoid installation problems.
|
||||
|
||||
# DOCKER
|
||||
## Install Docker
|
||||
[https://docs.docker.com/engine/installation/](https://docs.docker.com/engine/installation/)
|
||||
|
||||
## Get the image
|
||||
Pull the repo with `docker pull wpscanteam/wpscan`
|
||||
|
||||
## Start WPScan
|
||||
@@ -122,6 +127,8 @@ Mount a local wordlist to the docker container and start a bruteforce attack for
|
||||
docker run -it --rm -v ~/wordlists:/wordlists wpscanteam/wpscan --url https://yourblog.com --wordlist /wordlists/crackstation.txt --username admin
|
||||
```
|
||||
|
||||
(This mounts the host directory `~/wordlists` to the container in the path `/wordlists`)
|
||||
|
||||
Use logfile option
|
||||
```
|
||||
# the file must exist prior to starting the container, otherwise docker will create a directory with the filename
|
||||
@@ -129,15 +136,13 @@ touch ~/FILENAME
|
||||
docker run -it --rm -v ~/FILENAME:/wpscan/output.txt wpscanteam/wpscan --url https://yourblog.com --log /wpscan/output.txt
|
||||
```
|
||||
|
||||
(This mounts the host directory `~/wordlists` to the container in the path `/wordlists`)
|
||||
|
||||
Published on https://hub.docker.com/r/wpscanteam/wpscan/
|
||||
|
||||
# Manual install
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Ruby >= 2.1.9 - Recommended: 2.4.1
|
||||
- Ruby >= 2.1.9 - Recommended: 2.5.1
|
||||
- Curl >= 7.21 - Recommended: latest - FYI the 7.29 has a segfault
|
||||
- RubyGems - Recommended: latest
|
||||
- Git
|
||||
@@ -174,8 +179,8 @@ https://rvm.io/integration/gnome-terminal#integrating-rvm-with-gnome-terminal
|
||||
curl -sSL https://get.rvm.io | bash -s stable
|
||||
source ~/.rvm/scripts/rvm
|
||||
echo "source ~/.rvm/scripts/rvm" >> ~/.bashrc
|
||||
rvm install 2.4.1
|
||||
rvm use 2.4.1 --default
|
||||
rvm install 2.5.1
|
||||
rvm use 2.5.1 --default
|
||||
echo "gem: --no-ri --no-rdoc" > ~/.gemrc
|
||||
git clone https://github.com/wpscanteam/wpscan.git
|
||||
cd wpscan
|
||||
@@ -190,39 +195,6 @@ https://rvm.io/integration/gnome-terminal#integrating-rvm-with-gnome-terminal
|
||||
|
||||
# KNOWN ISSUES
|
||||
|
||||
- Typhoeus segmentation fault
|
||||
|
||||
Update cURL to version => 7.21 (may have to install from source)
|
||||
|
||||
- Proxy not working
|
||||
|
||||
Update cURL to version => 7.21.7 (may have to install from source).
|
||||
|
||||
Installation from sources :
|
||||
|
||||
Grab the sources from http://curl.haxx.se/download.html
|
||||
Decompress the archive
|
||||
Open the folder with the extracted files
|
||||
Run ./configure
|
||||
Run make
|
||||
Run sudo make install
|
||||
Run sudo ldconfig
|
||||
|
||||
|
||||
- cannot load such file -- readline:
|
||||
|
||||
sudo aptitude install libreadline5-dev libncurses5-dev
|
||||
|
||||
Then, open the directory of the readline gem (you have to locate it)
|
||||
|
||||
cd ~/.rvm/src/ruby-XXXX/ext/readline
|
||||
ruby extconf.rb
|
||||
make
|
||||
make install
|
||||
|
||||
|
||||
See [http://vvv.tobiassjosten.net/ruby-on-rails/fixing-readline-for-the-ruby-on-rails-console/](http://vvv.tobiassjosten.net/ruby-on-rails/fixing-readline-for-the-ruby-on-rails-console/) for more details
|
||||
|
||||
- no such file to load -- rubygems
|
||||
|
||||
```update-alternatives --config ruby```
|
||||
@@ -282,7 +254,6 @@ https://rvm.io/integration/gnome-terminal#integrating-rvm-with-gnome-terminal
|
||||
--request-timeout <request-timeout> Request Timeout.
|
||||
--connect-timeout <connect-timeout> Connect Timeout.
|
||||
--threads | -t <number of threads> The number of threads to use when multi-threading requests.
|
||||
--max-threads <max-threads> Maximum Threads.
|
||||
--throttle <milliseconds> Milliseconds to wait before doing another web request. If used, the --threads should be set to 1.
|
||||
--help | -h This help screen.
|
||||
--verbose | -v Verbose output.
|
||||
|
||||
@@ -9,4 +9,4 @@ done
|
||||
DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
|
||||
|
||||
cd $DIR/../
|
||||
docker run -it --rm -v "$DIR/../":/wpscan -w /wpscan ruby:2.4 bundle update
|
||||
docker run --rm -v "$DIR/../":/usr/src/app -w /usr/src/app ruby:2.5-alpine /bin/sh -c "gem install bundler; bundle lock --update"
|
||||
|
||||
2
data/.gitignore
vendored
2
data/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -126,7 +126,7 @@ class Browser
|
||||
def merge_request_params(params = {})
|
||||
if @proxy
|
||||
params.merge!(proxy: @proxy)
|
||||
params.merge!(proxyauth: @proxy_auth) if @proxy_auth
|
||||
params.merge!(proxyuserpwd: @proxy_auth) if @proxy_auth
|
||||
end
|
||||
|
||||
if @basic_auth
|
||||
|
||||
@@ -26,6 +26,10 @@ class CacheFileStore
|
||||
unless Dir.exist?(@storage_path)
|
||||
FileUtils.mkdir_p(@storage_path)
|
||||
end
|
||||
|
||||
unless Pathname.new(@storage_path).writable?
|
||||
fail "#{@storage_path} is not writable"
|
||||
end
|
||||
end
|
||||
|
||||
def clean
|
||||
|
||||
@@ -95,7 +95,7 @@ class WpItems < Array
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
if ! code.valid_encoding?
|
||||
if !code.valid_encoding?
|
||||
code = code.encode('UTF-16be', :invalid => :replace, :replace => '?').encode('UTF-8')
|
||||
end
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ class WpUsers < WpItems
|
||||
# @return [ void ]
|
||||
def output(options = {})
|
||||
rows = []
|
||||
headings = ['Id', 'Login', 'Name']
|
||||
headings = ['ID', 'Login', 'Name']
|
||||
headings << 'Password' if options[:show_password]
|
||||
|
||||
remove_junk_from_display_names
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
LIB_DIR = File.expand_path(File.join(__dir__, '..'))
|
||||
ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # expand_path is used to get "wpscan/" instead of "wpscan/lib/../"
|
||||
DATA_DIR = File.join(ROOT_DIR, 'data')
|
||||
CONF_DIR = File.join(ROOT_DIR, 'conf')
|
||||
CACHE_DIR = File.join(ROOT_DIR, 'cache')
|
||||
WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan')
|
||||
UPDATER_LIB_DIR = File.join(LIB_DIR, 'updater')
|
||||
COMMON_LIB_DIR = File.join(LIB_DIR, 'common')
|
||||
MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models')
|
||||
COLLECTIONS_LIB_DIR = File.join(COMMON_LIB_DIR, 'collections')
|
||||
# Location directories
|
||||
LIB_DIR = File.expand_path(File.join(__dir__, '..')) # wpscan/lib/
|
||||
ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # wpscan/ - expand_path is used to get "wpscan/" instead of "wpscan/lib/../"
|
||||
USER_DIR = File.expand_path(Dir.home) # ~/
|
||||
|
||||
DEFAULT_LOG_FILE = File.join(ROOT_DIR, 'log.txt')
|
||||
# Core WPScan directories
|
||||
CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/
|
||||
DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/
|
||||
CONF_DIR = File.join(USER_DIR, '.wpscan/conf') # ~/.wpscan/conf/ - Not used ATM (only ref via ./spec/ for travis)
|
||||
COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/
|
||||
WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/
|
||||
MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/
|
||||
|
||||
# Plugins directories
|
||||
COMMON_PLUGINS_DIR = File.join(COMMON_LIB_DIR, 'plugins')
|
||||
WPSCAN_PLUGINS_DIR = File.join(WPSCAN_LIB_DIR, 'plugins') # Not used ATM
|
||||
# Core WPScan files
|
||||
DEFAULT_LOG_FILE = File.join(USER_DIR, '.wpscan/log.txt') # ~/.wpscan/log.txt
|
||||
DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip
|
||||
|
||||
# Data files
|
||||
WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json')
|
||||
PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json')
|
||||
THEMES_FILE = File.join(DATA_DIR, 'themes.json')
|
||||
WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml')
|
||||
LOCAL_FILES_FILE = File.join(DATA_DIR, 'local_vulnerable_files.xml')
|
||||
WP_VERSIONS_XSD = File.join(DATA_DIR, 'wp_versions.xsd')
|
||||
LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd')
|
||||
USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt')
|
||||
LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update')
|
||||
# WPScan Data files (data.zip)
|
||||
LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update
|
||||
PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') # ~/.wpscan/data/plugins.json
|
||||
THEMES_FILE = File.join(DATA_DIR, 'themes.json') # ~/.wpscan/data/themes.json
|
||||
TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') # ~/.wpscan/data/timthumbs.txt
|
||||
USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') # ~/.wpscan/data/user-agents.txt
|
||||
WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') # ~/.wpscan/data/wordpresses.json
|
||||
WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') # ~/.wpscan/data/wp_versions.xml
|
||||
|
||||
MIN_RUBY_VERSION = '2.1.9'
|
||||
|
||||
WPSCAN_VERSION = '2.9.3'
|
||||
WPSCAN_VERSION = '2.9.5-dev'
|
||||
|
||||
$LOAD_PATH.unshift(LIB_DIR)
|
||||
$LOAD_PATH.unshift(WPSCAN_LIB_DIR)
|
||||
@@ -50,6 +48,7 @@ def windows?
|
||||
end
|
||||
|
||||
require 'environment'
|
||||
require 'zip'
|
||||
|
||||
def escape_glob(s)
|
||||
s.gsub(/[\\\{\}\[\]\*\?]/) { |x| '\\' + x }
|
||||
@@ -78,13 +77,39 @@ def add_trailing_slash(url)
|
||||
url =~ /\/$/ ? url : "#{url}/"
|
||||
end
|
||||
|
||||
def missing_db_file?
|
||||
def missing_db_files?
|
||||
DbUpdater::FILES.each do |db_file|
|
||||
return true unless File.exist?(File.join(DATA_DIR, db_file))
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Find data.zip?
|
||||
def has_db_zip?
|
||||
return File.exist?(DATA_FILE)
|
||||
end
|
||||
|
||||
# Extract data.zip
|
||||
def extract_db_zip
|
||||
# Create data folder
|
||||
FileUtils.mkdir_p(DATA_DIR)
|
||||
|
||||
Zip::File.open(DATA_FILE) do |zip_file|
|
||||
zip_file.each do |f|
|
||||
# Feedback to the user
|
||||
#puts "[+] Extracting: #{File.basename(f.name)}"
|
||||
f_path = File.join(DATA_DIR, File.basename(f.name))
|
||||
|
||||
# Delete if already there
|
||||
#puts "[+] Deleting: #{File.basename(f.name)}" if File.exist?(f_path)
|
||||
FileUtils.rm(f_path) if File.exist?(f_path)
|
||||
|
||||
# Extract
|
||||
zip_file.extract(f, f_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def last_update
|
||||
date = nil
|
||||
if File.exists?(LAST_UPDATE_FILE)
|
||||
@@ -94,6 +119,7 @@ def last_update
|
||||
date
|
||||
end
|
||||
|
||||
# Was it 5 days ago?
|
||||
def update_required?
|
||||
date = last_update
|
||||
day_seconds = 24 * 60 * 60
|
||||
@@ -161,7 +187,7 @@ def banner
|
||||
puts ' WordPress Security Scanner by the WPScan Team '
|
||||
puts " Version #{WPSCAN_VERSION}"
|
||||
puts ' Sponsored by Sucuri - https://sucuri.net'
|
||||
puts ' @_WPScan_, @ethicalhack3r, @erwan_lr, pvdl, @_FireFart_'
|
||||
puts ' @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_'
|
||||
puts '_______________________________________________________________'
|
||||
puts
|
||||
end
|
||||
@@ -253,14 +279,26 @@ end
|
||||
# @return [ String ] A random user-agent from data/user-agents.txt
|
||||
def get_random_user_agent
|
||||
user_agents = []
|
||||
|
||||
# If we can't access the file, die
|
||||
raise('[ERROR] Missing user-agent data. Please re-run with just --update.') unless File.exist?(USER_AGENTS_FILE)
|
||||
|
||||
# Read in file
|
||||
f = File.open(USER_AGENTS_FILE, 'r')
|
||||
|
||||
# Read every line in the file
|
||||
f.each_line do |line|
|
||||
# ignore comments
|
||||
# Remove any End of Line issues, and leading/trailing spaces
|
||||
line = line.strip.chomp
|
||||
# Ignore empty files and comments
|
||||
next if line.empty? or line =~ /^\s*(#|\/\/)/
|
||||
# Add to array
|
||||
user_agents << line.strip
|
||||
end
|
||||
# Close file handler
|
||||
f.close
|
||||
# return ransom user-agent
|
||||
|
||||
# Return random user-agent
|
||||
user_agents.sample
|
||||
end
|
||||
|
||||
@@ -274,3 +312,21 @@ end
|
||||
def url_encode(str)
|
||||
CGI.escape(str).gsub("+", "%20")
|
||||
end
|
||||
|
||||
# Check valid JSON?
|
||||
def valid_json?(json)
|
||||
JSON.parse(json)
|
||||
return true
|
||||
rescue JSON::ParserError => e
|
||||
return false
|
||||
end
|
||||
|
||||
# Get the HTTP response code
|
||||
def get_http_status(url)
|
||||
Browser.get(url.to_s).code
|
||||
end
|
||||
|
||||
# Check to see if we need a "s"
|
||||
def grammar_s(size)
|
||||
size.to_i >= 2 ? "s" : ""
|
||||
end
|
||||
|
||||
@@ -13,8 +13,13 @@ class DbUpdater
|
||||
def initialize(repo_directory)
|
||||
@repo_directory = repo_directory
|
||||
|
||||
fail "#{repo_directory} is not writable" unless \
|
||||
Pathname.new(repo_directory).writable?
|
||||
unless Dir.exist?(@repo_directory)
|
||||
FileUtils.mkdir_p(@repo_directory)
|
||||
end
|
||||
|
||||
unless Pathname.new(@repo_directory).writable?
|
||||
fail "#{@repo_directory} is not writable"
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Hash ] The params for Typhoeus::Request
|
||||
@@ -83,7 +88,7 @@ class DbUpdater
|
||||
def update(verbose = false)
|
||||
FILES.each do |filename|
|
||||
begin
|
||||
puts "[+] Checking #{filename}" if verbose
|
||||
puts "[+] Checking: #{filename}" if verbose
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
|
||||
# Checking if the file needs to be updated
|
||||
@@ -95,7 +100,7 @@ class DbUpdater
|
||||
puts ' [i] Needs to be updated' if verbose
|
||||
create_backup(filename)
|
||||
puts ' [i] Backup Created' if verbose
|
||||
puts ' [i] Downloading new file' if verbose
|
||||
puts " [i] Downloading new file: #{remote_file_url(filename)}" if verbose
|
||||
dl_checksum = download(filename)
|
||||
puts " [i] Downloaded File Checksum: #{dl_checksum}" if verbose
|
||||
puts " [i] Database File Checksum : #{db_checksum}" if verbose
|
||||
|
||||
@@ -5,27 +5,6 @@ class WpItem
|
||||
# @uri is used instead of #uri to avoid the presence of the :path into it
|
||||
module Infos
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_readme?
|
||||
!readme_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String,nil ] The url to the readme file, nil if not found
|
||||
def readme_url
|
||||
# See https://github.com/wpscanteam/wpscan/pull/737#issuecomment-66375445
|
||||
# for any question about the order
|
||||
%w{readme.txt README.txt Readme.txt ReadMe.txt README.TXT readme.TXT}.each do |readme|
|
||||
url = @uri.merge(readme).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_changelog?
|
||||
url_is_200?(changelog_url)
|
||||
end
|
||||
|
||||
# Checks if the url status code is 200
|
||||
#
|
||||
# @param [ String ] url
|
||||
@@ -35,9 +14,34 @@ class WpItem
|
||||
Browser.get(url).code == 200
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_readme?
|
||||
!readme_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String,nil ] The url to the readme file, nil if not found
|
||||
def readme_url
|
||||
# See https://github.com/wpscanteam/wpscan/pull/737#issuecomment-66375445
|
||||
# for any question about the order
|
||||
%w{readme.txt README.txt README.md readme.md Readme.txt}.each do |readme|
|
||||
url = @uri.merge(readme).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_changelog?
|
||||
!changelog_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the changelog file
|
||||
def changelog_url
|
||||
@uri.merge('changelog.txt').to_s
|
||||
%w{changelog.txt CHANGELOG.md changelog.md}.each do |changelog|
|
||||
url = @uri.merge(changelog).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
|
||||
@@ -23,7 +23,7 @@ class WpItem
|
||||
|
||||
if version.nil? && vulnerabilities.length > 0
|
||||
puts
|
||||
puts warning('We could not determine a version so all vulnerabilities are printed out')
|
||||
puts warning('We could not determine the version installed. All of the past known vulnerabilities will be output to allow you to do your own manual investigation.')
|
||||
end
|
||||
|
||||
vulnerabilities.output
|
||||
|
||||
@@ -30,7 +30,7 @@ class WpTheme < WpItem
|
||||
response = Browser.get_and_follow_location(target_uri.to_s)
|
||||
|
||||
# https + domain is optional because of relative links
|
||||
return unless response.body =~ %r{(?:https?://[^"']+/)?([^/\s]+)/themes/([^"'/]+)[^"']*/style.css}i
|
||||
return unless response.body =~ %r{(?:https?://[^"']+/)?([^"'/\s]+)/themes/([^"'/]+)[^"']*/style\.css}i
|
||||
|
||||
new(
|
||||
target_uri,
|
||||
|
||||
@@ -168,11 +168,14 @@ class WpVersion < WpItem
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_readme(target_uri)
|
||||
scan_url(
|
||||
version = scan_url(
|
||||
target_uri,
|
||||
%r{<br />\sversion #{version_pattern}}i,
|
||||
'readme.html'
|
||||
)
|
||||
|
||||
# Since WP >= 4.7, the Readme only contains the major version
|
||||
VersionCompare.lesser?(version, '4.7') ? version : nil
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the sitemap.xml file.
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'web_site/robots_txt'
|
||||
require 'web_site/humans_txt'
|
||||
require 'web_site/interesting_headers'
|
||||
require 'web_site/robots_txt'
|
||||
require 'web_site/security_txt'
|
||||
require 'web_site/sitemap'
|
||||
require 'web_site/sql_file_export'
|
||||
|
||||
class WebSite
|
||||
include WebSite::RobotsTxt
|
||||
include WebSite::HumansTxt
|
||||
include WebSite::InterestingHeaders
|
||||
include WebSite::RobotsTxt
|
||||
include WebSite::SecurityTxt
|
||||
include WebSite::Sitemap
|
||||
include WebSite::SqlFileExport
|
||||
|
||||
attr_reader :uri
|
||||
|
||||
@@ -121,13 +129,6 @@ class WebSite
|
||||
@error_404_hash
|
||||
end
|
||||
|
||||
# Will try to find the rss url in the homepage
|
||||
# Only the first one found is returned
|
||||
def rss_url
|
||||
homepage_body = Browser.get(@uri.to_s).body
|
||||
homepage_body[%r{<link .* type="application/rss\+xml" .* href="([^"]+)" />}, 1]
|
||||
end
|
||||
|
||||
# Only the first 700 bytes are checked to avoid the download
|
||||
# of the whole file which can be very huge (like 2 Go)
|
||||
#
|
||||
|
||||
13
lib/wpscan/web_site/humans_txt.rb
Normal file
13
lib/wpscan/web_site/humans_txt.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module HumansTxt
|
||||
|
||||
# Gets the humans.txt URL
|
||||
# @return [ String ]
|
||||
def humans_url
|
||||
@uri.clone.merge('humans.txt').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -18,49 +18,53 @@ class WebSite
|
||||
# Parse robots.txt
|
||||
# @return [ Array ] URLs generated from robots.txt
|
||||
def parse_robots_txt
|
||||
return unless has_robots?
|
||||
|
||||
return_object = []
|
||||
|
||||
# Make request
|
||||
response = Browser.get(robots_url.to_s)
|
||||
body = response.body
|
||||
|
||||
# Get all allow and disallow urls
|
||||
entries = body.scan(/^(?:dis)?allow:\s*(.*)$/i)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
entries.flatten!
|
||||
entries.compact.sort!
|
||||
entries.uniq!
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
|
||||
# Sort
|
||||
entries.sort!
|
||||
|
||||
# Wordpress URL
|
||||
wordpress_path = @uri.path
|
||||
|
||||
# Each "boring" value as defined below, remove
|
||||
RobotsTxt.known_dirs.each do |d|
|
||||
entries.delete(d)
|
||||
# also delete when wordpress is installed in subdir
|
||||
# Also delete when wordpress is installed in subdir
|
||||
dir_with_subdir = "#{wordpress_path}/#{d}".gsub(/\/+/, '/')
|
||||
entries.delete(dir_with_subdir)
|
||||
end
|
||||
|
||||
entries.each do |d|
|
||||
begin
|
||||
temp = @uri.clone
|
||||
temp.path = d.strip
|
||||
rescue URI::Error
|
||||
temp = d.strip
|
||||
end
|
||||
return_object << temp.to_s
|
||||
end
|
||||
# Convert to full URIs
|
||||
return_object = full_uri(entries)
|
||||
end
|
||||
return_object
|
||||
return return_object
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Useful ~ "function do_robots()" -> https://github.com/WordPress/WordPress/blob/master/wp-includes/functions.php
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.known_dirs
|
||||
%w{
|
||||
/
|
||||
/wp-admin/
|
||||
/wp-admin/admin-ajax.php
|
||||
/wp-includes/
|
||||
/wp-content/
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
13
lib/wpscan/web_site/security_txt.rb
Normal file
13
lib/wpscan/web_site/security_txt.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module SecurityTxt
|
||||
|
||||
# Gets the security.txt URL
|
||||
# @return [ String ]
|
||||
def security_url
|
||||
@uri.clone.merge('.well-known/security.txt').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
53
lib/wpscan/web_site/sitemap.rb
Normal file
53
lib/wpscan/web_site/sitemap.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module Sitemap
|
||||
|
||||
# Checks if a sitemap.txt file exists
|
||||
# @return [ Boolean ]
|
||||
def has_sitemap?
|
||||
# Make the request
|
||||
response = Browser.get(sitemap_url)
|
||||
|
||||
# Make sure its HTTP 200
|
||||
return false unless response.code == 200
|
||||
|
||||
# Is there a sitemap value?
|
||||
result = response.body.scan(/^sitemap\s*:\s*(.*)$/i)
|
||||
return true if result[0]
|
||||
return false
|
||||
end
|
||||
|
||||
# Get the robots.txt URL
|
||||
# @return [ String ]
|
||||
def sitemap_url
|
||||
@uri.clone.merge('robots.txt').to_s
|
||||
end
|
||||
|
||||
# Parse robots.txt
|
||||
# @return [ Array ] URLs generated from robots.txt
|
||||
def parse_sitemap
|
||||
return_object = []
|
||||
|
||||
# Make request
|
||||
response = Browser.get(sitemap_url.to_s)
|
||||
|
||||
# Get all allow and disallow urls
|
||||
entries = response.body.scan(/^sitemap\s*:\s*(.*)$/i)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
|
||||
# Sort
|
||||
entries.sort!
|
||||
|
||||
# Convert to full URIs
|
||||
return_object = full_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
35
lib/wpscan/web_site/sql_file_export.rb
Normal file
35
lib/wpscan/web_site/sql_file_export.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module SqlFileExport
|
||||
|
||||
# Checks if a .sql file exists
|
||||
# @return [ Array ]
|
||||
def sql_file_export
|
||||
export_files = []
|
||||
|
||||
self.sql_file_export_urls.each do |url|
|
||||
response = Browser.get(url)
|
||||
export_files << url if response.code == 200 && response.body =~ /INSERT INTO/
|
||||
end
|
||||
|
||||
export_files
|
||||
end
|
||||
|
||||
# Gets a .sql export file URL
|
||||
# @return [ Array ]
|
||||
def sql_file_export_urls
|
||||
urls = []
|
||||
host = @uri.host[/(^[\w|-]+)/,1]
|
||||
|
||||
files = ["#{host}.sql", "#{host}.sql.gz", "#{host}.zip", 'db.sql', 'site.sql', 'database.sql',
|
||||
'data.sql', 'backup.sql', 'dump.sql', 'db_backup.sql', 'dbdump.sql', 'wordpress.sql', 'mysql.sql']
|
||||
|
||||
files.each do |file|
|
||||
urls << @uri.clone.merge(file).to_s
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +1,26 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'web_site'
|
||||
require 'wp_target/wp_readme'
|
||||
require 'wp_target/wp_registrable'
|
||||
require 'wp_target/wp_api'
|
||||
require 'wp_target/wp_config_backup'
|
||||
require 'wp_target/wp_must_use_plugins'
|
||||
require 'wp_target/wp_login_protection'
|
||||
require 'wp_target/wp_custom_directories'
|
||||
require 'wp_target/wp_full_path_disclosure'
|
||||
require 'wp_target/wp_login_protection'
|
||||
require 'wp_target/wp_must_use_plugins'
|
||||
require 'wp_target/wp_readme'
|
||||
require 'wp_target/wp_registrable'
|
||||
require 'wp_target/wp_rss'
|
||||
|
||||
class WpTarget < WebSite
|
||||
include WpTarget::WpReadme
|
||||
include WpTarget::WpRegistrable
|
||||
include WpTarget::WpAPI
|
||||
include WpTarget::WpConfigBackup
|
||||
include WpTarget::WpMustUsePlugins
|
||||
include WpTarget::WpLoginProtection
|
||||
include WpTarget::WpCustomDirectories
|
||||
include WpTarget::WpFullPathDisclosure
|
||||
include WpTarget::WpLoginProtection
|
||||
include WpTarget::WpMustUsePlugins
|
||||
include WpTarget::WpReadme
|
||||
include WpTarget::WpRegistrable
|
||||
include WpTarget::WpRSS
|
||||
|
||||
attr_reader :verbose
|
||||
|
||||
|
||||
86
lib/wpscan/wp_target/wp_api.rb
Normal file
86
lib/wpscan/wp_target/wp_api.rb
Normal file
@@ -0,0 +1,86 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpAPI
|
||||
|
||||
# Checks to see if the REST API is enabled
|
||||
#
|
||||
# This by default in a WordPress installation since 4.5+
|
||||
# @return [ Boolean ]
|
||||
def has_api?(url)
|
||||
# Make the request
|
||||
response = Browser.get(url)
|
||||
|
||||
# Able to view the output?
|
||||
if valid_json?(response.body) && response.body != ''
|
||||
# Read in JSON
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
# If there is nothing there, return false
|
||||
if data.empty?
|
||||
return false
|
||||
# WAF/API disabled response
|
||||
elsif data.include?('message') and data['message'] =~ /Only authenticated users can access the REST API/
|
||||
return false
|
||||
# Success!
|
||||
elsif response.code == 200
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# Something went wrong
|
||||
return false
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL
|
||||
def json_url
|
||||
@uri.merge('/wp-json/').to_s
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL to show users
|
||||
def json_users_url
|
||||
@uri.merge('/wp-json/wp/v2/users').to_s
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL to show users
|
||||
def json_get_users(url)
|
||||
# Variables
|
||||
users = []
|
||||
|
||||
# Make the request
|
||||
response = Browser.get(url)
|
||||
|
||||
# If not HTTP 200, return false
|
||||
return false unless response.code == 200
|
||||
|
||||
# Able to view the output?
|
||||
return false unless valid_json?(response.body)
|
||||
|
||||
# Read in JSON
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
# If there is nothing there, return false
|
||||
return false if data.empty?
|
||||
|
||||
# Add to array
|
||||
data.each do |child|
|
||||
row = [ child['id'], child['name'], child['link'] ]
|
||||
users << row
|
||||
end
|
||||
|
||||
# Sort and uniq
|
||||
users = users.sort.uniq
|
||||
|
||||
if users and users.size >= 1
|
||||
# Feedback
|
||||
grammar = grammar_s(users.size)
|
||||
puts warning("#{users.size} user#{grammar} exposed via API: #{json_users_url}")
|
||||
|
||||
# Print results
|
||||
table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'],
|
||||
rows: users)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
73
lib/wpscan/wp_target/wp_rss.rb
Normal file
73
lib/wpscan/wp_target/wp_rss.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpRSS
|
||||
|
||||
# Checks to see if there is an rss feed
|
||||
# Will try to find the rss url in the homepage
|
||||
# Only the first one found is returned
|
||||
#
|
||||
# This file comes by default in a WordPress installation
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def rss_url
|
||||
homepage_body = Browser.get(@uri.to_s).body
|
||||
# Format: <link rel="alternate" type="application/rss+xml" title=".*" href=".*" />
|
||||
homepage_body[%r{<link\s*.*\s*type=['|"]application\/rss\+xml['|"]\s*.*\stitle=".*" href=['|"]([^"]+)['|"]\s*\/?>}i, 1]
|
||||
end
|
||||
|
||||
|
||||
# Gets all the authors from the RSS feed
|
||||
#
|
||||
# @return [ string ]
|
||||
def rss_authors(url)
|
||||
# Variables
|
||||
users = []
|
||||
|
||||
# Make the request
|
||||
response = Browser.get(url, followlocation: true)
|
||||
|
||||
# Valid repose to view? HTTP 200?
|
||||
return false unless response.code == 200
|
||||
|
||||
# Get output
|
||||
data = response.body
|
||||
|
||||
# If there is nothing there, return false
|
||||
return false if data.empty?
|
||||
|
||||
begin
|
||||
# Read in RSS/XML
|
||||
xml = Nokogiri::XML(data)
|
||||
rescue
|
||||
puts critical("Missformed XML")
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
# Look for <dc:creator> item
|
||||
xml.xpath('//item/dc:creator').each do |node|
|
||||
#Format: <dc:creator><![CDATA[.*]]></dc:creator>
|
||||
users << [%r{.*}i.match(node).to_s]
|
||||
end
|
||||
rescue
|
||||
puts critical("Missing Author field. Maybe non-standard WordPress RSS feed?")
|
||||
return false
|
||||
end
|
||||
|
||||
# Sort and uniq
|
||||
users = users.sort_by { |user| user.to_s.downcase }.uniq
|
||||
|
||||
if users and users.size >= 1
|
||||
# Feedback
|
||||
grammar = grammar_s(users.size)
|
||||
puts warning("Detected #{users.size} user#{grammar} from RSS feed:")
|
||||
|
||||
# Print results
|
||||
table = Terminal::Table.new(headings: ['Name'],
|
||||
rows: users)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -28,9 +28,12 @@ def usage
|
||||
puts '-Enumerate installed themes ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate t"
|
||||
puts
|
||||
puts '-Enumerate users ...'
|
||||
puts '-Enumerate users (from 1 - 10)...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate u"
|
||||
puts
|
||||
puts '-Enumerate users (from 1 - 20)...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate u[1-20]"
|
||||
puts
|
||||
puts '-Enumerate installed timthumbs ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate tt"
|
||||
puts
|
||||
@@ -46,7 +49,7 @@ def usage
|
||||
puts '-Use custom plugins directory ...'
|
||||
puts "ruby #{script_name} -u www.example.com --wp-plugins-dir wp-content/custom-plugins"
|
||||
puts
|
||||
puts '-Update the DB ...'
|
||||
puts '-Update the Database ...'
|
||||
puts "ruby #{script_name} --update"
|
||||
puts
|
||||
puts '-Debug output ...'
|
||||
@@ -110,7 +113,6 @@ def help
|
||||
puts '--request-timeout <request-timeout> Request Timeout.'
|
||||
puts '--connect-timeout <connect-timeout> Connect Timeout.'
|
||||
puts '--threads | -t <number of threads> The number of threads to use when multi-threading requests.'
|
||||
puts '--max-threads <max-threads> Maximum Threads.'
|
||||
puts '--throttle <milliseconds> Milliseconds to wait before doing another web request. If used, the --threads should be set to 1.'
|
||||
puts '--help | -h This help screen.'
|
||||
puts '--verbose | -v Verbose output.'
|
||||
@@ -118,6 +120,58 @@ def help
|
||||
puts
|
||||
end
|
||||
|
||||
|
||||
def clean_uri(entries)
|
||||
# Extract elements
|
||||
entries.flatten!
|
||||
# Remove any leading/trailing spaces
|
||||
entries.collect{|x| x.strip || x }
|
||||
# End Of Line issues
|
||||
entries.collect{|x| x.chomp! || x }
|
||||
# Remove nil's
|
||||
entries.compact
|
||||
# Unique values only
|
||||
entries.uniq!
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
# Return the full URL
|
||||
def full_uri(entries)
|
||||
return_object = []
|
||||
# Each value now, try and make it a full URL
|
||||
entries.each do |d|
|
||||
begin
|
||||
temp = @uri.clone
|
||||
temp.path = d.strip
|
||||
rescue URI::Error
|
||||
temp = d.strip
|
||||
end
|
||||
return_object << temp.to_s
|
||||
end
|
||||
|
||||
return return_object
|
||||
end
|
||||
|
||||
# Parse humans.txt
|
||||
# @return [ Array ] URLs generated from humans.txt
|
||||
def parse_txt(url)
|
||||
return_object = []
|
||||
response = Browser.get(url.to_s)
|
||||
body = response.body
|
||||
|
||||
# Get all non-comments
|
||||
entries = body.split(/\n/)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
|
||||
# Hook to check if the target if down during the scan
|
||||
# And have the number of requests performed to display at the end of the scan
|
||||
# The target is considered down after 30 requests with status = 0
|
||||
@@ -136,3 +190,4 @@ Typhoeus.on_complete do |response|
|
||||
|
||||
sleep(Browser.instance.throttle)
|
||||
end
|
||||
|
||||
|
||||
@@ -277,7 +277,6 @@ class WpscanOptions
|
||||
['--cache-ttl', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--request-timeout', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--connect-timeout', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--max-threads', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--batch', GetoptLong::NO_ARGUMENT],
|
||||
['--no-color', GetoptLong::NO_ARGUMENT],
|
||||
['--cookie', GetoptLong::REQUIRED_ARGUMENT],
|
||||
|
||||
@@ -168,7 +168,7 @@ describe Browser do
|
||||
it 'sets the proxy_auth' do
|
||||
browser.proxy = proxy
|
||||
browser.proxy_auth = 'user:pass'
|
||||
@expected = proxy_expectation.merge(proxyauth: 'user:pass')
|
||||
@expected = proxy_expectation.merge(proxyuserpwd: 'user:pass')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,9 +45,18 @@ describe 'WpTheme::Findable' do
|
||||
|
||||
# FIXME: the style_url should be checked in WpTheme for absolute / relative
|
||||
context 'when relative url is used' do
|
||||
it 'returns the WpTheme' do
|
||||
@file = 'relative_urls.html'
|
||||
@expected = WpTheme.new(uri, name: 'theme_name')
|
||||
context 'when leading slash' do
|
||||
it 'returns the WpTheme' do
|
||||
@file = 'relative_urls.html'
|
||||
@expected = WpTheme.new(uri, name: 'theme_name')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no leading slash' do
|
||||
it 'returns the WpTheme' do
|
||||
@file = 'relative_urls_missing_slash.html'
|
||||
@expected = WpTheme.new(uri, name: 'theme_name')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -134,6 +134,13 @@ describe 'WpVersion::Findable' do
|
||||
@fixture = '/3.3.2.html'
|
||||
@expected = '3.3.2'
|
||||
end
|
||||
|
||||
context 'when version >= 4.7' do
|
||||
it 'returns nil' do
|
||||
@fixture = '/4.7.2.html'
|
||||
@expected = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '::find_from_links_opml' do
|
||||
|
||||
@@ -207,18 +207,6 @@ describe 'WebSite' do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#rss_url' do
|
||||
it 'returns nil if the url is not found' do
|
||||
stub_request(:get, web_site.url).to_return(body: 'No RSS link in this body !')
|
||||
expect(web_site.rss_url).to be_nil
|
||||
end
|
||||
|
||||
it "returns 'http://lamp-wp/wordpress-3.5/?feed=rss2'" do
|
||||
stub_request_to_fixture(url: web_site.url, fixture: fixtures_dir + '/rss_url/wordpress-3.5.htm')
|
||||
expect(web_site.rss_url).to be === 'http://lamp-wp/wordpress-3.5/?feed=rss2'
|
||||
end
|
||||
end
|
||||
|
||||
describe '::has_log?' do
|
||||
let(:log_url) { web_site.uri.merge('log.txt').to_s }
|
||||
let(:pattern) { %r{PHP Fatal error} }
|
||||
|
||||
@@ -1 +1 @@
|
||||
<html><head><title>Test</title><link rel="stylesheet" type="text/css" href="/wp-content/themes/theme_name/style.css" /></head><body></body></html>
|
||||
<html><head><title>Test</title><link rel="stylesheet" type="text/css" href="/wp-content/themes/theme_name/style.css" /></head><body></body></html>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<html><head><title>Test</title><link rel="stylesheet" type="text/css" href="wp-content/themes/theme_name/style.css" /></head><body></body></html>
|
||||
@@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<title>WordPress › ReadMe</title>
|
||||
<link rel="stylesheet" href="wp-admin/css/install.css?ver=20100228" type="text/css" />
|
||||
</head>
|
||||
<body>
|
||||
<h1 id="logo">
|
||||
<a href="https://wordpress.org/"><img alt="WordPress" src="wp-admin/images/wordpress-logo.png" /></a>
|
||||
<br /> Version 4.7
|
||||
</h1>
|
||||
<p style="text-align: center">Semantic Personal Publishing Platform</p>
|
||||
|
||||
<h2>First Things First</h2>
|
||||
<p>Welcome. WordPress is a very special project to me. Every developer and contributor adds something unique to the mix, and together we create something beautiful that I’m proud to be a part of. Thousands of hours have gone into WordPress, and we’re dedicated to making it better every day. Thank you for making it part of your world.</p>
|
||||
<p style="text-align: right">— Matt Mullenweg</p>
|
||||
|
||||
<h2>Installation: Famous 5-minute install</h2>
|
||||
<ol>
|
||||
<li>Unzip the package in an empty directory and upload everything.</li>
|
||||
<li>Open <span class="file"><a href="wp-admin/install.php">wp-admin/install.php</a></span> in your browser. It will take you through the process to set up a <code>wp-config.php</code> file with your database connection details.
|
||||
<ol>
|
||||
<li>If for some reason this doesn’t work, don’t worry. It doesn’t work on all web hosts. Open up <code>wp-config-sample.php</code> with a text editor like WordPad or similar and fill in your database connection details.</li>
|
||||
<li>Save the file as <code>wp-config.php</code> and upload it.</li>
|
||||
<li>Open <span class="file"><a href="wp-admin/install.php">wp-admin/install.php</a></span> in your browser.</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li>Once the configuration file is set up, the installer will set up the tables needed for your blog. If there is an error, double check your <code>wp-config.php</code> file, and try again. If it fails again, please go to the <a href="https://wordpress.org/support/" title="WordPress support">support forums</a> with as much data as you can gather.</li>
|
||||
<li><strong>If you did not enter a password, note the password given to you.</strong> If you did not provide a username, it will be <code>admin</code>.</li>
|
||||
<li>The installer should then send you to the <a href="wp-login.php">login page</a>. Sign in with the username and password you chose during the installation. If a password was generated for you, you can then click on “Profile” to change the password.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Updating</h2>
|
||||
<h3>Using the Automatic Updater</h3>
|
||||
<p>If you are updating from version 2.7 or higher, you can use the automatic updater:</p>
|
||||
<ol>
|
||||
<li>Open <span class="file"><a href="wp-admin/update-core.php">wp-admin/update-core.php</a></span> in your browser and follow the instructions.</li>
|
||||
<li>You wanted more, perhaps? That’s it!</li>
|
||||
</ol>
|
||||
|
||||
<h3>Updating Manually</h3>
|
||||
<ol>
|
||||
<li>Before you update anything, make sure you have backup copies of any files you may have modified such as <code>index.php</code>.</li>
|
||||
<li>Delete your old WordPress files, saving ones you’ve modified.</li>
|
||||
<li>Upload the new files.</li>
|
||||
<li>Point your browser to <span class="file"><a href="wp-admin/upgrade.php">/wp-admin/upgrade.php</a>.</span></li>
|
||||
</ol>
|
||||
|
||||
<h2>Migrating from other systems</h2>
|
||||
<p>WordPress can <a href="https://codex.wordpress.org/Importing_Content">import from a number of systems</a>. First you need to get WordPress installed and working as described above, before using <a href="wp-admin/import.php" title="Import to WordPress">our import tools</a>.</p>
|
||||
|
||||
<h2>System Requirements</h2>
|
||||
<ul>
|
||||
<li><a href="https://secure.php.net/">PHP</a> version <strong>5.2.4</strong> or higher.</li>
|
||||
<li><a href="https://www.mysql.com/">MySQL</a> version <strong>5.0</strong> or higher.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Recommendations</h3>
|
||||
<ul>
|
||||
<li><a href="https://secure.php.net/">PHP</a> version <strong>7</strong> or higher.</li>
|
||||
<li><a href="https://www.mysql.com/">MySQL</a> version <strong>5.6</strong> or higher.</li>
|
||||
<li>The <a href="https://httpd.apache.org/docs/2.2/mod/mod_rewrite.html">mod_rewrite</a> Apache module.</li>
|
||||
<li><a href="https://wordpress.org/news/2016/12/moving-toward-ssl/">HTTPS</a> support.</li>
|
||||
<li>A link to <a href="https://wordpress.org/">wordpress.org</a> on your site.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Online Resources</h2>
|
||||
<p>If you have any questions that aren’t addressed in this document, please take advantage of WordPress’ numerous online resources:</p>
|
||||
<dl>
|
||||
<dt><a href="https://codex.wordpress.org/">The WordPress Codex</a></dt>
|
||||
<dd>The Codex is the encyclopedia of all things WordPress. It is the most comprehensive source of information for WordPress available.</dd>
|
||||
<dt><a href="https://wordpress.org/news/">The WordPress Blog</a></dt>
|
||||
<dd>This is where you’ll find the latest updates and news related to WordPress. Recent WordPress news appears in your administrative dashboard by default.</dd>
|
||||
<dt><a href="https://planet.wordpress.org/">WordPress Planet</a></dt>
|
||||
<dd>The WordPress Planet is a news aggregator that brings together posts from WordPress blogs around the web.</dd>
|
||||
<dt><a href="https://wordpress.org/support/">WordPress Support Forums</a></dt>
|
||||
<dd>If you’ve looked everywhere and still can’t find an answer, the support forums are very active and have a large community ready to help. To help them help you be sure to use a descriptive thread title and describe your question in as much detail as possible.</dd>
|
||||
<dt><a href="https://codex.wordpress.org/IRC">WordPress <abbr title="Internet Relay Chat">IRC</abbr> Channel</a></dt>
|
||||
<dd>There is an online chat channel that is used for discussion among people who use WordPress and occasionally support topics. The above wiki page should point you in the right direction. (<a href="irc://irc.freenode.net/wordpress">irc.freenode.net #wordpress</a>)</dd>
|
||||
</dl>
|
||||
|
||||
<h2>Final Notes</h2>
|
||||
<ul>
|
||||
<li>If you have any suggestions, ideas, or comments, or if you (gasp!) found a bug, join us in the <a href="https://wordpress.org/support/">Support Forums</a>.</li>
|
||||
<li>WordPress has a robust plugin <abbr title="application programming interface">API</abbr> that makes extending the code easy. If you are a developer interested in utilizing this, see the <a href="https://developer.wordpress.org/plugins/">Plugin Developer Handbook</a>. You shouldn’t modify any of the core code.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Share the Love</h2>
|
||||
<p>WordPress has no multi-million dollar marketing campaign or celebrity sponsors, but we do have something even better—you. If you enjoy WordPress please consider telling a friend, setting it up for someone less knowledgable than yourself, or writing the author of a media article that overlooks us.</p>
|
||||
|
||||
<p>WordPress is the official continuation of <a href="http://cafelog.com/">b2/cafélog</a>, which came from Michel V. The work has been continued by the <a href="https://wordpress.org/about/">WordPress developers</a>. If you would like to support WordPress, please consider <a href="https://wordpress.org/donate/" title="Donate to WordPress">donating</a>.</p>
|
||||
|
||||
<h2>License</h2>
|
||||
<p>WordPress is free software, and is released under the terms of the <abbr title="GNU General Public License">GPL</abbr> version 2 or (at your option) any later version. See <a href="license.txt">license.txt</a>.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
108
spec/shared_examples/web_site/humans_txt.rb
Normal file
108
spec/shared_examples/web_site/humans_txt.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
shared_examples 'WebSite::HumansTxt' do
|
||||
let(:known_dirs) { WebSite::HumansTxt.known_dirs }
|
||||
|
||||
describe '#humans_url' do
|
||||
it 'returns the correct url' do
|
||||
expect(web_site.humans_url).to eql 'http://example.localhost/humans.txt'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_humans?' do
|
||||
it 'returns true' do
|
||||
stub_request(:get, web_site.humans_url).to_return(status: 200)
|
||||
expect(web_site.has_humans?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
stub_request(:get, web_site.humans_url).to_return(status: 404)
|
||||
expect(web_site.has_humans?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_humans_txt' do
|
||||
|
||||
context 'installed in root' do
|
||||
after :each do
|
||||
stub_request_to_fixture(url: web_site.humans_url, fixture: @fixture)
|
||||
humans = web_site.parse_humans_txt
|
||||
expect(humans).to match_array @expected
|
||||
end
|
||||
|
||||
it 'returns an empty Array (empty humans.txt)' do
|
||||
@fixture = fixtures_dir + '/humans_txt/empty_humans.txt'
|
||||
@expected = []
|
||||
end
|
||||
|
||||
it 'returns an empty Array (invalid humans.txt)' do
|
||||
@fixture = fixtures_dir + '/humans_txt/invalid_humans.txt'
|
||||
@expected = []
|
||||
end
|
||||
|
||||
it 'returns some urls and some strings' do
|
||||
@fixture = fixtures_dir + '/humans_txt/invalid_humans_2.txt'
|
||||
@expected = %w(
|
||||
/ÖÜ()=?
|
||||
http://10.0.0.0/wp-includes/
|
||||
http://example.localhost/asdf/
|
||||
wooooza
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns an Array of urls (valid humans.txt)' do
|
||||
@fixture = fixtures_dir + '/humans_txt/humans.txt'
|
||||
@expected = %w(
|
||||
http://example.localhost/wordpress/admin/
|
||||
http://example.localhost/wordpress/wp-admin/
|
||||
http://example.localhost/wordpress/secret/
|
||||
http://example.localhost/Wordpress/wp-admin/
|
||||
http://example.localhost/wp-admin/tralling-space/
|
||||
http://example.localhost/asdf/
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes duplicate entries from humans.txt test 1' do
|
||||
@fixture = fixtures_dir + '/humans_txt/humans_duplicate_1.txt'
|
||||
@expected = %w(
|
||||
http://example.localhost/wordpress/
|
||||
http://example.localhost/wordpress/admin/
|
||||
http://example.localhost/wordpress/wp-admin/
|
||||
http://example.localhost/wordpress/secret/
|
||||
http://example.localhost/Wordpress/wp-admin/
|
||||
http://example.localhost/wp-admin/tralling-space/
|
||||
http://example.localhost/asdf/
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes duplicate entries from humans.txt test 2' do
|
||||
@fixture = fixtures_dir + '/humans_txt/humans_duplicate_2.txt'
|
||||
@expected = nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'installed in sub directory' do
|
||||
it 'returns an Array of urls (valid humans.txt, WP installed in subdir)' do
|
||||
web_site_sub = WebSite.new('http://example.localhost/wordpress/')
|
||||
fixture = fixtures_dir + '/humans_txt/humans.txt'
|
||||
expected = %w(
|
||||
http://example.localhost/wordpress/admin/
|
||||
http://example.localhost/wordpress/secret/
|
||||
http://example.localhost/Wordpress/wp-admin/
|
||||
http://example.localhost/wp-admin/tralling-space/
|
||||
http://example.localhost/asdf/
|
||||
)
|
||||
stub_request_to_fixture(url: web_site_sub.humans_url, fixture: fixture)
|
||||
humans = web_site_sub.parse_humans_txt
|
||||
expect(humans).to match_array expected
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#known_dirs' do
|
||||
it 'does not contain duplicates' do
|
||||
expect(known_dirs.flatten.uniq.length).to eq known_dirs.length
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
108
spec/shared_examples/web_site/security_txt.rb
Normal file
108
spec/shared_examples/web_site/security_txt.rb
Normal file
@@ -0,0 +1,108 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
shared_examples 'WebSite::SecurityTxt' do
|
||||
let(:known_dirs) { WebSite::SecurityTxt.known_dirs }
|
||||
|
||||
describe '#security_url' do
|
||||
it 'returns the correct url' do
|
||||
expect(web_site.security_url).to eql 'http://example.localhost/security.txt'
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_security?' do
|
||||
it 'returns true' do
|
||||
stub_request(:get, web_site.security_url).to_return(status: 200)
|
||||
expect(web_site.has_security?).to be_truthy
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
stub_request(:get, web_site.security_url).to_return(status: 404)
|
||||
expect(web_site.has_security?).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
describe '#parse_security_txt' do
|
||||
|
||||
context 'installed in root' do
|
||||
after :each do
|
||||
stub_request_to_fixture(url: web_site.security_url, fixture: @fixture)
|
||||
security = web_site.parse_security_txt
|
||||
expect(security).to match_array @expected
|
||||
end
|
||||
|
||||
it 'returns an empty Array (empty security.txt)' do
|
||||
@fixture = fixtures_dir + '/security_txt/empty_security.txt'
|
||||
@expected = []
|
||||
end
|
||||
|
||||
it 'returns an empty Array (invalid security.txt)' do
|
||||
@fixture = fixtures_dir + '/security_txt/invalid_security.txt'
|
||||
@expected = []
|
||||
end
|
||||
|
||||
it 'returns some urls and some strings' do
|
||||
@fixture = fixtures_dir + '/security_txt/invalid_security_2.txt'
|
||||
@expected = %w(
|
||||
/ÖÜ()=?
|
||||
http://10.0.0.0/wp-includes/
|
||||
http://example.localhost/asdf/
|
||||
wooooza
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns an Array of urls (valid security.txt)' do
|
||||
@fixture = fixtures_dir + '/security_txt/security.txt'
|
||||
@expected = %w(
|
||||
http://example.localhost/wordpress/admin/
|
||||
http://example.localhost/wordpress/wp-admin/
|
||||
http://example.localhost/wordpress/secret/
|
||||
http://example.localhost/Wordpress/wp-admin/
|
||||
http://example.localhost/wp-admin/tralling-space/
|
||||
http://example.localhost/asdf/
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes duplicate entries from security.txt test 1' do
|
||||
@fixture = fixtures_dir + '/security_txt/security_duplicate_1.txt'
|
||||
@expected = %w(
|
||||
http://example.localhost/wordpress/
|
||||
http://example.localhost/wordpress/admin/
|
||||
http://example.localhost/wordpress/wp-admin/
|
||||
http://example.localhost/wordpress/secret/
|
||||
http://example.localhost/Wordpress/wp-admin/
|
||||
http://example.localhost/wp-admin/tralling-space/
|
||||
http://example.localhost/asdf/
|
||||
)
|
||||
end
|
||||
|
||||
it 'removes duplicate entries from security.txt test 2' do
|
||||
@fixture = fixtures_dir + '/security_txt/security_duplicate_2.txt'
|
||||
@expected = nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'installed in sub directory' do
|
||||
it 'returns an Array of urls (valid security.txt, WP installed in subdir)' do
|
||||
web_site_sub = WebSite.new('http://example.localhost/wordpress/')
|
||||
fixture = fixtures_dir + '/security_txt/security.txt'
|
||||
expected = %w(
|
||||
http://example.localhost/wordpress/admin/
|
||||
http://example.localhost/wordpress/secret/
|
||||
http://example.localhost/Wordpress/wp-admin/
|
||||
http://example.localhost/wp-admin/tralling-space/
|
||||
http://example.localhost/asdf/
|
||||
)
|
||||
stub_request_to_fixture(url: web_site_sub.security_url, fixture: fixture)
|
||||
security = web_site_sub.parse_security_txt
|
||||
expect(security).to match_array expected
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#known_dirs' do
|
||||
it 'does not contain duplicates' do
|
||||
expect(known_dirs.flatten.uniq.length).to eq known_dirs.length
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -16,7 +16,7 @@ shared_examples 'WpItem::Infos' do
|
||||
end
|
||||
|
||||
context 'when the file exists' do
|
||||
%w{readme.txt README.TXT}.each do |readme|
|
||||
%w{readme.txt readme.md}.each do |readme|
|
||||
it 'returns the correct url' do
|
||||
url = uri.merge(readme).to_s
|
||||
@expected = url
|
||||
@@ -48,25 +48,42 @@ shared_examples 'WpItem::Infos' do
|
||||
end
|
||||
|
||||
describe '#changelog_url' do
|
||||
it 'returns the correct url' do
|
||||
expect(subject.changelog_url).to eq changelog_url
|
||||
after { expect(subject.changelog_url).to eql @expected }
|
||||
|
||||
it 'returns nil' do
|
||||
stub_request(:get, /.*/).to_return(status: 404)
|
||||
@expected = nil
|
||||
end
|
||||
|
||||
context 'when the file exists' do
|
||||
%w{changelog.txt CHANGELOG.md}.each do |changelog|
|
||||
it 'returns the correct url' do
|
||||
url = uri.merge(changelog).to_s
|
||||
@expected = url
|
||||
|
||||
stub_request(:get, %r{^(?!#{url})}).to_return(status: 404)
|
||||
stub_request(:get, url).to_return(status: 200)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_changelog?' do
|
||||
after :each do
|
||||
stub_request(:get, subject.changelog_url).to_return(status: @status)
|
||||
after do
|
||||
allow(subject).to receive_messages(changelog_url: @stub)
|
||||
expect(subject.has_changelog?).to eql @expected
|
||||
end
|
||||
|
||||
it 'returns true on a 200' do
|
||||
@status = 200
|
||||
@expected = true
|
||||
context 'when changelog_url is nil'
|
||||
it 'returns false' do
|
||||
@stub = nil
|
||||
@expected = false
|
||||
end
|
||||
|
||||
it 'returns false otherwise' do
|
||||
@status = 404
|
||||
@expected = false
|
||||
context 'when changelog_url is not nil'
|
||||
it 'returns true' do
|
||||
@stub = uri.merge('changelog.txt').to_s
|
||||
@expected = true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ shared_examples 'WpItem::Vulnerable' do
|
||||
let(:empty_file) { MODELS_FIXTURES + '/wp_item/vulnerable/empty.json' }
|
||||
|
||||
before do
|
||||
stub_request(:get, /.*\/readme\.txt/i)
|
||||
stub_request(:get, /.*\/readme\.(?:txt|md)/i)
|
||||
stub_request(:get, /.*\/style\.css/i)
|
||||
end
|
||||
|
||||
@@ -94,7 +94,7 @@ shared_examples 'WpItem::Vulnerable' do
|
||||
|
||||
context 'no version found in wp_item' do
|
||||
before do
|
||||
stub_request(:get, /.*\/readme\.txt/i).to_return(status: 404)
|
||||
stub_request(:get, /.*\/readme\.(?:txt|md)/i).to_return(status: 404)
|
||||
stub_request(:get, /.*\/style\.css/i).to_return(status: 404)
|
||||
end
|
||||
|
||||
|
||||
250
wpscan.rb
250
wpscan.rb
@@ -10,6 +10,7 @@ require File.join(__dir__, 'lib', 'wpscan', 'wpscan_helper')
|
||||
def main
|
||||
begin
|
||||
wpscan_options = WpscanOptions.load_from_arguments
|
||||
date = last_update
|
||||
|
||||
$log = wpscan_options.log
|
||||
|
||||
@@ -27,7 +28,7 @@ def main
|
||||
# check if file exists and has a size greater zero
|
||||
if File.exist?($log) && File.size?($log)
|
||||
puts notice("The supplied log file #{$log} already exists. If you continue the new output will be appended.")
|
||||
print '[?] Do you want to continue? [Y]es [N]o, default: [N]'
|
||||
print '[?] Do you want to continue? [Y]es [N]o, default: [N] >'
|
||||
if Readline.readline !~ /^y/i
|
||||
# unset logging so puts will try to log to the file
|
||||
$log = nil
|
||||
@@ -54,6 +55,8 @@ def main
|
||||
unless wpscan_options.has_options?
|
||||
# first parameter only url?
|
||||
if ARGV.length == 1
|
||||
puts
|
||||
puts notice("Please use '-u #{ARGV[0]}' next time")
|
||||
wpscan_options.url = ARGV[0]
|
||||
else
|
||||
usage()
|
||||
@@ -72,8 +75,7 @@ def main
|
||||
|
||||
if wpscan_options.version
|
||||
puts "Current version: #{WPSCAN_VERSION}"
|
||||
date = last_update
|
||||
puts "Last DB update: #{date.strftime('%Y-%m-%d')}" unless date.nil?
|
||||
puts "Last database update: #{date.strftime('%Y-%m-%d')}" unless date.nil?
|
||||
exit(0)
|
||||
end
|
||||
|
||||
@@ -83,28 +85,44 @@ def main
|
||||
wpscan_options.to_h.merge(max_threads: wpscan_options.threads)
|
||||
)
|
||||
|
||||
# check if db file needs upgrade and we are not running in batch mode
|
||||
# also no need to check if the user supplied the --update switch
|
||||
if update_required? && !wpscan_options.batch && !wpscan_options.update
|
||||
puts notice('It seems like you have not updated the database for some time.')
|
||||
print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N]'
|
||||
if (input = Readline.readline) =~ /^y/i
|
||||
# Check if database needs upgrade (if its older than 5 days) and we are not running in --batch mode
|
||||
# Also no need to check if the user supplied the --update switch
|
||||
if update_required? and not wpscan_options.batch and not wpscan_options.update
|
||||
# Banner
|
||||
puts
|
||||
puts notice('It seems like you have not updated the database for some time')
|
||||
puts notice("Last database update: #{date.strftime('%Y-%m-%d')}") unless date.nil?
|
||||
|
||||
# User prompt
|
||||
print '[?] Do you want to update now? [Y]es [N]o [A]bort update, default: [N] > '
|
||||
if (input = Readline.readline) =~ /^a/i
|
||||
puts 'Update aborted'
|
||||
elsif input =~ /^y/i
|
||||
wpscan_options.update = true
|
||||
elsif input =~ /^a/i
|
||||
puts 'Scan aborted'
|
||||
exit(1)
|
||||
else
|
||||
if missing_db_file?
|
||||
puts critical('You can not run a scan without any databases. Extract the data.zip file.')
|
||||
end
|
||||
|
||||
# Is there a database to go on with?
|
||||
if missing_db_files? and not wpscan_options.update
|
||||
# Check for data.zip
|
||||
if has_db_zip?
|
||||
puts notice('Extracting the Database ...')
|
||||
# Extract data.zip
|
||||
extract_db_zip
|
||||
puts notice('Extraction completed')
|
||||
# Missing, so can't go on!
|
||||
else
|
||||
puts critical('You can not run a scan without any databases')
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Should we update?
|
||||
if wpscan_options.update
|
||||
puts notice('Updating the Database ...')
|
||||
DbUpdater.new(DATA_DIR).update(wpscan_options.verbose)
|
||||
puts notice('Update completed.')
|
||||
puts notice('Update completed')
|
||||
|
||||
# Exit program if only option --update is used
|
||||
exit(0) unless wpscan_options.url
|
||||
end
|
||||
@@ -120,12 +138,18 @@ def main
|
||||
end
|
||||
|
||||
if wp_target.ssl_error?
|
||||
raise "The target site returned an SSL/TLS error. You can try again using the --disable-tls-checks option.\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code"
|
||||
raise "The target site returned an SSL/TLS error. You can try again using --disable-tls-checks\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code"
|
||||
end
|
||||
|
||||
# Remote website up?
|
||||
unless wp_target.online?
|
||||
raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking wpscan so you can try the --random-agent parameter."
|
||||
if wpscan_options.user_agent
|
||||
puts info("User-Agent: #{wpscan_options.user_agent}")
|
||||
raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking the user-agent?"
|
||||
else
|
||||
raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking the wpscan user-agent, so you can try --random-agent"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if wpscan_options.proxy
|
||||
@@ -145,7 +169,7 @@ def main
|
||||
puts "Following redirection #{redirection}"
|
||||
else
|
||||
puts notice("The remote host tried to redirect to: #{redirection}")
|
||||
print '[?] Do you want follow the redirection ? [Y]es [N]o [A]bort, default: [N]'
|
||||
print '[?] Do you want follow the redirection ? [Y]es [N]o [A]bort, default: [N] >'
|
||||
end
|
||||
if wpscan_options.follow_redirection || !wpscan_options.batch
|
||||
if wpscan_options.follow_redirection || (input = Readline.readline) =~ /^y/i
|
||||
@@ -174,7 +198,7 @@ def main
|
||||
# Remote website is wordpress?
|
||||
unless wpscan_options.force
|
||||
unless wp_target.wordpress?
|
||||
raise 'The remote website is up, but does not seem to be running WordPress.'
|
||||
raise 'The remote website is up, but does not seem to be running WordPress. If you are sure, use --force'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -196,40 +220,9 @@ def main
|
||||
start_memory = get_memory_usage unless windows?
|
||||
puts info("URL: #{wp_target.url}")
|
||||
puts info("Started: #{start_time.asctime}")
|
||||
puts info("User-Agent: #{wpscan_options.user_agent}") if wpscan_options.verbose and wpscan_options.user_agent
|
||||
puts
|
||||
|
||||
if wp_target.has_robots?
|
||||
puts info("robots.txt available under: '#{wp_target.robots_url}'")
|
||||
|
||||
wp_target.parse_robots_txt.each do |dir|
|
||||
puts info("Interesting entry from robots.txt: #{dir}")
|
||||
end
|
||||
end
|
||||
|
||||
if wp_target.has_readme?
|
||||
puts warning("The WordPress '#{wp_target.readme_url}' file exists exposing a version number")
|
||||
end
|
||||
|
||||
if wp_target.has_full_path_disclosure?
|
||||
puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}")
|
||||
end
|
||||
|
||||
if wp_target.has_debug_log?
|
||||
puts critical("Debug log file found: #{wp_target.debug_log_url}")
|
||||
end
|
||||
|
||||
wp_target.config_backup.each do |file_url|
|
||||
puts critical("A wp-config.php backup file has been found in: '#{file_url}'")
|
||||
end
|
||||
|
||||
if wp_target.search_replace_db_2_exists?
|
||||
puts critical("searchreplacedb2.php has been found in: '#{wp_target.search_replace_db_2_url}'")
|
||||
end
|
||||
|
||||
if wp_target.emergency_exists?
|
||||
puts critical("emergency.php has been found in: '#{wp_target.emergency_url}'")
|
||||
end
|
||||
|
||||
wp_target.interesting_headers.each do |header|
|
||||
output = info('Interesting header: ')
|
||||
|
||||
@@ -242,6 +235,66 @@ def main
|
||||
end
|
||||
end
|
||||
|
||||
if wp_target.has_robots?
|
||||
code = get_http_status(wp_target.robots_url)
|
||||
puts info("robots.txt available under: #{wp_target.robots_url} [HTTP #{code}]")
|
||||
|
||||
wp_target.parse_robots_txt.each do |dir|
|
||||
code = get_http_status(dir)
|
||||
puts info("Interesting entry from robots.txt: #{dir} [HTTP #{code}]")
|
||||
end
|
||||
end
|
||||
|
||||
if wp_target.has_sitemap?
|
||||
code = get_http_status(wp_target.sitemap_url)
|
||||
puts info("Sitemap found: #{wp_target.sitemap_url} [HTTP #{code}]")
|
||||
|
||||
wp_target.parse_sitemap.each do |dir|
|
||||
code = get_http_status(dir)
|
||||
puts info("Sitemap entry: #{dir} [HTTP #{code}]")
|
||||
end
|
||||
end
|
||||
|
||||
code = get_http_status(wp_target.humans_url)
|
||||
if code == 200
|
||||
puts info("humans.txt available under: #{wp_target.humans_url} [HTTP #{code}]")
|
||||
|
||||
parse_txt(wp_target.humans_url).each do |dir|
|
||||
puts info("Entry from humans.txt: #{dir}")
|
||||
end
|
||||
end
|
||||
|
||||
code = get_http_status(wp_target.security_url)
|
||||
if code == 200
|
||||
puts info("security.txt available under: #{wp_target.security_url} [HTTP #{code}]")
|
||||
|
||||
parse_txt(wp_target.security_url).each do |dir|
|
||||
puts info("Entry from security.txt: #{dir}")
|
||||
end
|
||||
end
|
||||
|
||||
unless wp_target.sql_file_export.empty?
|
||||
wp_target.sql_file_export.each do |file|
|
||||
puts critical("SQL export file found: #{file}")
|
||||
end
|
||||
end
|
||||
|
||||
if wp_target.has_debug_log?
|
||||
puts critical("Debug log file found: #{wp_target.debug_log_url}")
|
||||
end
|
||||
|
||||
wp_target.config_backup.each do |file_url|
|
||||
puts critical("A wp-config.php backup file has been found in: #{file_url}")
|
||||
end
|
||||
|
||||
if wp_target.search_replace_db_2_exists?
|
||||
puts critical("searchreplacedb2.php has been found in: #{wp_target.search_replace_db_2_url}")
|
||||
end
|
||||
|
||||
if wp_target.emergency_exists?
|
||||
puts critical("emergency.php has been found in: #{wp_target.emergency_url}")
|
||||
end
|
||||
|
||||
if wp_target.multisite?
|
||||
puts info('This site seems to be a multisite (http://codex.wordpress.org/Glossary#Multisite)')
|
||||
end
|
||||
@@ -250,12 +303,35 @@ def main
|
||||
puts info("This site has 'Must Use Plugins' (http://codex.wordpress.org/Must_Use_Plugins)")
|
||||
end
|
||||
|
||||
if wp_target.registration_enabled?
|
||||
puts warning("Registration is enabled: #{wp_target.registration_url}")
|
||||
if wp_target.has_xml_rpc?
|
||||
code = get_http_status(wp_target.xml_rpc_url)
|
||||
puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url} [HTTP #{code}]")
|
||||
end
|
||||
|
||||
if wp_target.has_xml_rpc?
|
||||
puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}")
|
||||
# Test to see if MAIN API URL gives anything back
|
||||
if wp_target.has_api?(wp_target.json_url)
|
||||
code = get_http_status(wp_target.json_url)
|
||||
puts info("API exposed: #{wp_target.json_url} [HTTP #{code}]")
|
||||
|
||||
# Test to see if USER API URL gives anything back
|
||||
if wp_target.has_api?(wp_target.json_users_url)
|
||||
# Print users from JSON
|
||||
wp_target.json_get_users(wp_target.json_users_url)
|
||||
end
|
||||
end
|
||||
|
||||
# Get RSS
|
||||
rss = wp_target.rss_url
|
||||
if rss
|
||||
code = get_http_status(rss)
|
||||
puts info("Found an RSS Feed: #{rss} [HTTP #{code}]")
|
||||
|
||||
# Print users from RSS feed
|
||||
wp_target.rss_authors(rss)
|
||||
end
|
||||
|
||||
if wp_target.has_full_path_disclosure?
|
||||
puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}")
|
||||
end
|
||||
|
||||
if wp_target.upload_directory_listing_enabled?
|
||||
@@ -270,13 +346,20 @@ def main
|
||||
show_progression: true,
|
||||
exclude_content: wpscan_options.exclude_content_based
|
||||
}
|
||||
|
||||
puts
|
||||
puts info('Enumerating WordPress version ...')
|
||||
if (wp_version = wp_target.version(WP_VERSIONS_FILE))
|
||||
if wp_target.has_readme? && VersionCompare::lesser?(wp_version.identifier, '4.7')
|
||||
puts warning("The WordPress '#{wp_target.readme_url}' file exists exposing a version number")
|
||||
end
|
||||
|
||||
if wp_version = wp_target.version(WP_VERSIONS_FILE)
|
||||
wp_version.output(wpscan_options.verbose)
|
||||
else
|
||||
puts
|
||||
puts notice('WordPress version can not be detected')
|
||||
end
|
||||
|
||||
|
||||
if wp_theme = wp_target.theme
|
||||
puts
|
||||
@@ -295,7 +378,7 @@ def main
|
||||
parent.output(wpscan_options.verbose)
|
||||
wp_theme = parent
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
if wpscan_options.enumerate_plugins == nil and wpscan_options.enumerate_only_vulnerable_plugins == nil
|
||||
@@ -304,14 +387,11 @@ def main
|
||||
|
||||
wp_plugins = WpPlugins.passive_detection(wp_target)
|
||||
if !wp_plugins.empty?
|
||||
if wp_plugins.size == 1
|
||||
puts " | #{wp_plugins.size} plugin found:"
|
||||
else
|
||||
puts " | #{wp_plugins.size} plugins found:"
|
||||
end
|
||||
grammar = grammar_s(wp_plugins.size)
|
||||
puts " | #{wp_plugins.size} plugin#{grammar} found:"
|
||||
wp_plugins.output(wpscan_options.verbose)
|
||||
else
|
||||
puts info('No plugins found')
|
||||
puts info('No plugins found passively')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -343,12 +423,14 @@ def main
|
||||
|
||||
puts
|
||||
if !wp_plugins.empty?
|
||||
puts info("We found #{wp_plugins.size} plugins:")
|
||||
grammar = grammar_s(wp_plugins.size)
|
||||
puts info("We found #{wp_plugins.size} plugin#{grammar}:")
|
||||
|
||||
wp_plugins.output(wpscan_options.verbose)
|
||||
else
|
||||
puts info('No plugins found')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Enumerate installed themes
|
||||
@@ -378,12 +460,14 @@ def main
|
||||
)
|
||||
puts
|
||||
if !wp_themes.empty?
|
||||
puts info("We found #{wp_themes.size} themes:")
|
||||
grammar = grammar_s(wp_themes.size)
|
||||
puts info("We found #{wp_themes.size} theme#{grammar}:")
|
||||
|
||||
wp_themes.output(wpscan_options.verbose)
|
||||
else
|
||||
puts info('No themes found')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
if wpscan_options.enumerate_timthumbs
|
||||
@@ -393,18 +477,20 @@ def main
|
||||
|
||||
wp_timthumbs = WpTimthumbs.aggressive_detection(wp_target,
|
||||
enum_options.merge(
|
||||
file: DATA_DIR + '/timthumbs.txt',
|
||||
file: TIMTHUMBS_FILE,
|
||||
theme_name: wp_theme ? wp_theme.name : nil
|
||||
)
|
||||
)
|
||||
puts
|
||||
if !wp_timthumbs.empty?
|
||||
puts info("We found #{wp_timthumbs.size} timthumb file/s:")
|
||||
grammar = grammar_s(wp_timthumbs.size)
|
||||
puts info("We found #{wp_timthumbs.size} timthumb file#{grammar}:")
|
||||
|
||||
wp_timthumbs.output(wpscan_options.verbose)
|
||||
else
|
||||
puts info('No timthumb files found')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# If we haven't been supplied a username/usernames list, enumerate them...
|
||||
@@ -432,7 +518,8 @@ def main
|
||||
exit(1)
|
||||
end
|
||||
else
|
||||
puts info("Identified the following #{wp_users.size} user/s:")
|
||||
grammar = grammar_s(wp_users.size)
|
||||
puts info("We identified the following #{wp_users.size} user#{grammar}:")
|
||||
wp_users.output(margin_left: ' ' * 4)
|
||||
if wp_users[0].login == "admin"
|
||||
puts warning("Default first WordPress username 'admin' is still used")
|
||||
@@ -442,10 +529,12 @@ def main
|
||||
else
|
||||
wp_users = WpUsers.new
|
||||
|
||||
# Username file?
|
||||
if wpscan_options.usernames
|
||||
File.open(wpscan_options.usernames).each do |username|
|
||||
wp_users << WpUser.new(wp_target.uri, login: username.chomp)
|
||||
end
|
||||
# Single username?
|
||||
else
|
||||
wp_users << WpUser.new(wp_target.uri, login: wpscan_options.username)
|
||||
end
|
||||
@@ -455,7 +544,6 @@ def main
|
||||
bruteforce = true
|
||||
if wpscan_options.wordlist
|
||||
if wp_target.has_login_protection?
|
||||
|
||||
protection_plugin = wp_target.login_protection_plugin()
|
||||
|
||||
puts
|
||||
@@ -481,6 +569,7 @@ def main
|
||||
else
|
||||
puts critical('Brute forcing aborted')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
stop_time = Time.now
|
||||
@@ -489,9 +578,9 @@ def main
|
||||
|
||||
puts
|
||||
puts info("Finished: #{stop_time.asctime}")
|
||||
puts info("Requests Done: #{@total_requests_done}")
|
||||
puts info("Memory used: #{used_memory.bytes_to_human}") unless windows?
|
||||
puts info("Elapsed time: #{Time.at(elapsed).utc.strftime('%H:%M:%S')}")
|
||||
puts info("Requests made: #{@total_requests_done}")
|
||||
puts info("Memory used: #{used_memory.bytes_to_human}") unless windows?
|
||||
|
||||
# do nothing on interrupt
|
||||
rescue Interrupt
|
||||
@@ -501,14 +590,16 @@ def main
|
||||
puts critical(e.message)
|
||||
|
||||
if e.file
|
||||
puts critical("Current Version: #{WPSCAN_VERSION}")
|
||||
puts critical('Downloaded File Content:')
|
||||
puts e.file[0..500]
|
||||
puts e.file[0..500] # print first 500 chars
|
||||
puts '.........'
|
||||
puts e.file[-500..-1] || e.file # print last 500 chars or the whole file if it's < 500
|
||||
puts
|
||||
end
|
||||
|
||||
puts critical('Some hints to help you with this issue:')
|
||||
puts critical('-) Try updating again')
|
||||
puts critical('-) Try updating again using --verbose')
|
||||
puts critical('-) If you see SSL/TLS related error messages you have to fix your local TLS setup')
|
||||
puts critical('-) Windows is still not supported')
|
||||
exit(1)
|
||||
@@ -522,10 +613,13 @@ def main
|
||||
end
|
||||
exit(1)
|
||||
ensure
|
||||
# Ensure a clean abort of Hydra
|
||||
# See https://github.com/wpscanteam/wpscan/issues/461#issuecomment-42735615
|
||||
Browser.instance.hydra.abort
|
||||
Browser.instance.hydra.run
|
||||
# Make sure there was an argument
|
||||
if ARGV.length != 0
|
||||
# Ensure a clean abort of Hydra
|
||||
# See https://github.com/wpscanteam/wpscan/issues/461#issuecomment-42735615
|
||||
Browser.instance.hydra.abort
|
||||
Browser.instance.hydra.run
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user