Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0997bfe0d | ||
|
|
8b67dad456 | ||
|
|
53fdac1038 | ||
|
|
534a7602e6 | ||
|
|
30f329fe43 | ||
|
|
4ce39951a9 | ||
|
|
0e9eb34626 | ||
|
|
0ff299c425 | ||
|
|
6366258ce9 | ||
|
|
bca69a026e | ||
|
|
adc26ea42a | ||
|
|
b16e8d84d7 | ||
|
|
5ee405d5a0 | ||
|
|
a5b9470636 | ||
|
|
16a3d54cb6 | ||
|
|
9677dcd978 | ||
|
|
17ea42f918 | ||
|
|
bd8915918d | ||
|
|
91db6773a0 | ||
|
|
f50680b61f | ||
|
|
3fb5d33333 | ||
|
|
f70bbb2660 | ||
|
|
589c1ac9bb | ||
|
|
d458fa1b89 | ||
|
|
dc2c99434f | ||
|
|
bbf36562d0 | ||
|
|
c458edf3e4 | ||
|
|
99c2aaef7a | ||
|
|
921096ca10 | ||
|
|
b0fbd6fa36 | ||
|
|
21bd67c44f | ||
|
|
4f142985a2 | ||
|
|
bfa89b44bc | ||
|
|
eba876e72b | ||
|
|
f1a7413e20 | ||
|
|
4d32749489 | ||
|
|
d911a16684 | ||
|
|
d7193bc755 | ||
|
|
aee9ffdb9c | ||
|
|
1f627d5e49 | ||
|
|
bb67626d09 | ||
|
|
4e0153e94a | ||
|
|
065142ff19 | ||
|
|
8bb6fae52f | ||
|
|
8cb7b81903 | ||
|
|
cb214ccda9 | ||
|
|
3fa7b96f27 | ||
|
|
7c8e259072 | ||
|
|
743d067042 | ||
|
|
50ea410718 | ||
|
|
e71182aed2 | ||
|
|
97f7963e0b | ||
|
|
6cea6a10bd | ||
|
|
344d41e365 | ||
|
|
597a8adfed | ||
|
|
5682e5483a | ||
|
|
18779edd7d | ||
|
|
63aeaea77a | ||
|
|
f51e48cb40 | ||
|
|
193372c79c | ||
|
|
34d0afe7e5 | ||
|
|
d33a9dd56d | ||
|
|
af2be90176 | ||
|
|
701fb21544 | ||
|
|
c8f010d9a6 | ||
|
|
c1ca7580e2 | ||
|
|
11d3c2cbf1 | ||
|
|
412f576aee | ||
|
|
ff98a7b23b | ||
|
|
507bac8542 | ||
|
|
3bd6cf4805 | ||
|
|
5712b31869 | ||
|
|
b0f9a0b18f | ||
|
|
f7665b460e | ||
|
|
100029b640 | ||
|
|
2b89bddf0f | ||
|
|
ca46bad8ec | ||
|
|
1ecd2600a3 | ||
|
|
28306b126b | ||
|
|
5c842e192b | ||
|
|
f9f307118d | ||
|
|
2266fa4f4b | ||
|
|
6df2564d1a | ||
|
|
b2a62ebd26 | ||
|
|
2fca30752a | ||
|
|
210eced369 | ||
|
|
08c574aff8 | ||
|
|
f4db2d65f1 | ||
|
|
23b02ade96 | ||
|
|
71d35b16ac | ||
|
|
200058c52a | ||
|
|
edb5fb202a | ||
|
|
d114c25cdb | ||
|
|
64e469568b | ||
|
|
c63d777372 | ||
|
|
ae343b8cb0 | ||
|
|
86eb5d2d57 | ||
|
|
b562d241db | ||
|
|
49b1829b78 | ||
|
|
1a5bf4035c | ||
|
|
f3810a1504 | ||
|
|
4831760c11 | ||
|
|
f375d8991e | ||
|
|
8145a4a3a6 | ||
|
|
12c9b49d4c | ||
|
|
c8eb81161e | ||
|
|
8ab246a66c | ||
|
|
8dfc4797fa | ||
|
|
7888fe1176 | ||
|
|
8a6f3056a3 | ||
|
|
5fbdf9e013 | ||
|
|
1da2f5e823 | ||
|
|
888779f81b | ||
|
|
352286e497 | ||
|
|
025ce37c05 | ||
|
|
d6c2c63679 | ||
|
|
49efbf25ea | ||
|
|
02cdee2776 | ||
|
|
7c9d4d5b05 | ||
|
|
609b7551f8 | ||
|
|
e8f215ae00 | ||
|
|
2e00aea16e | ||
|
|
dd274d77f5 | ||
|
|
58171a7b8c | ||
|
|
8b05179401 | ||
|
|
51d61a7e88 | ||
|
|
d653ce4e0e | ||
|
|
07b3826806 | ||
|
|
1baa3e23b2 | ||
|
|
0aa1f20d47 | ||
|
|
1cf330b389 | ||
|
|
1771c4b346 | ||
|
|
4c053b4873 | ||
|
|
743ba0541b | ||
|
|
cfab2a9cd7 | ||
|
|
32270efd65 | ||
|
|
7ea1acb7c1 | ||
|
|
bf91f60242 | ||
|
|
660885c0b1 | ||
|
|
15fd3b969f | ||
|
|
f1d15ca7f2 | ||
|
|
6f4f4a5924 | ||
|
|
9af0520701 | ||
|
|
2edeab558e | ||
|
|
87bf59f50b | ||
|
|
eeb69e63f7 | ||
|
|
f9435906e7 | ||
|
|
6c8adbe50e | ||
|
|
23bdb6c579 | ||
|
|
264411bfb9 | ||
|
|
2104237584 | ||
|
|
0ae2525737 | ||
|
|
b12973a837 | ||
|
|
fa0582ce0b | ||
|
|
231f5157bf | ||
|
|
8b18204a69 | ||
|
|
95eb6a732c | ||
|
|
047a188b34 | ||
|
|
d407815c30 | ||
|
|
1f0f87633b | ||
|
|
c15ff4e32e | ||
|
|
72bddca314 | ||
|
|
496fc4ebee | ||
|
|
f414e6eeb7 | ||
|
|
f09606cfa3 | ||
|
|
6304fe4c19 | ||
|
|
5f2b8f8a2e | ||
|
|
898e8d4546 | ||
|
|
f1657164d5 | ||
|
|
357e13be2b | ||
|
|
9685568c75 | ||
|
|
b316940790 | ||
|
|
2ced489e1e | ||
|
|
5969fe08d8 | ||
|
|
4a427f1ff6 | ||
|
|
9a3db275f3 | ||
|
|
475dd4d1ff | ||
|
|
57c99c4a34 | ||
|
|
966f5691a2 | ||
|
|
5088ece8a1 | ||
|
|
943d87fe17 | ||
|
|
b5363b2689 | ||
|
|
c15cb16ca8 | ||
|
|
18b7f088fc | ||
|
|
4f9822743c | ||
|
|
e7925de5bc | ||
|
|
27fc6a7279 | ||
|
|
ab5f46e955 | ||
|
|
d30d212cc5 | ||
|
|
adff971d62 | ||
|
|
23b22f71b8 | ||
|
|
fee3671e32 | ||
|
|
26c6be7268 | ||
|
|
01c5bcf2be | ||
|
|
1ab8a5ab98 | ||
|
|
b54aaca28a | ||
|
|
86a29ae000 | ||
|
|
a5dbee93ff | ||
|
|
e0465e6e10 | ||
|
|
7da48b9dd1 | ||
|
|
a64895c3a6 | ||
|
|
21f1a5d4c4 | ||
|
|
d60f79ca33 | ||
|
|
2d5cea5033 | ||
|
|
b0615215fe | ||
|
|
7a0f98b2cb |
@@ -12,5 +12,5 @@ spec/
|
||||
Dockerfile
|
||||
**/*.orig
|
||||
*.orig
|
||||
bin/wpscan-docker*
|
||||
bin/wpscan-*
|
||||
.wpscan/
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before submitting an issue, please make sure you fully read any potential error messages output and did some research on your own.
|
||||
|
||||
### Subject of the issue
|
||||
Describe your issue here.
|
||||
|
||||
@@ -24,4 +35,4 @@ Things you have tried (where relevant):
|
||||
* Update Ruby to the latest version [ ]
|
||||
* Ensure you can reach the target site using cURL [ ]
|
||||
* Proxied WPScan through a HTTP proxy to view the raw traffic [ ]
|
||||
* Ensure you are using a supported Operating System (Linux and macOS) [ ]
|
||||
* Ensure you are using a supported Operating System (Linux and macOS) [ ]
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
10
.github/ISSUE_TEMPLATE/other-issue.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/other-issue.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Other Issue
|
||||
about: Create a report which is not a related to a Bug or Feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before submitting an issue, please make sure you fully read any potential error messages output and did some research on your own.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,3 +21,6 @@ doc/
|
||||
# Old files from v2
|
||||
cache/
|
||||
data/
|
||||
|
||||
# Profiling reports
|
||||
bin/memprof*.report
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
require: rubocop-performance
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.3
|
||||
TargetRubyVersion: 2.4
|
||||
Exclude:
|
||||
- '*.gemspec'
|
||||
- 'vendor/**/*'
|
||||
@@ -9,6 +10,8 @@ LineLength:
|
||||
Max: 120
|
||||
MethodLength:
|
||||
Max: 20
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Lint/UriEscapeUnescape:
|
||||
Enabled: false
|
||||
Metrics/AbcSize:
|
||||
@@ -18,11 +21,11 @@ Metrics/BlockLength:
|
||||
- 'spec/**/*'
|
||||
Metrics/ClassLength:
|
||||
Max: 150
|
||||
Exclude:
|
||||
- 'app/controllers/enumeration/cli_options.rb'
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 8
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Style/FormatStringToken:
|
||||
Enabled: false
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.0
|
||||
2.6.2
|
||||
|
||||
15
.travis.yml
15
.travis.yml
@@ -2,25 +2,22 @@ language: ruby
|
||||
sudo: false
|
||||
cache: bundler
|
||||
rvm:
|
||||
- 2.3.0
|
||||
- 2.3.1
|
||||
- 2.3.2
|
||||
- 2.3.3
|
||||
- 2.3.4
|
||||
- 2.3.5
|
||||
- 2.3.6
|
||||
- 2.3.7
|
||||
- 2.3.8
|
||||
- 2.4.1
|
||||
- 2.4.2
|
||||
- 2.4.3
|
||||
- 2.4.4
|
||||
- 2.4.5
|
||||
- 2.4.6
|
||||
- 2.5.0
|
||||
- 2.5.1
|
||||
- 2.5.2
|
||||
- 2.5.3
|
||||
- 2.5.4
|
||||
- 2.5.5
|
||||
- 2.6.0
|
||||
- 2.6.1
|
||||
- 2.6.2
|
||||
- 2.6.3
|
||||
- ruby-head
|
||||
before_install:
|
||||
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ruby:2.5.1-alpine AS builder
|
||||
FROM ruby:2.6.3-alpine AS builder
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
|
||||
ARG BUNDLER_ARGS="--jobs=8 --without test development"
|
||||
@@ -19,19 +19,22 @@ RUN rake install --trace
|
||||
RUN chmod -R a+r /usr/local/bundle
|
||||
|
||||
|
||||
FROM ruby:2.5-alpine
|
||||
FROM ruby:2.6.3-alpine
|
||||
LABEL maintainer="WPScan Team <team@wpscan.org>"
|
||||
|
||||
RUN adduser -h /wpscan -g WPScan -D wpscan
|
||||
|
||||
COPY --from=builder /usr/local/bundle /usr/local/bundle
|
||||
|
||||
RUN chown -R wpscan:wpscan /wpscan
|
||||
|
||||
# runtime dependencies
|
||||
RUN apk add --no-cache libcurl procps sqlite-libs
|
||||
|
||||
WORKDIR /wpscan
|
||||
|
||||
USER wpscan
|
||||
|
||||
RUN /usr/local/bundle/bin/wpscan --update --verbose
|
||||
|
||||
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
|
||||
|
||||
4
Gemfile
4
Gemfile
@@ -1,2 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
gemspec
|
||||
|
||||
# gem 'cms_scanner', branch: 'xxx', git: 'https://github.com/wpscanteam/CMSScanner.git'
|
||||
|
||||
10
LICENSE
10
LICENSE
@@ -6,9 +6,9 @@ Cases that include commercialization of WPScan require a commercial, non-free li
|
||||
|
||||
1. Definitions
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
2. Commercialization
|
||||
|
||||
@@ -29,6 +29,8 @@ Example cases which do not require a commercial license, and thus fall under the
|
||||
|
||||
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
|
||||
|
||||
We may grant commercial licenses at no monetary cost at our own discretion if the commercial usage is deemed by the WPScan Team to significantly benefit WPScan.
|
||||
|
||||
Free-use Terms and Conditions;
|
||||
|
||||
3. Redistribution
|
||||
@@ -57,7 +59,7 @@ WPScan is provided under an AS-IS basis and without any support, updates or main
|
||||
|
||||
8. Disclaimer of Warranty
|
||||
|
||||
WPScan is provided under this License on an “as is” basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
WPScan is provided under this License on an "as is" basis, without warranty of any kind, either expressed, implied, or statutory, including, without limitation, warranties that the WPScan is free of defects, merchantable, fit for a particular purpose or non-infringing.
|
||||
|
||||
9. Limitation of Liability
|
||||
|
||||
|
||||
93
README.md
93
README.md
@@ -1,13 +1,27 @@
|
||||

|
||||
<p align="center">
|
||||
<a href="https://wpscan.org/">
|
||||
<img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
[](https://badge.fury.io/rb/wpscan)
|
||||
[](https://travis-ci.org/wpscanteam/wpscan)
|
||||
[](https://codeclimate.com/github/wpscanteam/wpscan)
|
||||
[](https://www.patreon.com/wpscan)
|
||||
<h3 align="center">WPScan</h3>
|
||||
|
||||
<p align="center">
|
||||
WordPress Vulnerability 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>
|
||||
</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://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
|
||||
</p>
|
||||
|
||||
# INSTALL
|
||||
|
||||
## Prerequisites:
|
||||
## Prerequisites
|
||||
|
||||
- (Optional but highly recommended: [RVM](https://rvm.io/rvm/install))
|
||||
- Ruby >= 2.3 - Recommended: latest
|
||||
@@ -15,20 +29,21 @@
|
||||
- Curl >= 7.21 - Recommended: latest
|
||||
- The 7.29 has a segfault
|
||||
- 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):
|
||||
### From RubyGems (Recommended)
|
||||
|
||||
```
|
||||
```shell
|
||||
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):
|
||||
### From sources (NOT Recommended)
|
||||
|
||||
Prerequisites: Git
|
||||
|
||||
```
|
||||
```shell
|
||||
git clone https://github.com/wpscanteam/wpscan
|
||||
|
||||
cd wpscan/
|
||||
@@ -47,14 +62,17 @@ Updating WPScan itself is either done via ```gem update wpscan``` or the package
|
||||
Pull the repo with ```docker pull wpscanteam/wpscan```
|
||||
|
||||
Enumerating usernames
|
||||
```
|
||||
|
||||
```shell
|
||||
docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u
|
||||
```
|
||||
|
||||
Enumerating a range of usernames
|
||||
```
|
||||
|
||||
```shell
|
||||
docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-100
|
||||
```
|
||||
|
||||
** replace u1-100 with a range of your choice.
|
||||
|
||||
# Usage
|
||||
@@ -68,50 +86,45 @@ The DB is located at ~/.wpscan/db
|
||||
|
||||
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
|
||||
|
||||
* ~/.wpscan/cli_options.json
|
||||
* ~/.wpscan/cli_options.yml
|
||||
* pwd/.wpscan/cli_options.json
|
||||
* pwd/.wpscan/cli_options.yml
|
||||
- ~/.wpscan/cli_options.json
|
||||
- ~/.wpscan/cli_options.yml
|
||||
- pwd/.wpscan/cli_options.json
|
||||
- pwd/.wpscan/cli_options.yml
|
||||
|
||||
If those files exist, options from them will be loaded and overridden if found twice.
|
||||
|
||||
e.g:
|
||||
|
||||
~/.wpscan/cli_options.yml:
|
||||
```
|
||||
|
||||
```yml
|
||||
proxy: 'http://127.0.0.1:8080'
|
||||
verbose: true
|
||||
```
|
||||
|
||||
pwd/.wpscan/cli_options.yml:
|
||||
```
|
||||
|
||||
```yml
|
||||
proxy: 'socks5://127.0.0.1:9090'
|
||||
url: 'http://target.tld'
|
||||
```
|
||||
|
||||
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
|
||||
|
||||
|
||||
Enumerating usernames
|
||||
```
|
||||
|
||||
```shell
|
||||
wpscan --url https://target.tld/ --enumerate u
|
||||
```
|
||||
|
||||
Enumerating a range of usernames
|
||||
```
|
||||
|
||||
```shell
|
||||
wpscan --url https://target.tld/ --enumerate u1-100
|
||||
```
|
||||
|
||||
** replace u1-100 with a range of your choice.
|
||||
|
||||
|
||||
# PROJECT HOME
|
||||
|
||||
[https://wpscan.org](https://wpscan.org)
|
||||
|
||||
# VULNERABILITY DATABASE
|
||||
|
||||
[https://wpvulndb.com](https://wpvulndb.com)
|
||||
|
||||
# LICENSE
|
||||
|
||||
## WPScan Public Source License
|
||||
@@ -134,16 +147,16 @@ A commercial use is one intended for commercial advantage or monetary compensati
|
||||
|
||||
Example cases of commercialization are:
|
||||
|
||||
- Using WPScan to provide commercial managed/Software-as-a-Service services.
|
||||
- Distributing WPScan as a commercial product or as part of one.
|
||||
- Using WPScan as a value added service/product.
|
||||
- Using WPScan to provide commercial managed/Software-as-a-Service services.
|
||||
- Distributing WPScan as a commercial product or as part of one.
|
||||
- Using WPScan as a value added service/product.
|
||||
|
||||
Example cases which do not require a commercial license, and thus fall under the terms set out below, include (but are not limited to):
|
||||
|
||||
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
|
||||
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
|
||||
- Using WPScan to test your own systems.
|
||||
- Any non-commercial use of WPScan.
|
||||
- Penetration testers (or penetration testing organizations) using WPScan as part of their assessment toolkit.
|
||||
- Penetration Testing Linux Distributions including but not limited to Kali Linux, SamuraiWTF, BackBox Linux.
|
||||
- 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.
|
||||
|
||||
@@ -153,9 +166,9 @@ Free-use Terms and Conditions;
|
||||
|
||||
Redistribution is permitted under the following conditions:
|
||||
|
||||
- Unmodified License is provided with WPScan.
|
||||
- Unmodified Copyright notices are provided with WPScan.
|
||||
- Does not conflict with the commercialization clause.
|
||||
- Unmodified License is provided with WPScan.
|
||||
- Unmodified Copyright notices are provided with WPScan.
|
||||
- Does not conflict with the commercialization clause.
|
||||
|
||||
### 4. Copying
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'models'
|
||||
require_relative 'finders'
|
||||
require_relative 'controllers'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'controllers/core'
|
||||
require_relative 'controllers/custom_directories'
|
||||
require_relative 'controllers/wp_version'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Controller to add the aliases in the CLI
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Specific Core controller to include WordPress checks
|
||||
@@ -25,53 +27,56 @@ module WPScan
|
||||
# @return [ Boolean ]
|
||||
def update_db_required?
|
||||
if local_db.missing_files?
|
||||
raise MissingDatabaseFile if parsed_options[:update] == false
|
||||
raise Error::MissingDatabaseFile if ParsedCli.update == false
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return parsed_options[:update] unless parsed_options[:update].nil?
|
||||
return ParsedCli.update unless ParsedCli.update.nil?
|
||||
|
||||
return false unless user_interaction? && local_db.outdated?
|
||||
|
||||
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]'
|
||||
|
||||
Readline.readline =~ /^y/i ? true : false
|
||||
/^y/i.match?(Readline.readline) ? true : false
|
||||
end
|
||||
|
||||
def update_db
|
||||
output('db_update_started')
|
||||
output('db_update_finished', updated: local_db.update, verbose: parsed_options[:verbose])
|
||||
output('db_update_finished', updated: local_db.update, verbose: ParsedCli.verbose)
|
||||
|
||||
exit(0) unless parsed_options[:url]
|
||||
exit(0) unless ParsedCli.url
|
||||
end
|
||||
|
||||
def before_scan
|
||||
@last_update = local_db.last_update
|
||||
|
||||
maybe_output_banner_help_and_version # From CMS Scanner
|
||||
maybe_output_banner_help_and_version # From CMSScanner
|
||||
|
||||
update_db if update_db_required?
|
||||
setup_cache
|
||||
check_target_availability
|
||||
load_server_module
|
||||
check_wordpress_state
|
||||
rescue Error::NotWordPress => e
|
||||
target.maybe_add_cookies
|
||||
raise e unless target.wordpress?(ParsedCli.detection_mode)
|
||||
end
|
||||
|
||||
# Raises errors if the target is hosted on wordpress.com or is not running WordPress
|
||||
# Also check if the homepage_url is still the install url
|
||||
def check_wordpress_state
|
||||
raise WordPressHostedError if target.wordpress_hosted?
|
||||
raise Error::WordPressHosted if target.wordpress_hosted?
|
||||
|
||||
if Addressable::URI.parse(target.homepage_url).path =~ %r{/wp-admin/install.php$}i
|
||||
if %r{/wp-admin/install.php$}i.match?(Addressable::URI.parse(target.homepage_url).path)
|
||||
|
||||
output('not_fully_configured', url: target.homepage_url)
|
||||
|
||||
exit(WPScan::ExitCode::VULNERABLE)
|
||||
end
|
||||
|
||||
raise NotWordPressError unless target.wordpress?(parsed_options[:detection_mode]) || parsed_options[:force]
|
||||
raise Error::NotWordPress unless target.wordpress?(ParsedCli.detection_mode) || ParsedCli.force
|
||||
end
|
||||
|
||||
# Loads the related server module in the target
|
||||
@@ -83,7 +88,7 @@ module WPScan
|
||||
server = target.server || :Apache # Tries to auto detect the server
|
||||
|
||||
# Force a specific server module to be loaded if supplied
|
||||
case parsed_options[:server]
|
||||
case ParsedCli.server
|
||||
when :apache
|
||||
server = :Apache
|
||||
when :iis
|
||||
@@ -95,7 +100,7 @@ module WPScan
|
||||
mod = CMSScanner::Target::Server.const_get(server)
|
||||
|
||||
target.extend mod
|
||||
WPScan::WpItem.include mod
|
||||
Model::WpItem.include mod
|
||||
|
||||
server
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Controller to ensure that the wp-content and wp-plugins
|
||||
@@ -5,18 +7,20 @@ module WPScan
|
||||
class CustomDirectories < CMSScanner::Controller::Base
|
||||
def cli_options
|
||||
[
|
||||
OptString.new(['--wp-content-dir DIR']),
|
||||
OptString.new(['--wp-plugins-dir DIR'])
|
||||
OptString.new(['--wp-content-dir DIR',
|
||||
'The wp-content directory if custom or not detected, such as "wp-content"']),
|
||||
OptString.new(['--wp-plugins-dir DIR',
|
||||
'The plugins directory if custom or not detected, such as "wp-content/plugins"'])
|
||||
]
|
||||
end
|
||||
|
||||
def before_scan
|
||||
target.content_dir = parsed_options[:wp_content_dir] if parsed_options[:wp_content_dir]
|
||||
target.plugins_dir = parsed_options[:wp_plugins_dir] if parsed_options[:wp_plugins_dir]
|
||||
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
|
||||
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
|
||||
|
||||
return if target.content_dir
|
||||
return if target.content_dir(ParsedCli.detection_mode)
|
||||
|
||||
raise 'Unable to identify the wp-content dir, please supply it with --wp-content-dir'
|
||||
raise Error::WpContentDirNotDetected
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'enumeration/cli_options'
|
||||
require_relative 'enumeration/enum_methods'
|
||||
|
||||
@@ -5,13 +7,8 @@ module WPScan
|
||||
module Controller
|
||||
# Enumeration Controller
|
||||
class Enumeration < CMSScanner::Controller::Base
|
||||
def before_scan
|
||||
DB::DynamicFinders::Plugin.create_versions_finders
|
||||
DB::DynamicFinders::Theme.create_versions_finders
|
||||
end
|
||||
|
||||
def run
|
||||
enum = parsed_options[:enumerate] || {}
|
||||
enum = ParsedCli.enumerate || {}
|
||||
|
||||
enum_plugins if enum_plugins?(enum)
|
||||
enum_themes if enum_themes?(enum)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Enumeration CLI Options
|
||||
@@ -9,7 +11,6 @@ module WPScan
|
||||
end
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def cli_enum_choices
|
||||
[
|
||||
OptMultiChoices.new(
|
||||
@@ -43,7 +44,6 @@ module WPScan
|
||||
)
|
||||
]
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
# @return [ Array<OptParseValidator::OptBase> ]
|
||||
def cli_plugins_opts
|
||||
@@ -65,6 +65,11 @@ module WPScan
|
||||
'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
|
||||
'or --plugins-detection modes.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
|
||||
),
|
||||
OptInteger.new(
|
||||
['--plugins-threshold THRESHOLD',
|
||||
'Raise an error when the number of detected plugins via known locations reaches the threshold. ' \
|
||||
'Set to 0 to ignore the threshold.'], default: 100
|
||||
)
|
||||
]
|
||||
end
|
||||
@@ -89,6 +94,11 @@ module WPScan
|
||||
'Use the supplied mode to check themes versions instead of the --detection-mode ' \
|
||||
'or --themes-detection modes.'],
|
||||
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
|
||||
),
|
||||
OptInteger.new(
|
||||
['--themes-threshold THRESHOLD',
|
||||
'Raise an error when the number of detected themes via known locations reaches the threshold. ' \
|
||||
'Set to 0 to ignore the threshold.'], default: 20
|
||||
)
|
||||
]
|
||||
end
|
||||
@@ -98,7 +108,7 @@ module WPScan
|
||||
[
|
||||
OptFilePath.new(
|
||||
['--timthumbs-list FILE-PATH', 'List of timthumbs\' location to use'],
|
||||
exists: true, default: File.join(DB_DIR, 'timthumbs-v3.txt'), advanced: true
|
||||
exists: true, default: DB_DIR.join('timthumbs-v3.txt').to_s, advanced: true
|
||||
),
|
||||
OptChoice.new(
|
||||
['--timthumbs-detection MODE',
|
||||
@@ -113,7 +123,7 @@ module WPScan
|
||||
[
|
||||
OptFilePath.new(
|
||||
['--config-backups-list FILE-PATH', 'List of config backups\' filenames to use'],
|
||||
exists: true, default: File.join(DB_DIR, 'config_backups.txt'), advanced: true
|
||||
exists: true, default: DB_DIR.join('config_backups.txt').to_s, advanced: true
|
||||
),
|
||||
OptChoice.new(
|
||||
['--config-backups-detection MODE',
|
||||
@@ -128,7 +138,7 @@ module WPScan
|
||||
[
|
||||
OptFilePath.new(
|
||||
['--db-exports-list FILE-PATH', 'List of DB exports\' paths to use'],
|
||||
exists: true, default: File.join(DB_DIR, 'db_exports.txt'), advanced: true
|
||||
exists: true, default: DB_DIR.join('db_exports.txt').to_s, advanced: true
|
||||
),
|
||||
OptChoice.new(
|
||||
['--db-exports-detection MODE',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Enumeration Methods
|
||||
@@ -5,13 +7,13 @@ module WPScan
|
||||
# @param [ String ] type (plugins or themes)
|
||||
# @param [ Symbol ] detection_mode
|
||||
#
|
||||
# @return [ String ] The related enumration message depending on the parsed_options and type supplied
|
||||
# @return [ String ] The related enumration message depending on the ParsedCli and type supplied
|
||||
def enum_message(type, detection_mode)
|
||||
return unless %w[plugins themes].include?(type)
|
||||
|
||||
details = if parsed_options[:enumerate][:"vulnerable_#{type}"]
|
||||
details = if ParsedCli.enumerate[:"vulnerable_#{type}"]
|
||||
'Vulnerable'
|
||||
elsif parsed_options[:enumerate][:"all_#{type}"]
|
||||
elsif ParsedCli.enumerate[:"all_#{type}"]
|
||||
'All'
|
||||
else
|
||||
'Most Popular'
|
||||
@@ -37,15 +39,15 @@ module WPScan
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def default_opts(type)
|
||||
mode = parsed_options[:"#{type}_detection"] || parsed_options[:detection_mode]
|
||||
mode = ParsedCli.options[:"#{type}_detection"] || ParsedCli.detection_mode
|
||||
|
||||
{
|
||||
mode: mode,
|
||||
exclude_content: parsed_options[:exclude_content_based],
|
||||
exclude_content: ParsedCli.exclude_content_based,
|
||||
show_progression: user_interaction?,
|
||||
version_detection: {
|
||||
mode: parsed_options[:"#{type}_version_detection"] || mode,
|
||||
confidence_threshold: parsed_options[:"#{type}_version_all"] ? 0 : 100
|
||||
mode: ParsedCli.options[:"#{type}_version_detection"] || mode,
|
||||
confidence_threshold: ParsedCli.options[:"#{type}_version_all"] ? 0 : 100
|
||||
}
|
||||
}
|
||||
end
|
||||
@@ -59,7 +61,8 @@ module WPScan
|
||||
|
||||
def enum_plugins
|
||||
opts = default_opts('plugins').merge(
|
||||
list: plugins_list_from_opts(parsed_options),
|
||||
list: plugins_list_from_opts(ParsedCli.options),
|
||||
threshold: ParsedCli.plugins_threshold,
|
||||
sort: true
|
||||
)
|
||||
|
||||
@@ -75,7 +78,7 @@ module WPScan
|
||||
|
||||
plugins.each(&:version)
|
||||
|
||||
plugins.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_plugins]
|
||||
plugins.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_plugins]
|
||||
|
||||
output('plugins', plugins: plugins)
|
||||
end
|
||||
@@ -105,7 +108,8 @@ module WPScan
|
||||
|
||||
def enum_themes
|
||||
opts = default_opts('themes').merge(
|
||||
list: themes_list_from_opts(parsed_options),
|
||||
list: themes_list_from_opts(ParsedCli.options),
|
||||
threshold: ParsedCli.themes_threshold,
|
||||
sort: true
|
||||
)
|
||||
|
||||
@@ -121,7 +125,7 @@ module WPScan
|
||||
|
||||
themes.each(&:version)
|
||||
|
||||
themes.select!(&:vulnerable?) if parsed_options[:enumerate][:vulnerable_themes]
|
||||
themes.select!(&:vulnerable?) if ParsedCli.enumerate[:vulnerable_themes]
|
||||
|
||||
output('themes', themes: themes)
|
||||
end
|
||||
@@ -143,28 +147,28 @@ module WPScan
|
||||
end
|
||||
|
||||
def enum_timthumbs
|
||||
opts = default_opts('timthumbs').merge(list: parsed_options[:timthumbs_list])
|
||||
opts = default_opts('timthumbs').merge(list: ParsedCli.timthumbs_list)
|
||||
|
||||
output('@info', msg: "Enumerating Timthumbs #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||
output('timthumbs', timthumbs: target.timthumbs(opts))
|
||||
end
|
||||
|
||||
def enum_config_backups
|
||||
opts = default_opts('config_backups').merge(list: parsed_options[:config_backups_list])
|
||||
opts = default_opts('config_backups').merge(list: ParsedCli.config_backups_list)
|
||||
|
||||
output('@info', msg: "Enumerating Config Backups #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||
output('config_backups', config_backups: target.config_backups(opts))
|
||||
end
|
||||
|
||||
def enum_db_exports
|
||||
opts = default_opts('db_exports').merge(list: parsed_options[:db_exports_list])
|
||||
opts = default_opts('db_exports').merge(list: ParsedCli.db_exports_list)
|
||||
|
||||
output('@info', msg: "Enumerating DB Exports #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||
output('db_exports', db_exports: target.db_exports(opts))
|
||||
end
|
||||
|
||||
def enum_medias
|
||||
opts = default_opts('medias').merge(range: parsed_options[:enumerate][:medias])
|
||||
opts = default_opts('medias').merge(range: ParsedCli.enumerate[:medias])
|
||||
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
@@ -179,13 +183,13 @@ module WPScan
|
||||
#
|
||||
# @return [ Boolean ] Wether or not to enumerate the users
|
||||
def enum_users?(opts)
|
||||
opts[:users] || (parsed_options[:passwords] && !parsed_options[:username] && !parsed_options[:usernames])
|
||||
opts[:users] || (ParsedCli.passwords && !ParsedCli.username && !ParsedCli.usernames)
|
||||
end
|
||||
|
||||
def enum_users
|
||||
opts = default_opts('users').merge(
|
||||
range: enum_users_range,
|
||||
list: parsed_options[:users_list]
|
||||
list: ParsedCli.users_list
|
||||
)
|
||||
|
||||
output('@info', msg: "Enumerating Users #{enum_detection_message(opts[:mode])}") if user_interaction?
|
||||
@@ -196,7 +200,7 @@ module WPScan
|
||||
# If the --enumerate is used, the default value is handled by the Option
|
||||
# However, when using --passwords alone, the default has to be set by the code below
|
||||
def enum_users_range
|
||||
parsed_options[:enumerate][:users] || cli_enum_choices[0].choices[:u].validate(nil)
|
||||
ParsedCli.enumerate[:users] || cli_enum_choices[0].choices[:u].validate(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Main Theme Controller
|
||||
@@ -16,9 +18,9 @@ module WPScan
|
||||
output(
|
||||
'theme',
|
||||
theme: target.main_theme(
|
||||
mode: parsed_options[:main_theme_detection] || parsed_options[:detection_mode]
|
||||
mode: ParsedCli.main_theme_detection || ParsedCli.detection_mode
|
||||
),
|
||||
verbose: parsed_options[:verbose]
|
||||
verbose: ParsedCli.verbose
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Password Attack Controller
|
||||
@@ -22,7 +24,7 @@ module WPScan
|
||||
end
|
||||
|
||||
def run
|
||||
return unless parsed_options[:passwords]
|
||||
return unless ParsedCli.passwords
|
||||
|
||||
if user_interaction?
|
||||
output('@info',
|
||||
@@ -31,13 +33,13 @@ module WPScan
|
||||
|
||||
attack_opts = {
|
||||
show_progression: user_interaction?,
|
||||
multicall_max_passwords: parsed_options[:multicall_max_passwords]
|
||||
multicall_max_passwords: ParsedCli.multicall_max_passwords
|
||||
}
|
||||
|
||||
begin
|
||||
found = []
|
||||
|
||||
attacker.attack(users, passwords(parsed_options[:passwords]), attack_opts) do |user|
|
||||
attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user|
|
||||
found << user
|
||||
|
||||
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
|
||||
@@ -52,46 +54,63 @@ module WPScan
|
||||
@attacker ||= attacker_from_cli_options || attacker_from_automatic_detection
|
||||
end
|
||||
|
||||
# @return [ WPScan::XMLRPC ]
|
||||
# @return [ Model::XMLRPC ]
|
||||
def xmlrpc
|
||||
@xmlrpc ||= target.xmlrpc
|
||||
end
|
||||
|
||||
# @return [ CMSScanner::Finders::Finder ]
|
||||
def attacker_from_cli_options
|
||||
return unless parsed_options[:password_attack]
|
||||
return unless ParsedCli.password_attack
|
||||
|
||||
case parsed_options[:password_attack]
|
||||
case ParsedCli.password_attack
|
||||
when :wp_login
|
||||
WPScan::Finders::Passwords::WpLogin.new(target)
|
||||
Finders::Passwords::WpLogin.new(target)
|
||||
when :xmlrpc
|
||||
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||
|
||||
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
when :xmlrpc_multicall
|
||||
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||
raise Error::XMLRPCNotDetected unless xmlrpc
|
||||
|
||||
Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
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/
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ CMSScanner::Finders::Finder ]
|
||||
def attacker_from_automatic_detection
|
||||
if xmlrpc&.enabled? && xmlrpc.available_methods.include?('wp.getUsersBlogs')
|
||||
if xmlrpc_get_users_blogs_enabled?
|
||||
wp_version = target.wp_version
|
||||
|
||||
if wp_version && wp_version < '4.4'
|
||||
WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||
Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
|
||||
else
|
||||
WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
Finders::Passwords::XMLRPC.new(xmlrpc)
|
||||
end
|
||||
else
|
||||
WPScan::Finders::Passwords::WpLogin.new(target)
|
||||
Finders::Passwords::WpLogin.new(target)
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Array<Users> ] The users to brute force
|
||||
def users
|
||||
return target.users unless parsed_options[:usernames]
|
||||
return target.users unless ParsedCli.usernames
|
||||
|
||||
parsed_options[:usernames].reduce([]) do |acc, elem|
|
||||
acc << CMSScanner::User.new(elem.chomp)
|
||||
ParsedCli.usernames.reduce([]) do |acc, elem|
|
||||
acc << Model::User.new(elem.chomp)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Controller
|
||||
# Wp Version Controller
|
||||
@@ -15,15 +17,15 @@ module WPScan
|
||||
end
|
||||
|
||||
def before_scan
|
||||
WPScan::DB::DynamicFinders::Wordpress.create_versions_finders
|
||||
DB::DynamicFinders::Wordpress.create_versions_finders
|
||||
end
|
||||
|
||||
def run
|
||||
output(
|
||||
'version',
|
||||
version: target.wp_version(
|
||||
mode: parsed_options[:wp_version_detection] || parsed_options[:detection_mode],
|
||||
confidence_threshold: parsed_options[:wp_version_all] ? 0 : 100,
|
||||
mode: ParsedCli.wp_version_detection || ParsedCli.detection_mode,
|
||||
confidence_threshold: ParsedCli.wp_version_all ? 0 : 100,
|
||||
show_progression: user_interaction?
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'finders/interesting_findings'
|
||||
require_relative 'finders/wp_items'
|
||||
require_relative 'finders/wp_version'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'config_backups/known_filenames'
|
||||
|
||||
module WPScan
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ConfigBackups
|
||||
@@ -13,11 +15,10 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(potential_urls(opts), opts) do |res|
|
||||
# Might need to improve that
|
||||
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
|
||||
next unless res.body =~ /define/i && res.body !~ /<\s?html/i
|
||||
|
||||
found << WPScan::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||
found << Model::ConfigBackup.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'db_exports/known_locations'
|
||||
|
||||
module WPScan
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module DbExports
|
||||
@@ -6,6 +8,8 @@ module WPScan
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE) TABLE|INSERT INTO/.freeze
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
@@ -14,15 +18,23 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(potential_urls(opts), opts) do |res|
|
||||
next unless res.code == 200 && res.body =~ /INSERT INTO/
|
||||
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
|
||||
if res.effective_url.end_with?('.zip')
|
||||
next unless %r{\Aapplication/zip}i.match?(res.headers['Content-Type'])
|
||||
else
|
||||
next unless SQL_PATTERN.match?(res.body)
|
||||
end
|
||||
|
||||
found << WPScan::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def full_request_params
|
||||
@full_request_params ||= { headers: { 'Range' => 'bytes=0-3000' } }
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'interesting_findings/readme'
|
||||
require_relative 'interesting_findings/wp_cron'
|
||||
require_relative 'interesting_findings/multisite'
|
||||
require_relative 'interesting_findings/debug_log'
|
||||
require_relative 'interesting_findings/backup_db'
|
||||
@@ -23,7 +26,7 @@ module WPScan
|
||||
%w[
|
||||
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog
|
||||
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
|
||||
UploadSQLDump EmergencyPwdResetScript
|
||||
UploadSQLDump EmergencyPwdResetScript WPCron
|
||||
].each do |f|
|
||||
finders << InterestingFindings.const_get(f).new(target)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -6,13 +8,12 @@ module WPScan
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/backup-db/'
|
||||
url = target.url(path)
|
||||
res = Browser.get(url)
|
||||
res = target.head_and_get(path, [200, 403])
|
||||
|
||||
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
|
||||
|
||||
WPScan::BackupDB.new(
|
||||
url,
|
||||
Model::BackupDB.new(
|
||||
target.url(path),
|
||||
confidence: 70,
|
||||
found_by: DIRECT_ACCESS,
|
||||
interesting_entries: target.directory_listing_entries(path),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -9,7 +11,7 @@ module WPScan
|
||||
|
||||
return unless target.debug_log?(path)
|
||||
|
||||
WPScan::DebugLog.new(
|
||||
Model::DebugLog.new(
|
||||
target.url(path),
|
||||
confidence: 100, found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -5,13 +7,12 @@ module WPScan
|
||||
class DuplicatorInstallerLog < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('installer-log.txt')
|
||||
res = Browser.get(url)
|
||||
path = 'installer-log.txt'
|
||||
|
||||
return unless res.body =~ /DUPLICATOR INSTALL-LOG/
|
||||
return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body)
|
||||
|
||||
WPScan::DuplicatorInstallerLog.new(
|
||||
url,
|
||||
Model::DuplicatorInstallerLog.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -5,14 +7,14 @@ module WPScan
|
||||
class EmergencyPwdResetScript < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = target.url('/emergency.php')
|
||||
res = Browser.get(url)
|
||||
path = 'emergency.php'
|
||||
res = target.head_and_get(path)
|
||||
|
||||
return unless res.code == 200 && !target.homepage_or_404?(res)
|
||||
|
||||
WPScan::EmergencyPwdResetScript.new(
|
||||
url,
|
||||
confidence: res.body =~ /password/i ? 100 : 40,
|
||||
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'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -10,7 +12,7 @@ module WPScan
|
||||
|
||||
return if fpd_entries.empty?
|
||||
|
||||
WPScan::FullPathDisclosure.new(
|
||||
Model::FullPathDisclosure.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -7,12 +9,14 @@ module WPScan
|
||||
def passive(_opts = {})
|
||||
pattern = %r{#{target.content_dir}/mu\-plugins/}i
|
||||
|
||||
target.in_scope_urls(target.homepage_res) do |url|
|
||||
next unless Addressable::URI.parse(url).path =~ pattern
|
||||
target.in_scope_uris(target.homepage_res) do |uri|
|
||||
next unless uri.path&.match?(pattern)
|
||||
|
||||
url = target.url('wp-content/mu-plugins/')
|
||||
|
||||
return WPScan::MuPlugins.new(
|
||||
target.mu_plugins = true
|
||||
|
||||
return Model::MuPlugins.new(
|
||||
url,
|
||||
confidence: 70,
|
||||
found_by: 'URLs In Homepage (Passive Detection)',
|
||||
@@ -31,11 +35,9 @@ module WPScan
|
||||
return unless [200, 401, 403].include?(res.code)
|
||||
return if target.homepage_or_404?(res)
|
||||
|
||||
# TODO: add the check for --exclude-content once implemented ?
|
||||
|
||||
target.mu_plugins = true
|
||||
|
||||
WPScan::MuPlugins.new(
|
||||
Model::MuPlugins.new(
|
||||
url,
|
||||
confidence: 80,
|
||||
found_by: DIRECT_ACCESS,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -15,7 +17,7 @@ module WPScan
|
||||
|
||||
target.multisite = true
|
||||
|
||||
WPScan::Multisite.new(
|
||||
Model::Multisite.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -5,14 +7,14 @@ module WPScan
|
||||
class Readme < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
potential_files.each do |file|
|
||||
url = target.url(file)
|
||||
res = Browser.get(url)
|
||||
potential_files.each do |path|
|
||||
res = target.head_and_get(path)
|
||||
|
||||
if res.code == 200 && res.body =~ /wordpress/i
|
||||
return WPScan::Readme.new(url, confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
next unless res.code == 200 && res.body =~ /wordpress/i
|
||||
|
||||
return Model::Readme.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -18,7 +20,7 @@ module WPScan
|
||||
|
||||
target.registration_enabled = true
|
||||
|
||||
WPScan::Registration.new(
|
||||
Model::Registration.new(
|
||||
res.effective_url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -7,11 +9,11 @@ module WPScan
|
||||
def aggressive(_opts = {})
|
||||
path = 'wp-content/uploads/tmm_db_migrate/tmm_db_migrate.zip'
|
||||
url = target.url(path)
|
||||
res = Browser.get(url)
|
||||
res = browser.forge_request(url, target.head_or_get_request_params).run
|
||||
|
||||
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
|
||||
|
||||
WPScan::TmmDbMigrate.new(
|
||||
Model::TmmDbMigrate.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
@@ -11,7 +13,7 @@ module WPScan
|
||||
|
||||
url = target.url(path)
|
||||
|
||||
WPScan::UploadDirectoryListing.new(
|
||||
Model::UploadDirectoryListing.new(
|
||||
url,
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS,
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# UploadSQLDump finder
|
||||
class UploadSQLDump < CMSScanner::Finders::Finder
|
||||
SQL_PATTERN = /(?:(?:(?:DROP|CREATE) TABLE)|INSERT INTO)/.freeze
|
||||
SQL_PATTERN = /(?:DROP|CREATE|(?:UN)?LOCK) TABLE|INSERT INTO/.freeze
|
||||
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
url = dump_url
|
||||
res = Browser.get(url)
|
||||
path = 'wp-content/uploads/dump.sql'
|
||||
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
|
||||
|
||||
return unless res.code == 200 && res.body =~ SQL_PATTERN
|
||||
return unless SQL_PATTERN.match?(res.body)
|
||||
|
||||
WPScan::UploadSQLDump.new(
|
||||
url,
|
||||
Model::UploadSQLDump.new(
|
||||
target.url(path),
|
||||
confidence: 100,
|
||||
found_by: DIRECT_ACCESS
|
||||
)
|
||||
end
|
||||
|
||||
def dump_url
|
||||
target.url('wp-content/uploads/dump.sql')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
33
app/finders/interesting_findings/wp_cron.rb
Normal file
33
app/finders/interesting_findings/wp_cron.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module InterestingFindings
|
||||
# wp-cron.php finder
|
||||
class WPCron < CMSScanner::Finders::Finder
|
||||
# @return [ InterestingFinding ]
|
||||
def aggressive(_opts = {})
|
||||
res = Browser.get(wp_cron_url)
|
||||
|
||||
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'
|
||||
]
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
def wp_cron_url
|
||||
@wp_cron_url ||= target.url('wp-cron.php')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'main_theme/css_style'
|
||||
require_relative 'main_theme/woo_framework_meta_generator'
|
||||
require_relative 'main_theme/urls_in_homepage'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
@@ -6,7 +8,7 @@ module WPScan
|
||||
include Finders::WpItems::URLsInHomepage
|
||||
|
||||
def create_theme(slug, style_url, opts)
|
||||
WPScan::Theme.new(
|
||||
Model::Theme.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by, confidence: 70, style_url: style_url)
|
||||
@@ -18,10 +20,10 @@ module WPScan
|
||||
end
|
||||
|
||||
def passive_from_css_href(res, opts)
|
||||
target.in_scope_urls(res, '//style/@src|//link/@href') do |url|
|
||||
next unless Addressable::URI.parse(url).path =~ %r{/themes/([^\/]+)/style.css\z}i
|
||||
target.in_scope_uris(res, '//style/@src|//link/@href') do |uri|
|
||||
next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
|
||||
|
||||
return create_theme(Regexp.last_match[1], url, opts)
|
||||
return create_theme(Regexp.last_match[1], uri.to_s, opts)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
@@ -14,7 +16,7 @@ module WPScan
|
||||
slugs = items_from_links('themes', false) + items_from_codes('themes', false)
|
||||
|
||||
slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module MainTheme
|
||||
@@ -10,7 +12,7 @@ module WPScan
|
||||
def passive(opts = {})
|
||||
return unless target.homepage_res.body =~ PATTERN
|
||||
|
||||
WPScan::Theme.new(
|
||||
Model::Theme.new(
|
||||
Regexp.last_match[1],
|
||||
target,
|
||||
opts.merge(found_by: found_by, confidence: 80)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'medias/attachment_brute_forcing'
|
||||
|
||||
module WPScan
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Medias
|
||||
@@ -15,7 +17,7 @@ module WPScan
|
||||
enumerate(target_urls(opts), opts) do |res|
|
||||
next unless res.code == 200
|
||||
|
||||
found << WPScan::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
|
||||
found << Model::Media.new(res.effective_url, opts.merge(found_by: found_by, confidence: 100))
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'passwords/wp_login'
|
||||
require_relative 'passwords/xml_rpc'
|
||||
require_relative 'passwords/xml_rpc_multicall'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
@@ -10,7 +12,8 @@ module WPScan
|
||||
end
|
||||
|
||||
def valid_credentials?(response)
|
||||
response.code == 302
|
||||
response.code == 302 &&
|
||||
response.headers['Set-Cookie']&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
||||
end
|
||||
|
||||
def errored_response?(response)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Passwords
|
||||
@@ -20,13 +22,13 @@ module WPScan
|
||||
target.multi_call(methods).run
|
||||
end
|
||||
|
||||
# @param [ Array<CMSScanner::User> ] users
|
||||
# @param [ Array<Model::User> ] users
|
||||
# @param [ Array<String> ] passwords
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Boolean ] :show_progression
|
||||
# @option opts [ Integer ] :multicall_max_passwords
|
||||
#
|
||||
# @yield [ CMSScanner::User ] When a valid combination is found
|
||||
# @yield [ Model::User ] When a valid combination is found
|
||||
#
|
||||
# TODO: Make rubocop happy about metrics etc
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'plugin_version/readme'
|
||||
|
||||
module WPScan
|
||||
@@ -7,29 +9,19 @@ module WPScan
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Plugin ] plugin
|
||||
# @param [ Model::Plugin ] plugin
|
||||
def initialize(plugin)
|
||||
finders << PluginVersion::Readme.new(plugin)
|
||||
|
||||
load_specific_finders(plugin)
|
||||
create_and_load_dynamic_versions_finders(plugin)
|
||||
end
|
||||
|
||||
# Load the finders associated with the plugin
|
||||
# Create the dynamic version finders related to the plugin and register them
|
||||
#
|
||||
# @param [ WPScan::Plugin ] plugin
|
||||
def load_specific_finders(plugin)
|
||||
module_name = plugin.classify
|
||||
|
||||
return unless Finders::PluginVersion.constants.include?(module_name)
|
||||
|
||||
mod = Finders::PluginVersion.const_get(module_name)
|
||||
|
||||
mod.constants.each do |constant|
|
||||
c = mod.const_get(constant)
|
||||
|
||||
next unless c.is_a?(Class)
|
||||
|
||||
finders << c.new(plugin)
|
||||
# @param [ Model::Plugin ] plugin
|
||||
def create_and_load_dynamic_versions_finders(plugin)
|
||||
DB::DynamicFinders::Plugin.create_versions_finders(plugin.slug).each do |finder|
|
||||
finders << finder.new(plugin)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module PluginVersion
|
||||
@@ -7,21 +9,23 @@ module WPScan
|
||||
def aggressive(_opts = {})
|
||||
found_by_msg = 'Readme - %s (Aggressive Detection)'
|
||||
|
||||
WPScan::WpItem::READMES.each do |file|
|
||||
url = target.url(file)
|
||||
res = Browser.get(url)
|
||||
# The target(plugin)#readme_url can't be used directly here
|
||||
# as if the --detection-mode is passive, it will always return nil
|
||||
target.potential_readme_filenames.each do |file|
|
||||
res = target.head_and_get(file)
|
||||
|
||||
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
|
||||
|
||||
return numbers.reduce([]) do |a, e|
|
||||
a << WPScan::Version.new(
|
||||
a << Model::Version.new(
|
||||
e[0],
|
||||
found_by: format(found_by_msg, e[1]),
|
||||
confidence: e[2],
|
||||
interesting_entries: [url]
|
||||
interesting_entries: [res.effective_url]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
@@ -48,7 +52,7 @@ module WPScan
|
||||
|
||||
number = Regexp.last_match[1]
|
||||
|
||||
number if number =~ /[0-9]+/
|
||||
number if /[0-9]+/.match?(number)
|
||||
end
|
||||
|
||||
# @param [ String ] body
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'plugins/urls_in_homepage'
|
||||
require_relative 'plugins/known_locations'
|
||||
# From the DynamicFinders
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'BodyPattern'
|
||||
class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class BodyPattern < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
@@ -13,9 +15,9 @@ module WPScan
|
||||
#
|
||||
# @return [ Plugin ] The detected plugin in the response, related to the config
|
||||
def process_response(opts, response, slug, klass, config)
|
||||
return unless response.body =~ config['pattern']
|
||||
return unless response.body&.match?(config['pattern'])
|
||||
|
||||
Plugin.new(
|
||||
Model::Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'Comment'
|
||||
class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class Comment < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
@@ -16,9 +18,9 @@ module WPScan
|
||||
response.html.xpath(config['xpath'] || '//comment()').each do |node|
|
||||
comment = node.text.to_s.strip
|
||||
|
||||
next unless comment =~ config['pattern']
|
||||
next unless comment&.match?(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
return Model::Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'ConfigParser'
|
||||
class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class ConfigParser < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 40
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
@@ -19,7 +21,7 @@ module WPScan
|
||||
# when checking for plugins
|
||||
#
|
||||
|
||||
Plugin.new(
|
||||
Model::Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'HeaderPattern'
|
||||
class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class HeaderPattern < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 30
|
||||
|
||||
# @param [ Hash ] opts
|
||||
@@ -18,7 +20,7 @@ module WPScan
|
||||
configs.each do |klass, config|
|
||||
next unless headers[config['header']] && headers[config['header']].to_s =~ config['pattern']
|
||||
|
||||
found << Plugin.new(
|
||||
found << Model::Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'JavascriptVar'
|
||||
class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class JavascriptVar < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 60
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
@@ -16,7 +18,7 @@ module WPScan
|
||||
response.html.xpath(config['xpath'] || '//script[not(@src)]').each do |node|
|
||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
return Model::Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
@@ -5,6 +7,11 @@ module WPScan
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @return [ Array<Integer> ]
|
||||
def valid_response_codes
|
||||
@valid_response_codes ||= [200, 401, 403, 500].freeze
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
@@ -12,12 +19,10 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, slug|
|
||||
# TODO: follow the location (from enumerate()) and remove the 301 here ?
|
||||
# As a result, it might remove false positive due to redirection to the homepage
|
||||
next unless [200, 401, 403, 301].include?(res.code)
|
||||
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))
|
||||
|
||||
found << WPScan::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||
end
|
||||
|
||||
found
|
||||
@@ -30,10 +35,9 @@ module WPScan
|
||||
def target_urls(opts = {})
|
||||
slugs = opts[:list] || DB::Plugins.vulnerable_slugs
|
||||
urls = {}
|
||||
plugins_url = target.plugins_url
|
||||
|
||||
slugs.each do |slug|
|
||||
urls["#{plugins_url}#{URI.encode(slug)}/"] = slug
|
||||
urls[target.plugin_url(slug)] = slug
|
||||
end
|
||||
|
||||
urls
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from Dynamic Finder 'QueryParameter'
|
||||
class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class QueryParameter < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 10
|
||||
|
||||
def passive(_opts = {})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
@@ -14,7 +16,7 @@ module WPScan
|
||||
found = []
|
||||
|
||||
(items_from_links('plugins') + items_from_codes('plugins')).uniq.sort.each do |slug|
|
||||
found << Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Plugins
|
||||
# Plugins finder from the Dynamic Finder 'Xpath'
|
||||
class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
|
||||
class Xpath < Finders::DynamicFinder::WpItems::Finder
|
||||
DEFAULT_CONFIDENCE = 40
|
||||
|
||||
# @param [ Hash ] opts The options from the #passive, #aggressive methods
|
||||
@@ -16,7 +18,7 @@ module WPScan
|
||||
response.html.xpath(config['xpath']).each do |node|
|
||||
next if config['pattern'] && !node.text.match(config['pattern'])
|
||||
|
||||
return Plugin.new(
|
||||
return Model::Plugin.new(
|
||||
slug,
|
||||
target,
|
||||
opts.merge(found_by: found_by(klass), confidence: config['confidence'] || DEFAULT_CONFIDENCE)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'theme_version/style'
|
||||
require_relative 'theme_version/woo_framework_meta_generator'
|
||||
|
||||
@@ -8,31 +10,21 @@ module WPScan
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Theme ] theme
|
||||
# @param [ Model::Theme ] theme
|
||||
def initialize(theme)
|
||||
finders <<
|
||||
ThemeVersion::Style.new(theme) <<
|
||||
ThemeVersion::WooFrameworkMetaGenerator.new(theme)
|
||||
|
||||
load_specific_finders(theme)
|
||||
create_and_load_dynamic_versions_finders(theme)
|
||||
end
|
||||
|
||||
# Load the finders associated with the theme
|
||||
# Create the dynamic version finders related to the theme and register them
|
||||
#
|
||||
# @param [ WPScan::Theme ] theme
|
||||
def load_specific_finders(theme)
|
||||
module_name = theme.classify
|
||||
|
||||
return unless Finders::ThemeVersion.constants.include?(module_name)
|
||||
|
||||
mod = Finders::ThemeVersion.const_get(module_name)
|
||||
|
||||
mod.constants.each do |constant|
|
||||
c = mod.const_get(constant)
|
||||
|
||||
next unless c.is_a?(Class)
|
||||
|
||||
finders << c.new(theme)
|
||||
# @param [ Model::Theme ] theme
|
||||
def create_and_load_dynamic_versions_finders(theme)
|
||||
DB::DynamicFinders::Theme.create_versions_finders(theme.slug).each do |finder|
|
||||
finders << finder.new(theme)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
@@ -30,7 +32,7 @@ module WPScan
|
||||
def style_version
|
||||
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
|
||||
|
||||
WPScan::Version.new(
|
||||
Model::Version.new(
|
||||
Regexp.last_match[1],
|
||||
found_by: found_by,
|
||||
confidence: 80,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module ThemeVersion
|
||||
@@ -11,7 +13,7 @@ module WPScan
|
||||
|
||||
return unless Regexp.last_match[1] == target.slug
|
||||
|
||||
WPScan::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
|
||||
Model::Version.new(Regexp.last_match[2], found_by: found_by, confidence: 80)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'themes/urls_in_homepage'
|
||||
require_relative 'themes/known_locations'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
@@ -5,6 +7,11 @@ module WPScan
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @return [ Array<Integer> ]
|
||||
def valid_response_codes
|
||||
@valid_response_codes ||= [200, 401, 403, 500].freeze
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list
|
||||
#
|
||||
@@ -12,12 +19,10 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, slug|
|
||||
# TODO: follow the location (from enumerate()) and remove the 301 here ?
|
||||
# As a result, it might remove false positive due to redirection to the homepage
|
||||
next unless [200, 401, 403, 301].include?(res.code)
|
||||
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))
|
||||
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
|
||||
end
|
||||
|
||||
found
|
||||
@@ -28,12 +33,11 @@ module WPScan
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def target_urls(opts = {})
|
||||
slugs = opts[:list] || DB::Themes.vulnerable_slugs
|
||||
urls = {}
|
||||
themes_url = target.url('wp-content/themes/')
|
||||
slugs = opts[:list] || DB::Themes.vulnerable_slugs
|
||||
urls = {}
|
||||
|
||||
slugs.each do |slug|
|
||||
urls["#{themes_url}#{URI.encode(slug)}/"] = slug
|
||||
urls[target.theme_url(slug)] = slug
|
||||
end
|
||||
|
||||
urls
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Themes
|
||||
@@ -12,7 +14,7 @@ module WPScan
|
||||
found = []
|
||||
|
||||
(items_from_links('themes') + items_from_codes('themes')).uniq.sort.each do |slug|
|
||||
found << WPScan::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'timthumb_version/bad_request'
|
||||
|
||||
module WPScan
|
||||
@@ -7,7 +9,7 @@ module WPScan
|
||||
class Base
|
||||
include CMSScanner::Finders::UniqueFinder
|
||||
|
||||
# @param [ WPScan::Timthumb ] target
|
||||
# @param [ Model::Timthumb ] target
|
||||
def initialize(target)
|
||||
finders << TimthumbVersion::BadRequest.new(target)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module TimthumbVersion
|
||||
@@ -8,7 +10,7 @@ module WPScan
|
||||
def aggressive(_opts = {})
|
||||
return unless Browser.get(target.url).body =~ /(TimThumb version\s*: ([^<]+))/
|
||||
|
||||
WPScan::Version.new(
|
||||
Model::Version.new(
|
||||
Regexp.last_match[2],
|
||||
found_by: 'Bad Request (Aggressive Detection)',
|
||||
confidence: 90,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'timthumbs/known_locations'
|
||||
|
||||
module WPScan
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Timthumbs
|
||||
# Known Locations Timthumbs Finder
|
||||
# Note: A vulnerable version, 2.8.13 can be found here:
|
||||
# https://github.com/GabrielGil/TimThumb/blob/980c3d6a823477761570475e8b83d3e9fcd2d7ae/timthumb.php
|
||||
class KnownLocations < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @return [ Array<Integer> ]
|
||||
def valid_response_codes
|
||||
@valid_response_codes ||= [400]
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ String ] :list Mandatory
|
||||
#
|
||||
@@ -12,10 +21,10 @@ module WPScan
|
||||
def aggressive(opts = {})
|
||||
found = []
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res|
|
||||
next unless res.code == 400 && res.body =~ /no image specified/i
|
||||
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
|
||||
next unless /no image specified/i.match?(res.body)
|
||||
|
||||
found << WPScan::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
||||
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'users/author_posts'
|
||||
require_relative 'users/wp_json_api'
|
||||
require_relative 'users/oembed_api'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
@@ -5,6 +7,11 @@ module WPScan
|
||||
class AuthorIdBruteForcing < CMSScanner::Finders::Finder
|
||||
include CMSScanner::Finders::Finder::Enumerator
|
||||
|
||||
# @return [ Array<Integer> ]
|
||||
def valid_response_codes
|
||||
@valid_response_codes ||= [200, 301, 302]
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Range ] :range Mandatory
|
||||
#
|
||||
@@ -13,12 +20,12 @@ module WPScan
|
||||
found = []
|
||||
found_by_msg = 'Author Id Brute Forcing - %s (Aggressive Detection)'
|
||||
|
||||
enumerate(target_urls(opts), opts) do |res, id|
|
||||
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, id|
|
||||
username, found_by, confidence = potential_username(res)
|
||||
|
||||
next unless username
|
||||
|
||||
found << CMSScanner::User.new(
|
||||
found << Model::User.new(
|
||||
username,
|
||||
id: id,
|
||||
found_by: format(found_by_msg, found_by),
|
||||
@@ -47,7 +54,7 @@ module WPScan
|
||||
super(opts.merge(title: ' Brute Forcing Author IDs -'))
|
||||
end
|
||||
|
||||
def request_params
|
||||
def full_request_params
|
||||
{ followlocation: true }
|
||||
end
|
||||
|
||||
@@ -76,8 +83,8 @@ module WPScan
|
||||
# @return [ String, nil ] The username found
|
||||
def username_from_response(res)
|
||||
# Permalink enabled
|
||||
target.in_scope_urls(res, '//link/@href|//a/@href') do |url|
|
||||
username = username_from_author_url(url)
|
||||
target.in_scope_uris(res, '//link/@href|//a/@href') do |uri|
|
||||
username = username_from_author_url(uri.to_s)
|
||||
return username if username
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
@@ -10,7 +12,7 @@ module WPScan
|
||||
found_by_msg = 'Author Posts - %s (Passive Detection)'
|
||||
|
||||
usernames(opts).reduce([]) do |a, e|
|
||||
a << CMSScanner::User.new(
|
||||
a << Model::User.new(
|
||||
e[0],
|
||||
found_by: format(found_by_msg, e[1]),
|
||||
confidence: e[2]
|
||||
@@ -43,12 +45,10 @@ module WPScan
|
||||
def potential_usernames(res)
|
||||
usernames = []
|
||||
|
||||
target.in_scope_urls(res, '//a/@href') do |url, node|
|
||||
uri = Addressable::URI.parse(url)
|
||||
|
||||
target.in_scope_uris(res, '//a/@href') do |uri, node|
|
||||
if uri.path =~ %r{/author/([^/\b]+)/?\z}i
|
||||
usernames << [Regexp.last_match[1], 'Author Pattern', 100]
|
||||
elsif uri.query =~ /author=[0-9]+/
|
||||
elsif /author=[0-9]+/.match?(uri.query)
|
||||
usernames << [node.text.to_s.strip, 'Display Name', 30]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
@@ -22,9 +24,9 @@ module WPScan
|
||||
|
||||
return found if error.empty? # Protection plugin / error disabled
|
||||
|
||||
next unless error =~ /The password you entered for the username|Incorrect Password/i
|
||||
next unless /The password you entered for the username|Incorrect Password/i.match?(error)
|
||||
|
||||
found << CMSScanner::User.new(username, found_by: found_by, confidence: 100)
|
||||
found << Model::User.new(username, found_by: found_by, confidence: 100)
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
@@ -21,10 +23,10 @@ module WPScan
|
||||
|
||||
return [] unless details
|
||||
|
||||
[CMSScanner::User.new(details[0],
|
||||
found_by: format(found_by_msg, details[1]),
|
||||
confidence: details[2],
|
||||
interesting_entries: [api_url])]
|
||||
[Model::User.new(details[0],
|
||||
found_by: format(found_by_msg, details[1]),
|
||||
confidence: details[2],
|
||||
interesting_entries: [api_url])]
|
||||
rescue JSON::ParserError
|
||||
[]
|
||||
end
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
# Users disclosed from the dc:creator field in the RSS
|
||||
# The names disclosed are display names, however depending on the configuration of the blog,
|
||||
# they can be the same than usernames
|
||||
class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
|
||||
class RSSGenerator < Finders::WpVersion::RSSGenerator
|
||||
def process_urls(urls, _opts = {})
|
||||
found = []
|
||||
|
||||
@@ -17,20 +19,20 @@ module WPScan
|
||||
|
||||
begin
|
||||
res.xml.xpath('//item/dc:creator').each do |node|
|
||||
potential_username = node.text.to_s
|
||||
username = node.text.to_s
|
||||
|
||||
# Ignoring potential username longer than 60 characters and containing accents
|
||||
# as they are considered invalid. See https://github.com/wpscanteam/wpscan/issues/1215
|
||||
next if potential_username.length > 60 || potential_username =~ /[^\x00-\x7F]/
|
||||
next if username.strip.empty? || username.length > 60 || username =~ /[^\x00-\x7F]/
|
||||
|
||||
potential_usernames << potential_username
|
||||
potential_usernames << username
|
||||
end
|
||||
rescue Nokogiri::XML::XPath::SyntaxError
|
||||
next
|
||||
end
|
||||
|
||||
potential_usernames.uniq.each do |potential_username|
|
||||
found << CMSScanner::User.new(potential_username, found_by: found_by, confidence: 50)
|
||||
potential_usernames.uniq.each do |username|
|
||||
found << Model::User.new(username, found_by: found_by, confidence: 50)
|
||||
end
|
||||
|
||||
break
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
@@ -41,11 +43,11 @@ module WPScan
|
||||
found = []
|
||||
|
||||
JSON.parse(response.body)&.each do |user|
|
||||
found << CMSScanner::User.new(user['slug'],
|
||||
id: user['id'],
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [response.effective_url])
|
||||
found << Model::User.new(user['slug'],
|
||||
id: user['id'],
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [response.effective_url])
|
||||
end
|
||||
|
||||
found
|
||||
@@ -53,7 +55,13 @@ module WPScan
|
||||
|
||||
# @return [ String ] The URL of the API listing the Users
|
||||
def api_url
|
||||
@api_url ||= target.url('wp-json/wp/v2/users/')
|
||||
return @api_url if @api_url
|
||||
|
||||
target.in_scope_uris(target.homepage_res, "//link[@rel='https://api.w.org/']/@href").each do |uri|
|
||||
return @api_url = uri.join('wp/v2/users/').to_s if uri.path.include?('wp-json')
|
||||
end
|
||||
|
||||
@api_url = target.url('wp-json/wp/v2/users/')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module Users
|
||||
@@ -15,10 +17,10 @@ module WPScan
|
||||
|
||||
next unless username && !username.strip.empty?
|
||||
|
||||
found << CMSScanner::User.new(username,
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [sitemap_url])
|
||||
found << Model::User.new(username,
|
||||
found_by: found_by,
|
||||
confidence: 100,
|
||||
interesting_entries: [sitemap_url])
|
||||
end
|
||||
|
||||
found
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'wp_items/urls_in_homepage'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpItems
|
||||
@@ -10,8 +12,8 @@ module WPScan
|
||||
def items_from_links(type, uniq = true)
|
||||
found = []
|
||||
|
||||
target.in_scope_urls(target.homepage_res) do |url|
|
||||
next unless url =~ item_attribute_pattern(type)
|
||||
target.in_scope_uris(target.homepage_res) do |uri|
|
||||
next unless uri.to_s =~ item_attribute_pattern(type)
|
||||
|
||||
found << Regexp.last_match[1]
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'wp_version/rss_generator'
|
||||
require_relative 'wp_version/atom_generator'
|
||||
require_relative 'wp_version/rdf_generator'
|
||||
@@ -26,7 +28,7 @@ module WPScan
|
||||
# @param [ WPScan::Target ] target
|
||||
def initialize(target)
|
||||
(%w[RSSGenerator AtomGenerator RDFGenerator] +
|
||||
WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
|
||||
DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
|
||||
%w[Readme UniqueFingerprinting]
|
||||
).each do |finder_name|
|
||||
finders << WpVersion.const_get(finder_name.to_sym).new(target)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
@@ -13,9 +15,9 @@ module WPScan
|
||||
|
||||
number = Regexp.last_match(1)
|
||||
|
||||
return unless WPScan::WpVersion.valid?(number)
|
||||
return unless Model::WpVersion.valid?(number)
|
||||
|
||||
WPScan::WpVersion.new(
|
||||
Model::WpVersion.new(
|
||||
number,
|
||||
found_by: 'Readme (Aggressive Detection)',
|
||||
# Since WP 4.7, the Readme only contains the major version (ie 4.7, 4.8 etc)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Finders
|
||||
module WpVersion
|
||||
@@ -11,7 +13,7 @@ module WPScan
|
||||
hydra.abort
|
||||
progress_bar.finish
|
||||
|
||||
return WPScan::WpVersion.new(
|
||||
return Model::WpVersion.new(
|
||||
version_number,
|
||||
found_by: 'Unique Fingerprinting (Aggressive Detection)',
|
||||
confidence: 100,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
module Model
|
||||
include CMSScanner::Model
|
||||
end
|
||||
end
|
||||
|
||||
require_relative 'models/interesting_finding'
|
||||
require_relative 'models/wp_version'
|
||||
require_relative 'models/xml_rpc'
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# Config Backup
|
||||
class ConfigBackup < InterestingFinding
|
||||
module Model
|
||||
# Config Backup
|
||||
class ConfigBackup < InterestingFinding
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# DB Export
|
||||
class DbExport < InterestingFinding
|
||||
module Model
|
||||
# DB Export
|
||||
class DbExport < InterestingFinding
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,45 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# Custom class to include the WPScan::References module
|
||||
class InterestingFinding < CMSScanner::InterestingFinding
|
||||
include References
|
||||
end
|
||||
module Model
|
||||
# Custom class to include the WPScan::References module
|
||||
class InterestingFinding < CMSScanner::Model::InterestingFinding
|
||||
include References
|
||||
end
|
||||
|
||||
#
|
||||
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
|
||||
#
|
||||
class BackupDB < InterestingFinding
|
||||
end
|
||||
#
|
||||
# Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
|
||||
#
|
||||
class BackupDB < InterestingFinding
|
||||
end
|
||||
|
||||
class DebugLog < InterestingFinding
|
||||
end
|
||||
class DebugLog < InterestingFinding
|
||||
end
|
||||
|
||||
class DuplicatorInstallerLog < InterestingFinding
|
||||
end
|
||||
class DuplicatorInstallerLog < InterestingFinding
|
||||
end
|
||||
|
||||
class EmergencyPwdResetScript < InterestingFinding
|
||||
end
|
||||
class EmergencyPwdResetScript < InterestingFinding
|
||||
end
|
||||
|
||||
class FullPathDisclosure < InterestingFinding
|
||||
end
|
||||
class FullPathDisclosure < InterestingFinding
|
||||
end
|
||||
|
||||
class MuPlugins < InterestingFinding
|
||||
end
|
||||
class MuPlugins < InterestingFinding
|
||||
end
|
||||
|
||||
class Multisite < InterestingFinding
|
||||
end
|
||||
class Multisite < InterestingFinding
|
||||
end
|
||||
|
||||
class Readme < InterestingFinding
|
||||
end
|
||||
class Readme < InterestingFinding
|
||||
end
|
||||
|
||||
class Registration < InterestingFinding
|
||||
end
|
||||
class Registration < InterestingFinding
|
||||
end
|
||||
|
||||
class TmmDbMigrate < InterestingFinding
|
||||
end
|
||||
class TmmDbMigrate < InterestingFinding
|
||||
end
|
||||
|
||||
class UploadDirectoryListing < InterestingFinding
|
||||
end
|
||||
class UploadDirectoryListing < InterestingFinding
|
||||
end
|
||||
|
||||
class UploadSQLDump < InterestingFinding
|
||||
class UploadSQLDump < InterestingFinding
|
||||
end
|
||||
|
||||
class WPCron < InterestingFinding
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# Media
|
||||
class Media < InterestingFinding
|
||||
module Model
|
||||
# Media
|
||||
class Media < InterestingFinding
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# WordPress Plugin
|
||||
class Plugin < WpItem
|
||||
# See WpItem
|
||||
def initialize(slug, blog, opts = {})
|
||||
super(slug, blog, opts)
|
||||
module Model
|
||||
# WordPress Plugin
|
||||
class Plugin < WpItem
|
||||
# See WpItem
|
||||
def initialize(slug, blog, opts = {})
|
||||
super(slug, blog, opts)
|
||||
|
||||
@uri = Addressable::URI.parse(blog.url("wp-content/plugins/#{slug}/"))
|
||||
end
|
||||
# To be used by #head_and_get
|
||||
# If custom wp-content, it will be replaced by blog#url
|
||||
@path_from_blog = "wp-content/plugins/#{slug}/"
|
||||
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
DB::Plugin.db_data(slug)
|
||||
end
|
||||
@uri = Addressable::URI.parse(blog.url(path_from_blog))
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WPScan::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::PluginVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
@db_data ||= DB::Plugin.db_data(slug)
|
||||
end
|
||||
|
||||
@version
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Model::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::PluginVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ Array<String> ]
|
||||
def potential_readme_filenames
|
||||
@potential_readme_filenames ||= [*(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,99 +1,107 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# WordPress Theme
|
||||
class Theme < WpItem
|
||||
attr_reader :style_url, :style_name, :style_uri, :author, :author_uri, :template, :description,
|
||||
:license, :license_uri, :tags, :text_domain
|
||||
module Model
|
||||
# WordPress Theme
|
||||
class Theme < WpItem
|
||||
attr_reader :style_url, :style_name, :style_uri, :author, :author_uri, :template, :description,
|
||||
:license, :license_uri, :tags, :text_domain
|
||||
|
||||
# See WpItem
|
||||
def initialize(slug, blog, opts = {})
|
||||
super(slug, blog, opts)
|
||||
# See WpItem
|
||||
def initialize(slug, blog, opts = {})
|
||||
super(slug, blog, opts)
|
||||
|
||||
@uri = Addressable::URI.parse(blog.url("wp-content/themes/#{slug}/"))
|
||||
@style_url = opts[:style_url] || url('style.css')
|
||||
# To be used by #head_and_get
|
||||
# If custom wp-content, it will be replaced by blog#url
|
||||
@path_from_blog = "wp-content/themes/#{slug}/"
|
||||
|
||||
parse_style
|
||||
end
|
||||
@uri = Addressable::URI.parse(blog.url(path_from_blog))
|
||||
@style_url = opts[:style_url] || url('style.css')
|
||||
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
DB::Theme.db_data(slug)
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WPScan::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::ThemeVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ Theme ]
|
||||
def parent_theme
|
||||
return unless template
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
|
||||
|
||||
opts = detection_opts.merge(
|
||||
style_url: url(Regexp.last_match[1]),
|
||||
found_by: 'Parent Themes (Passive Detection)',
|
||||
confidence: 100
|
||||
).merge(version_detection: version_detection_opts)
|
||||
|
||||
self.class.new(template, blog, opts)
|
||||
end
|
||||
|
||||
# @param [ Integer ] depth
|
||||
#
|
||||
# @retun [ Array<Theme> ]
|
||||
def parent_themes(depth = 3)
|
||||
theme = self
|
||||
found = []
|
||||
|
||||
(1..depth).each do |_|
|
||||
parent = theme.parent_theme
|
||||
|
||||
break unless parent
|
||||
|
||||
found << parent
|
||||
theme = parent
|
||||
parse_style
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def style_body
|
||||
@style_body ||= Browser.get(style_url).body
|
||||
end
|
||||
|
||||
def parse_style
|
||||
{
|
||||
style_name: 'Theme Name',
|
||||
style_uri: 'Theme URI',
|
||||
author: 'Author',
|
||||
author_uri: 'Author URI',
|
||||
template: 'Template',
|
||||
description: 'Description',
|
||||
license: 'License',
|
||||
license_uri: 'License URI',
|
||||
tags: 'Tags',
|
||||
text_domain: 'Text Domain'
|
||||
}.each do |attribute, tag|
|
||||
instance_variable_set(:"@#{attribute}", parse_style_tag(style_body, tag))
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
@db_data ||= DB::Theme.db_data(slug)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] bofy
|
||||
# @param [ String ] tag
|
||||
#
|
||||
# @return [ String ]
|
||||
def parse_style_tag(body, tag)
|
||||
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Model::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::ThemeVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
value && !value.strip.empty? ? value.strip : nil
|
||||
end
|
||||
@version
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
super(other) && style_url == other.style_url
|
||||
# @return [ Theme ]
|
||||
def parent_theme
|
||||
return unless template
|
||||
return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
|
||||
|
||||
opts = detection_opts.merge(
|
||||
style_url: url(Regexp.last_match[1]),
|
||||
found_by: 'Parent Themes (Passive Detection)',
|
||||
confidence: 100
|
||||
).merge(version_detection: version_detection_opts)
|
||||
|
||||
self.class.new(template, blog, opts)
|
||||
end
|
||||
|
||||
# @param [ Integer ] depth
|
||||
#
|
||||
# @retun [ Array<Theme> ]
|
||||
def parent_themes(depth = 3)
|
||||
theme = self
|
||||
found = []
|
||||
|
||||
(1..depth).each do |_|
|
||||
parent = theme.parent_theme
|
||||
|
||||
break unless parent
|
||||
|
||||
found << parent
|
||||
theme = parent
|
||||
end
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
def style_body
|
||||
@style_body ||= Browser.get(style_url).body
|
||||
end
|
||||
|
||||
def parse_style
|
||||
{
|
||||
style_name: 'Theme Name',
|
||||
style_uri: 'Theme URI',
|
||||
author: 'Author',
|
||||
author_uri: 'Author URI',
|
||||
template: 'Template',
|
||||
description: 'Description',
|
||||
license: 'License',
|
||||
license_uri: 'License URI',
|
||||
tags: 'Tags',
|
||||
text_domain: 'Text Domain'
|
||||
}.each do |attribute, tag|
|
||||
instance_variable_set(:"@#{attribute}", parse_style_tag(style_body, tag))
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] bofy
|
||||
# @param [ String ] tag
|
||||
#
|
||||
# @return [ String ]
|
||||
def parse_style_tag(body, tag)
|
||||
value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
|
||||
|
||||
value && !value.strip.empty? ? value.strip : nil
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
super(other) && style_url == other.style_url
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,71 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# Timthumb
|
||||
class Timthumb < InterestingFinding
|
||||
include Vulnerable
|
||||
module Model
|
||||
# Timthumb
|
||||
class Timthumb < InterestingFinding
|
||||
include Vulnerable
|
||||
|
||||
attr_reader :version_detection_opts
|
||||
attr_reader :version_detection_opts
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Symbol ] :mode The mode to use to detect the version
|
||||
def initialize(url, opts = {})
|
||||
super(url, opts)
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Symbol ] :mode The mode to use to detect the version
|
||||
def initialize(url, opts = {})
|
||||
super(url, opts)
|
||||
|
||||
@version_detection_opts = opts[:version_detection] || {}
|
||||
end
|
||||
@version_detection_opts = opts[:version_detection] || {}
|
||||
end
|
||||
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ WPScan::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::TimthumbVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
# @param [ Hash ] opts
|
||||
#
|
||||
# @return [ Model::Version, false ]
|
||||
def version(opts = {})
|
||||
@version = Finders::TimthumbVersion::Base.find(self, version_detection_opts.merge(opts)) if @version.nil?
|
||||
|
||||
@version
|
||||
end
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ Array<Vulnerability> ]
|
||||
def vulnerabilities
|
||||
vulns = []
|
||||
# @return [ Array<Vulnerability> ]
|
||||
def vulnerabilities
|
||||
vulns = []
|
||||
|
||||
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 << 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
|
||||
end
|
||||
vulns
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE in the <= 1.32
|
||||
def rce_132_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 1.32 Remote Code Execution',
|
||||
{ exploitdb: ['17602'] },
|
||||
'RCE',
|
||||
'1.33'
|
||||
)
|
||||
end
|
||||
# @return [ Vulnerability ] The RCE in the <= 1.32
|
||||
def rce_132_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 1.32 Remote Code Execution',
|
||||
{ exploitdb: ['17602'] },
|
||||
'RCE',
|
||||
'1.33'
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE due to the WebShot in the > 1.35 (or >= 2.0) and <= 2.8.13
|
||||
def rce_webshot_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||
{
|
||||
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
|
||||
cve: '2014-4663'
|
||||
},
|
||||
'RCE',
|
||||
'2.8.14'
|
||||
)
|
||||
end
|
||||
# @return [ Vulnerability ] The RCE due to the WebShot in the > 1.35 (or >= 2.0) and <= 2.8.13
|
||||
def rce_webshot_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||
{
|
||||
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
|
||||
cve: '2014-4663'
|
||||
},
|
||||
'RCE',
|
||||
'2.8.14'
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def webshot_enabled?
|
||||
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
|
||||
# @return [ Boolean ]
|
||||
def webshot_enabled?
|
||||
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
|
||||
|
||||
res.body =~ /WEBSHOT_ENABLED == true/ ? false : true
|
||||
end
|
||||
/WEBSHOT_ENABLED == true/.match?(res.body) ? false : true
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
||||
def default_allowed_domains
|
||||
%w[flickr.com picasa.com img.youtube.com upload.wikimedia.org]
|
||||
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
||||
def default_allowed_domains
|
||||
%w[flickr.com picasa.com img.youtube.com upload.wikimedia.org]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,158 +1,175 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# WpItem (superclass of Plugin & Theme)
|
||||
class WpItem
|
||||
include Vulnerable
|
||||
include Finders::Finding
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
include CMSScanner::Target::Server::Generic
|
||||
module Model
|
||||
# WpItem (superclass of Plugin & Theme)
|
||||
class WpItem
|
||||
include Vulnerable
|
||||
include Finders::Finding
|
||||
include CMSScanner::Target::Platform::PHP
|
||||
include CMSScanner::Target::Server::Generic
|
||||
|
||||
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
|
||||
CHANGELOGS = %w[changelog.txt CHANGELOG.md changelog.md].freeze
|
||||
# Most common readme filenames, based on checking all public plugins and themes.
|
||||
READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
|
||||
|
||||
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :db_data
|
||||
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
|
||||
|
||||
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_urls, to: :blog
|
||||
delegate :homepage_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, to: :blog
|
||||
|
||||
# @param [ String ] slug The plugin/theme slug
|
||||
# @param [ Target ] blog The targeted blog
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Symbol ] :mode The detection mode to use
|
||||
# @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)
|
||||
@blog = blog
|
||||
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
||||
# @param [ String ] slug The plugin/theme slug
|
||||
# @param [ Target ] blog The targeted blog
|
||||
# @param [ Hash ] opts
|
||||
# @option opts [ Symbol ] :mode The detection mode to use
|
||||
# @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)
|
||||
@blog = blog
|
||||
@uri = Addressable::URI.parse(opts[:url]) if opts[:url]
|
||||
|
||||
@detection_opts = { mode: opts[:mode] }
|
||||
@version_detection_opts = opts[:version_detection] || {}
|
||||
@detection_opts = { mode: opts[:mode] }
|
||||
@version_detection_opts = opts[:version_detection] || {}
|
||||
|
||||
parse_finding_options(opts)
|
||||
end
|
||||
|
||||
# @return [ Array<Vulnerabily> ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
vulnerability = Vulnerability.load_from_json(json_vuln)
|
||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||
parse_finding_options(opts)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
# @return [ Array<Vulnerabily> ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
# Checks if the wp_item is vulnerable to a specific vulnerability
|
||||
#
|
||||
# @param [ Vulnerability ] vuln Vulnerability to check the item against
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def vulnerable_to?(vuln)
|
||||
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
|
||||
@vulnerabilities = []
|
||||
|
||||
version < vuln.fixed_in
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def latest_version
|
||||
@latest_version ||= db_data['latest_version'] ? WPScan::Version.new(db_data['latest_version']) : nil
|
||||
end
|
||||
|
||||
# Not used anywhere ATM
|
||||
# @return [ Boolean ]
|
||||
def popular?
|
||||
@popular ||= db_data['popular']
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def last_updated
|
||||
@last_updated ||= db_data['last_updated']
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def outdated?
|
||||
@outdated ||= if version && latest_version
|
||||
version < latest_version
|
||||
else
|
||||
false
|
||||
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 ]
|
||||
def url(path = nil)
|
||||
return unless @uri
|
||||
return @uri.to_s unless path
|
||||
|
||||
@uri.join(URI.encode(path)).to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
self.class == other.class && slug == other.slug
|
||||
end
|
||||
|
||||
def to_s
|
||||
slug
|
||||
end
|
||||
|
||||
# @return [ Symbol ] The Class symbol associated to the item
|
||||
def classify
|
||||
@classify ||= classify_slug(slug)
|
||||
end
|
||||
|
||||
# @return [ String ] The readme url if found
|
||||
def readme_url
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
if @readme_url.nil?
|
||||
READMES.each do |path|
|
||||
return @readme_url = url(path) if Browser.get(url(path)).code == 200
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
vulnerability = Vulnerability.load_from_json(json_vuln)
|
||||
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
|
||||
@readme_url
|
||||
end
|
||||
# Checks if the wp_item is vulnerable to a specific vulnerability
|
||||
#
|
||||
# @param [ Vulnerability ] vuln Vulnerability to check the item against
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def vulnerable_to?(vuln)
|
||||
return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
|
||||
|
||||
# @return [ String, false ] The changelog urr if found
|
||||
def changelog_url
|
||||
return if detection_opts[:mode] == :passive
|
||||
version < vuln.fixed_in
|
||||
end
|
||||
|
||||
if @changelog_url.nil?
|
||||
CHANGELOGS.each do |path|
|
||||
return @changelog_url = url(path) if Browser.get(url(path)).code == 200
|
||||
# @return [ String ]
|
||||
def latest_version
|
||||
@latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
|
||||
end
|
||||
|
||||
# Not used anywhere ATM
|
||||
# @return [ Boolean ]
|
||||
def popular?
|
||||
@popular ||= db_data['popular']
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def last_updated
|
||||
@last_updated ||= db_data['last_updated']
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def outdated?
|
||||
@outdated ||= if version && latest_version
|
||||
version < latest_version
|
||||
else
|
||||
false
|
||||
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 ]
|
||||
def url(path = nil)
|
||||
return unless @uri
|
||||
return @uri.to_s unless path
|
||||
|
||||
@uri.join(URI.encode(path)).to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
self.class == other.class && slug == other.slug
|
||||
end
|
||||
|
||||
def to_s
|
||||
slug
|
||||
end
|
||||
|
||||
# @return [ Symbol ] The Class symbol associated to the item
|
||||
def classify
|
||||
@classify ||= classify_slug(slug)
|
||||
end
|
||||
|
||||
# @return [ String, False ] The readme url if found, false otherwise
|
||||
def readme_url
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
return @readme_url unless @readme_url.nil?
|
||||
|
||||
potential_readme_filenames.each do |path|
|
||||
t_url = url(path)
|
||||
|
||||
return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200
|
||||
end
|
||||
|
||||
@readme_url = false
|
||||
end
|
||||
|
||||
@changelog_url
|
||||
end
|
||||
def potential_readme_filenames
|
||||
@potential_readme_filenames ||= READMES
|
||||
end
|
||||
|
||||
# @param [ String ] path
|
||||
# @param [ Hash ] params The request params
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def directory_listing?(path = nil, params = {})
|
||||
return if detection_opts[:mode] == :passive
|
||||
# @param [ String ] path
|
||||
# @param [ Hash ] params The request params
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def directory_listing?(path = nil, params = {})
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
super(path, params)
|
||||
end
|
||||
super(path, params)
|
||||
end
|
||||
|
||||
# @param [ String ] path
|
||||
# @param [ Hash ] params The request params
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def error_log?(path = 'error_log', params = {})
|
||||
return if detection_opts[:mode] == :passive
|
||||
# @param [ String ] path
|
||||
# @param [ Hash ] params The request params
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def error_log?(path = 'error_log', params = {})
|
||||
return if detection_opts[:mode] == :passive
|
||||
|
||||
super(path, params)
|
||||
super(path, params)
|
||||
end
|
||||
|
||||
# See CMSScanner::Target#head_and_get
|
||||
#
|
||||
# This is used by the error_log? above in the super()
|
||||
# to have the correct path (ie readme.txt checked from the plugin/theme location
|
||||
# and not from the blog root). Could also be used in finders
|
||||
#
|
||||
# @param [ String ] path
|
||||
# @param [ Array<String> ] codes
|
||||
# @param [ Hash ] params The requests params
|
||||
# @option params [ Hash ] :head Request params for the HEAD
|
||||
# @option params [ hash ] :get Request params for the GET
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def head_and_get(path, codes = [200], params = {})
|
||||
final_path = +@path_from_blog
|
||||
final_path << URI.encode(path) unless path.nil?
|
||||
|
||||
blog.head_and_get(final_path, codes, params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,64 +1,67 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WPScan
|
||||
# WP Version
|
||||
class WpVersion < CMSScanner::Version
|
||||
include Vulnerable
|
||||
module Model
|
||||
# WP Version
|
||||
class WpVersion < CMSScanner::Model::Version
|
||||
include Vulnerable
|
||||
|
||||
def initialize(number, opts = {})
|
||||
raise InvalidWordPressVersion unless WpVersion.valid?(number.to_s)
|
||||
def initialize(number, opts = {})
|
||||
raise Error::InvalidWordPressVersion unless WpVersion.valid?(number.to_s)
|
||||
|
||||
super(number, opts)
|
||||
end
|
||||
super(number, opts)
|
||||
end
|
||||
|
||||
# @param [ String ] number
|
||||
#
|
||||
# @return [ Boolean ] true if the number is a valid WP version, false otherwise
|
||||
def self.valid?(number)
|
||||
all.include?(number)
|
||||
end
|
||||
# @param [ String ] number
|
||||
#
|
||||
# @return [ Boolean ] true if the number is a valid WP version, false otherwise
|
||||
def self.valid?(number)
|
||||
all.include?(number)
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] All the version numbers
|
||||
def self.all
|
||||
return @all_numbers if @all_numbers
|
||||
# @return [ Array<String> ] All the version numbers
|
||||
def self.all
|
||||
return @all_numbers if @all_numbers
|
||||
|
||||
@all_numbers = []
|
||||
@all_numbers = []
|
||||
|
||||
DB::Fingerprints.wp_fingerprints.each_value do |fp|
|
||||
fp.each_value do |versions|
|
||||
versions.each do |version|
|
||||
@all_numbers << version unless @all_numbers.include?(version)
|
||||
end
|
||||
DB::Fingerprints.wp_fingerprints.each_value do |fp|
|
||||
@all_numbers << fp.values
|
||||
end
|
||||
|
||||
# @all_numbers.flatten.uniq.sort! {} doesn't produce the same result here.
|
||||
@all_numbers.flatten!
|
||||
@all_numbers.uniq!
|
||||
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
|
||||
end
|
||||
|
||||
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
|
||||
end
|
||||
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
DB::Version.db_data(number)
|
||||
end
|
||||
|
||||
# @return [ Array<Vulnerability> ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
@vulnerabilities = []
|
||||
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
||||
# @return [ JSON ]
|
||||
def db_data
|
||||
@db_data ||= DB::Version.db_data(number)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
# @return [ Array<Vulnerability> ]
|
||||
def vulnerabilities
|
||||
return @vulnerabilities if @vulnerabilities
|
||||
|
||||
# @return [ String ]
|
||||
def release_date
|
||||
@release_date ||= db_data['release_date'] || 'Unknown'
|
||||
end
|
||||
@vulnerabilities = []
|
||||
|
||||
# @return [ String ]
|
||||
def status
|
||||
@status ||= db_data['status'] || 'Unknown'
|
||||
[*db_data['vulnerabilities']].each do |json_vuln|
|
||||
@vulnerabilities << Vulnerability.load_from_json(json_vuln)
|
||||
end
|
||||
|
||||
@vulnerabilities
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def release_date
|
||||
@release_date ||= db_data['release_date'] || 'Unknown'
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def status
|
||||
@status ||= db_data['status'] || 'Unknown'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user