Compare commits

..

3 Commits

Author SHA1 Message Date
erwanlr
82db02a688 Updates spec for #1342 2019-05-03 14:25:17 +01:00
erwanlr
2c07de8c6b Updates class comment 2019-05-03 14:23:04 +01:00
erwanlr
4b0b8fa624 Fixes #1342 2019-05-03 14:04:50 +01:00
2189 changed files with 34596 additions and 1044723 deletions

View File

@@ -1,7 +0,0 @@
version: "2"
# https://docs.codeclimate.com/docs/default-analysis-configuration#sample-codeclimateyml
checks:
method-complexity:
enabled: true
config:
threshold: 15

View File

@@ -14,4 +14,3 @@ Dockerfile
*.orig *.orig
bin/wpscan-* bin/wpscan-*
.wpscan/ .wpscan/
.github/

View File

@@ -1,17 +0,0 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "bundler"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
# Check for updates to GitHub Actions every weekday
interval: "daily"

View File

@@ -1,42 +0,0 @@
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
ruby: [2.7, '3.0', 3.1, 3.2]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Ruby ${{ matrix.ruby }}
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- name: Install GEMs
run: |
gem install bundler
bundle config force_ruby_platform true
bundle config path vendor/bundle
bundle install --jobs 4 --retry 3
- name: rubocop
run: |
bundle exec rubocop
- name: rspec
run: |
bundle exec rspec
- name: Coveralls
uses: coverallsapp/github-action@master
continue-on-error: true
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,56 +0,0 @@
name: Build Docker Images
on:
push:
branches:
- master
release:
types: [published]
schedule:
- cron: "0 7 * * *"
jobs:
images:
runs-on: ubuntu-latest
steps:
- name: checkout sources
uses: actions/checkout@v3
- name: Set tag to latest
if: (github.event_name == 'push' && github.ref == 'refs/heads/master') || github.event_name == 'schedule'
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Set tag to release name
if: github.event_name == 'release' && startsWith(github.ref, 'refs/tags/')
run: |
echo "DOCKER_TAG=${{ github.event.release.tag_name }}" >> $GITHUB_ENV
- name: Check if DOCKER_TAG is set
if: env.DOCKER_TAG == ''
run: |
echo DOCKER_TAG is not set!
exit 1
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
id: buildx
with:
install: true
- name: Login to Docker Hub
uses: docker/login-action@v2.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm/v7,linux/arm64
push: true
tags: wpscanteam/wpscan:${{ env.DOCKER_TAG }}

View File

@@ -1,40 +0,0 @@
name: Ruby Gem
on:
release:
types: [published]
jobs:
build:
name: Build + Publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby 2.6
uses: ruby/setup-ruby@v1
with:
ruby-version: 2.6.x
#- name: Publish to GPR
# run: |
# mkdir -p $HOME/.gem
# touch $HOME/.gem/credentials
# chmod 0600 $HOME/.gem/credentials
# printf -- "---\n:github: Bearer ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
# gem build *.gemspec
# gem push --KEY github --host https://rubygems.pkg.github.com/${OWNER} *.gem
# env:
# GEM_HOST_API_KEY: ${{secrets.GITHUB_TOKEN}}
# OWNER: wpscanteam
- name: Publish to RubyGems
run: |
mkdir -p $HOME/.gem
touch $HOME/.gem/credentials
chmod 0600 $HOME/.gem/credentials
printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials
gem build *.gemspec
gem push *.gem
env:
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}

5
.rspec
View File

@@ -1,2 +1,3 @@
--require spec_helper --color
--color --fail-fast
--require spec_helper

View File

@@ -1,42 +1,27 @@
require: rubocop-performance require: rubocop-performance
AllCops: AllCops:
NewCops: enable TargetRubyVersion: 2.4
SuggestExtensions: false
TargetRubyVersion: 2.5
Exclude: Exclude:
- '*.gemspec' - '*.gemspec'
- 'vendor/**/*' - 'vendor/**/*'
Layout/LineLength: ClassVars:
Enabled: false
LineLength:
Max: 120 Max: 120
Lint/ConstantDefinitionInBlock: MethodLength:
Enabled: false Max: 20
Lint/MissingSuper:
Enabled: false
Lint/UriEscapeUnescape: Lint/UriEscapeUnescape:
Enabled: false Enabled: false
Metrics/AbcSize: Metrics/AbcSize:
Max: 27 Max: 25
Metrics/BlockLength: Metrics/BlockLength:
Exclude: Exclude:
- 'spec/**/*' - 'spec/**/*'
Metrics/ClassLength: Metrics/ClassLength:
Max: 150 Max: 150
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 10 Max: 8
Metrics/MethodLength:
Max: 20
Exclude:
- 'app/controllers/enumeration/cli_options.rb'
Metrics/PerceivedComplexity:
Max: 11
Style/ClassVars:
Enabled: false
Style/Documentation: Style/Documentation:
Enabled: false Enabled: false
Style/FormatStringToken: Style/FormatStringToken:
Enabled: false Enabled: false
Style/NumericPredicate:
Exclude:
- 'app/controllers/vuln_api.rb'

View File

@@ -1 +1 @@
3.0.2 2.6.2

View File

@@ -1,19 +1,4 @@
# frozen_string_literal: true
if ENV['GITHUB_ACTION']
require 'simplecov-lcov'
SimpleCov::Formatter::LcovFormatter.config do |c|
c.single_report_path = 'coverage/lcov.info'
c.report_with_single_file = true
end
SimpleCov.formatter = SimpleCov::Formatter::LcovFormatter
end
SimpleCov.start do SimpleCov.start do
enable_coverage :branch # Only supported for Ruby >= 2.5
add_filter '/spec/' add_filter '/spec/'
add_filter 'helper' add_filter 'helper'
end end

33
.travis.yml Normal file
View File

@@ -0,0 +1,33 @@
language: ruby
sudo: false
cache: bundler
rvm:
- 2.4.1
- 2.4.2
- 2.4.3
- 2.4.4
- 2.4.5
- 2.4.6
- 2.5.0
- 2.5.1
- 2.5.2
- 2.5.3
- 2.5.4
- 2.5.5
- 2.6.0
- 2.6.1
- 2.6.2
- 2.6.3
- ruby-head
before_install:
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
- gem update --system
matrix:
allow_failures:
- rvm: ruby-head
script:
- bundle exec rubocop
- bundle exec rspec
notifications:
email:
- team@wpscan.org

View File

@@ -1,16 +1,16 @@
FROM ruby:3.0.2-alpine AS builder FROM ruby:2.6.2-alpine3.9 AS builder
LABEL maintainer="WPScan Team <contact@wpscan.com>" LABEL maintainer="WPScan Team <team@wpscan.org>"
RUN echo "install: --no-document --no-post-install-message\nupdate: --no-document --no-post-install-message" > /etc/gemrc ARG BUNDLER_ARGS="--jobs=8 --without test development"
RUN echo "gem: --no-ri --no-rdoc" > /etc/gemrc
COPY . /wpscan COPY . /wpscan
RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \ RUN apk add --no-cache git libcurl ruby-dev libffi-dev make gcc musl-dev zlib-dev procps sqlite-dev && \
bundle config force_ruby_platform true && \ bundle install --system --clean --no-cache --gemfile=/wpscan/Gemfile $BUNDLER_ARGS && \
bundle config disable_version_check 'true' && \ # temp fix for https://github.com/bundler/bundler/issues/6680
bundle config without "test development" && \ rm -rf /usr/local/bundle/cache
bundle config path.system 'true' && \
bundle install --gemfile=/wpscan/Gemfile --jobs=8
WORKDIR /wpscan WORKDIR /wpscan
RUN rake install --trace RUN rake install --trace
@@ -19,9 +19,8 @@ RUN rake install --trace
RUN chmod -R a+r /usr/local/bundle RUN chmod -R a+r /usr/local/bundle
FROM ruby:3.0.2-alpine FROM ruby:2.6.2-alpine3.9
LABEL maintainer="WPScan Team <contact@wpscan.com>" LABEL maintainer="WPScan Team <team@wpscan.org>"
LABEL org.opencontainers.image.source https://github.com/wpscanteam/wpscan
RUN adduser -h /wpscan -g WPScan -D wpscan RUN adduser -h /wpscan -g WPScan -D wpscan
@@ -39,3 +38,4 @@ USER wpscan
RUN /usr/local/bundle/bin/wpscan --update --verbose RUN /usr/local/bundle/bin/wpscan --update --verbose
ENTRYPOINT ["/usr/local/bundle/bin/wpscan"] ENTRYPOINT ["/usr/local/bundle/bin/wpscan"]
CMD ["--help"]

View File

@@ -27,7 +27,9 @@ Example cases which do not require a commercial license, and thus fall under the
- Using WPScan to test your own systems. - Using WPScan to test your own systems.
- Any non-commercial use of WPScan. - Any non-commercial use of WPScan.
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - contact@wpscan.com. 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; Free-use Terms and Conditions;

101
README.md
View File

@@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<a href="https://wpscan.com/"> <a href="https://wpscan.org/">
<img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo"> <img src="https://raw.githubusercontent.com/wpscanteam/wpscan/gh-pages/images/wpscan_logo.png" alt="WPScan logo">
</a> </a>
</p> </p>
@@ -7,17 +7,17 @@
<h3 align="center">WPScan</h3> <h3 align="center">WPScan</h3>
<p align="center"> <p align="center">
WordPress Security Scanner WordPress Vulnerability Scanner
<br> <br>
<br> <br>
<a href="https://wpscan.com/" title="homepage" target="_blank">WPScan WordPress Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress security plugin" target="_blank">WordPress Security Plugin</a> <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>
<p align="center"> <p align="center">
<a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a> <a href="https://badge.fury.io/rb/wpscan" target="_blank"><img src="https://badge.fury.io/rb/wpscan.svg"></a>
<a href="https://hub.docker.com/r/wpscanteam/wpscan/" target="_blank"><img src="https://img.shields.io/docker/pulls/wpscanteam/wpscan.svg"></a> <a href="https://travis-ci.org/wpscanteam/wpscan" target="_blank"><img src="https://travis-ci.org/wpscanteam/wpscan.svg?branch=master"></a>
<a href="https://github.com/wpscanteam/wpscan/actions?query=workflow%3ABuild" target="_blank"><img src="https://github.com/wpscanteam/wpscan/workflows/Build/badge.svg"></a>
<a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a> <a href="https://codeclimate.com/github/wpscanteam/wpscan" target="_blank"><img src="https://codeclimate.com/github/wpscanteam/wpscan/badges/gpa.svg"></a>
<a href="https://www.patreon.com/wpscan" target="_blank"><img src="https://img.shields.io/badge/patreon-donate-green.svg"></a>
</p> </p>
# INSTALL # INSTALL
@@ -25,23 +25,13 @@
## Prerequisites ## Prerequisites
- (Optional but highly recommended: [RVM](https://rvm.io/rvm/install)) - (Optional but highly recommended: [RVM](https://rvm.io/rvm/install))
- Ruby >= 2.5 - Recommended: latest - Ruby >= 2.3 - Recommended: latest
- Ruby 2.5.0 to 2.5.3 can cause an 'undefined symbol: rmpd_util_str_to_d' error in some systems, see [#1283](https://github.com/wpscanteam/wpscan/issues/1283) - Ruby 2.5.0 to 2.5.3 can cause an 'undefined symbol: rmpd_util_str_to_d' error in some systems, see [#1283](https://github.com/wpscanteam/wpscan/issues/1283)
- Curl >= 7.72 - Recommended: latest - Curl >= 7.21 - Recommended: latest
- The 7.29 has a segfault - The 7.29 has a segfault
- The < 7.72 could result in `Stream error in the HTTP/2 framing layer` in some cases
- RubyGems - Recommended: latest - 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
### In a Pentesting distribution ### From RubyGems (Recommended)
When using a pentesting distubution (such as Kali Linux), it is recommended to install/update wpscan via the package manager if available.
### In macOSX via Homebrew
`brew install wpscanteam/tap/wpscan`
### From RubyGems
```shell ```shell
gem install wpscan gem install wpscan
@@ -49,11 +39,23 @@ gem install wpscan
On MacOSX, if a ```Gem::FilePermissionError``` is raised due to the Apple's System Integrity Protection (SIP), either install RVM and install wpscan again, or run ```sudo gem install -n /usr/local/bin wpscan``` (see [#1286](https://github.com/wpscanteam/wpscan/issues/1286)) On MacOSX, if a ```Gem::FilePermissionError``` is raised due to the Apple's System Integrity Protection (SIP), either install RVM and install wpscan again, or run ```sudo gem install -n /usr/local/bin wpscan``` (see [#1286](https://github.com/wpscanteam/wpscan/issues/1286))
### From sources (NOT Recommended)
Prerequisites: Git
```shell
git clone https://github.com/wpscanteam/wpscan
cd wpscan/
bundle install && rake install
```
# Updating # Updating
You can update the local database by using ```wpscan --update``` You can update the local database by using ```wpscan --update```
Updating WPScan itself is either done via ```gem update wpscan``` or the packages manager (this is quite important for distributions such as in Kali Linux: ```apt-get update && apt-get upgrade```) depending on how WPScan was (pre)installed Updating WPScan itself is either done via ```gem update wpscan``` or the packages manager (this is quite important for distributions such as in Kali Linux: ```apt-get update && apt-get upgrade```) depending how WPScan was (pre)installed
# Docker # Docker
@@ -75,74 +77,41 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
# Usage # Usage
Full user documentation can be found here; https://github.com/wpscanteam/wpscan/wiki/WPScan-User-Documentation ```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings. If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings.
If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'. As a result, when using the ```--enumerate``` option, don't forget to set the ```--plugins-detection``` accordingly, as its default is 'passive'.
For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo) For more options, open a terminal and type ```wpscan --help``` (if you built wpscan from the source, you should type the command outside of the git repo)
The DB is located at ~/.wpscan/db The DB is located at ~/.wpscan/db
## Optional: WordPress Vulnerability Database API
The WPScan CLI tool uses the [WordPress Vulnerability Database API](https://wpscan.com/api) to retrieve WordPress vulnerability data in real time. For WPScan to retrieve the vulnerability data an API token must be supplied via the `--api-token` option, or via a configuration file, as discussed below. An API token can be obtained by registering an account on [WPScan.com](https://wpscan.com/register).
Up to **75** API requests per day are given free of charge, that should be suitable to scan most WordPress websites at least once per day. When the daily 75 API requests are exhausted, WPScan will continue to work as normal but without any vulnerability data.
### How many API requests do you need?
- Our WordPress scanner makes one API request for the WordPress version, one request per installed plugin and one request per installed theme.
- On average, a WordPress website has 22 installed plugins.
## Load CLI options from file/s
WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last): WPScan can load all options (including the --url) from configuration files, the following locations are checked (order: first to last):
- ~/.wpscan/scan.json - ~/.wpscan/cli_options.json
- ~/.wpscan/scan.yml - ~/.wpscan/cli_options.yml
- pwd/.wpscan/scan.json - pwd/.wpscan/cli_options.json
- pwd/.wpscan/scan.yml - pwd/.wpscan/cli_options.yml
If those files exist, options from the `cli_options` key will be loaded and overridden if found twice. If those files exist, options from them will be loaded and overridden if found twice.
e.g: e.g:
~/.wpscan/scan.yml: ~/.wpscan/cli_options.yml:
```yml ```yml
cli_options: proxy: 'http://127.0.0.1:8080'
proxy: 'http://127.0.0.1:8080' verbose: true
verbose: true
``` ```
pwd/.wpscan/scan.yml: pwd/.wpscan/cli_options.yml:
```yml ```yml
cli_options: proxy: 'socks5://127.0.0.1:9090'
proxy: 'socks5://127.0.0.1:9090' url: 'http://target.tld'
url: 'http://target.tld'
``` ```
Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld``` Running ```wpscan``` in the current directory (pwd), is the same as ```wpscan -v --proxy socks5://127.0.0.1:9090 --url http://target.tld```
## Save API Token in a file Enumerating usernames
The feature mentioned above is useful to keep the API Token in a config file and not have to supply it via the CLI each time. To do so, create the ~/.wpscan/scan.yml file containing the below:
```yml
cli_options:
api_token: 'YOUR_API_TOKEN'
```
## Load API Token From ENV (since v3.7.10)
The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
## Enumerating usernames
```shell ```shell
wpscan --url https://target.tld/ --enumerate u wpscan --url https://target.tld/ --enumerate u
@@ -189,7 +158,7 @@ Example cases which do not require a commercial license, and thus fall under the
- Using WPScan to test your own systems. - Using WPScan to test your own systems.
- Any non-commercial use of WPScan. - Any non-commercial use of WPScan.
If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - contact@wpscan.com. If you need to purchase a commercial license or are unsure whether you need to purchase a commercial license contact us - team@wpscan.org.
Free-use Terms and Conditions; Free-use Terms and Conditions;

View File

@@ -6,18 +6,14 @@ exec = []
begin begin
require 'rubocop/rake_task' require 'rubocop/rake_task'
RuboCop::RakeTask.new RuboCop::RakeTask.new
exec << :rubocop exec << :rubocop
rescue LoadError rescue LoadError
end end
begin begin
require 'rspec/core/rake_task' require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec)
RSpec::Core::RakeTask.new(:spec) { |t| t.rspec_opts = %w{--tag ~slow} }
exec << :spec exec << :spec
rescue LoadError rescue LoadError
end end

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'controllers/core' require_relative 'controllers/core'
require_relative 'controllers/vuln_api'
require_relative 'controllers/custom_directories' require_relative 'controllers/custom_directories'
require_relative 'controllers/wp_version' require_relative 'controllers/wp_version'
require_relative 'controllers/main_theme' require_relative 'controllers/main_theme'

View File

@@ -8,13 +8,13 @@ module WPScan
def cli_options def cli_options
[OptURL.new(['--url URL', 'The URL of the blog to scan'], [OptURL.new(['--url URL', 'The URL of the blog to scan'],
required_unless: %i[update help hh version], default_protocol: 'http')] + required_unless: %i[update help hh version], default_protocol: 'http')] +
super.drop(2) + # delete the --url and --force from CMSScanner super.drop(1) + # delete the --url from CMSScanner
[ [
OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'], OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'],
choices: %w[apache iis nginx], choices: %w[apache iis nginx],
normalize: %i[downcase to_sym], normalize: %i[downcase to_sym],
advanced: true), advanced: true),
OptBoolean.new(['--force', 'Do not check if the target is running WordPress or returns a 403']), OptBoolean.new(['--force', 'Do not check if the target is running WordPress']),
OptBoolean.new(['--[no-]update', 'Whether or not to update the Database']) OptBoolean.new(['--[no-]update', 'Whether or not to update the Database'])
] ]
end end
@@ -39,7 +39,7 @@ module WPScan
output('@notice', msg: 'It seems like you have not updated the database for some time.') 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]' print '[?] Do you want to update now? [Y]es [N]o, default: [N]'
/^y/i.match?(Readline.readline) /^y/i.match?(Readline.readline) ? true : false
end end
def update_db def update_db

View File

@@ -18,7 +18,9 @@ module WPScan
target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir target.content_dir = ParsedCli.wp_content_dir if ParsedCli.wp_content_dir
target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir target.plugins_dir = ParsedCli.wp_plugins_dir if ParsedCli.wp_plugins_dir
raise Error::WpContentDirNotDetected unless target.content_dir return if target.content_dir(ParsedCli.detection_mode)
raise Error::WpContentDirNotDetected
end end
end end
end end

View File

@@ -7,6 +7,15 @@ module WPScan
module Controller module Controller
# Enumeration Controller # Enumeration Controller
class Enumeration < CMSScanner::Controller::Base class Enumeration < CMSScanner::Controller::Base
def before_scan
DB::DynamicFinders::Plugin.create_versions_finders
DB::DynamicFinders::Theme.create_versions_finders
# Force the Garbage Collector to run due to the above method being
# quite heavy in objects allocation
GC.start
end
def run def run
enum = ParsedCli.enumerate || {} enum = ParsedCli.enumerate || {}

View File

@@ -11,6 +11,7 @@ module WPScan
end end
# @return [ Array<OptParseValidator::OptBase> ] # @return [ Array<OptParseValidator::OptBase> ]
# rubocop:disable Metrics/MethodLength
def cli_enum_choices def cli_enum_choices
[ [
OptMultiChoices.new( OptMultiChoices.new(
@@ -18,10 +19,10 @@ module WPScan
choices: { choices: {
vp: OptBoolean.new(['--vulnerable-plugins']), vp: OptBoolean.new(['--vulnerable-plugins']),
ap: OptBoolean.new(['--all-plugins']), ap: OptBoolean.new(['--all-plugins']),
p: OptBoolean.new(['--popular-plugins']), p: OptBoolean.new(['--plugins']),
vt: OptBoolean.new(['--vulnerable-themes']), vt: OptBoolean.new(['--vulnerable-themes']),
at: OptBoolean.new(['--all-themes']), at: OptBoolean.new(['--all-themes']),
t: OptBoolean.new(['--popular-themes']), t: OptBoolean.new(['--themes']),
tt: OptBoolean.new(['--timthumbs']), tt: OptBoolean.new(['--timthumbs']),
cb: OptBoolean.new(['--config-backups']), cb: OptBoolean.new(['--config-backups']),
dbe: OptBoolean.new(['--db-exports']), dbe: OptBoolean.new(['--db-exports']),
@@ -44,6 +45,7 @@ module WPScan
) )
] ]
end end
# rubocop:enable Metrics/MethodLength
# @return [ Array<OptParseValidator::OptBase> ] # @return [ Array<OptParseValidator::OptBase> ]
def cli_plugins_opts def cli_plugins_opts
@@ -51,7 +53,7 @@ module WPScan
OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true), OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
OptChoice.new( OptChoice.new(
['--plugins-detection MODE', ['--plugins-detection MODE',
'Use the supplied mode to enumerate Plugins.'], 'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
), ),
OptBoolean.new( OptBoolean.new(
@@ -62,13 +64,9 @@ module WPScan
), ),
OptChoice.new( OptChoice.new(
['--plugins-version-detection MODE', ['--plugins-version-detection MODE',
'Use the supplied mode to check plugins\' versions.'], '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 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, advanced: true
) )
] ]
end end
@@ -93,11 +91,6 @@ module WPScan
'Use the supplied mode to check themes versions instead of the --detection-mode ' \ 'Use the supplied mode to check themes versions instead of the --detection-mode ' \
'or --themes-detection modes.'], 'or --themes-detection modes.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true 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, advanced: true
) )
] ]
end end
@@ -170,12 +163,6 @@ module WPScan
['--users-detection MODE', ['--users-detection MODE',
'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'], 'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'],
choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
),
OptRegexp.new(
[
'--exclude-usernames REGEXP_OR_STRING',
'Exclude usernames matching the Regexp/string (case insensitive). Regexp delimiters are not required.'
], options: Regexp::IGNORECASE
) )
] ]
end end

View File

@@ -56,13 +56,12 @@ module WPScan
# #
# @return [ Boolean ] Wether or not to enumerate the plugins # @return [ Boolean ] Wether or not to enumerate the plugins
def enum_plugins?(opts) def enum_plugins?(opts)
opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins] opts[:plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
end end
def enum_plugins def enum_plugins
opts = default_opts('plugins').merge( opts = default_opts('plugins').merge(
list: plugins_list_from_opts(ParsedCli.options), list: plugins_list_from_opts(ParsedCli.options),
threshold: ParsedCli.plugins_threshold,
sort: true sort: true
) )
@@ -92,7 +91,7 @@ module WPScan
if opts[:enumerate][:all_plugins] if opts[:enumerate][:all_plugins]
DB::Plugins.all_slugs DB::Plugins.all_slugs
elsif opts[:enumerate][:popular_plugins] elsif opts[:enumerate][:plugins]
DB::Plugins.popular_slugs DB::Plugins.popular_slugs
else else
DB::Plugins.vulnerable_slugs DB::Plugins.vulnerable_slugs
@@ -103,13 +102,12 @@ module WPScan
# #
# @return [ Boolean ] Wether or not to enumerate the themes # @return [ Boolean ] Wether or not to enumerate the themes
def enum_themes?(opts) def enum_themes?(opts)
opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes] opts[:themes] || opts[:all_themes] || opts[:vulnerable_themes]
end end
def enum_themes def enum_themes
opts = default_opts('themes').merge( opts = default_opts('themes').merge(
list: themes_list_from_opts(ParsedCli.options), list: themes_list_from_opts(ParsedCli.options),
threshold: ParsedCli.themes_threshold,
sort: true sort: true
) )
@@ -139,7 +137,7 @@ module WPScan
if opts[:enumerate][:all_themes] if opts[:enumerate][:all_themes]
DB::Themes.all_slugs DB::Themes.all_slugs
elsif opts[:enumerate][:popular_themes] elsif opts[:enumerate][:themes]
DB::Themes.popular_slugs DB::Themes.popular_slugs
else else
DB::Themes.vulnerable_slugs DB::Themes.vulnerable_slugs

View File

@@ -17,40 +17,33 @@ module WPScan
'Maximum number of passwords to send by request with XMLRPC multicall'], 'Maximum number of passwords to send by request with XMLRPC multicall'],
default: 500), default: 500),
OptChoice.new(['--password-attack ATTACK', OptChoice.new(['--password-attack ATTACK',
'Force the supplied attack to be used rather than automatically determining one.', 'Force the supplied attack to be used rather than automatically determining one.'],
'Multicall will only work against WP < 4.4'],
choices: %w[wp-login xmlrpc xmlrpc-multicall], choices: %w[wp-login xmlrpc xmlrpc-multicall],
normalize: %i[downcase underscore to_sym]), normalize: %i[downcase underscore to_sym])
OptString.new(['--login-uri URI', 'The URI of the login page if different from /wp-login.php'])
] ]
end end
def attack_opts
@attack_opts ||= {
show_progression: user_interaction?,
multicall_max_passwords: ParsedCli.multicall_max_passwords
}
end
def run def run
return unless ParsedCli.passwords return unless ParsedCli.passwords
if user_interaction?
output('@info',
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
end
attack_opts = {
show_progression: user_interaction?,
multicall_max_passwords: ParsedCli.multicall_max_passwords
}
begin begin
found = [] found = []
if user_interaction? attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user|
output('@info',
msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
end
attacker.attack(users, ParsedCli.passwords, attack_opts) do |user|
found << user found << user
attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}") attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
end end
rescue Error::NoLoginInterfaceDetected => e
# TODO: Maybe output that in JSON as well.
output('@notice', msg: e.to_s) if user_interaction?
ensure ensure
output('users', users: found) output('users', users: found)
end end
@@ -72,47 +65,30 @@ module WPScan
case ParsedCli.password_attack case ParsedCli.password_attack
when :wp_login when :wp_login
raise Error::NoLoginInterfaceDetected unless target.login_url WPScan::Finders::Passwords::WpLogin.new(target)
Finders::Passwords::WpLogin.new(target)
when :xmlrpc when :xmlrpc
raise Error::XMLRPCNotDetected unless xmlrpc raise Error::XMLRPCNotDetected unless xmlrpc
Finders::Passwords::XMLRPC.new(xmlrpc) WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
when :xmlrpc_multicall when :xmlrpc_multicall
raise Error::XMLRPCNotDetected unless xmlrpc raise Error::XMLRPCNotDetected unless xmlrpc
Finders::Passwords::XMLRPCMulticall.new(xmlrpc) WPScan::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.match?(/>\s*405\s*</)
true
else
false
end end
end end
# @return [ CMSScanner::Finders::Finder ] # @return [ CMSScanner::Finders::Finder ]
def attacker_from_automatic_detection def attacker_from_automatic_detection
if xmlrpc_get_users_blogs_enabled? if xmlrpc&.enabled? && xmlrpc.available_methods.include?('wp.getUsersBlogs')
wp_version = target.wp_version wp_version = target.wp_version
if wp_version && wp_version < '4.4' if wp_version && wp_version < '4.4'
Finders::Passwords::XMLRPCMulticall.new(xmlrpc) WPScan::Finders::Passwords::XMLRPCMulticall.new(xmlrpc)
else else
Finders::Passwords::XMLRPC.new(xmlrpc) WPScan::Finders::Passwords::XMLRPC.new(xmlrpc)
end end
elsif target.login_url
Finders::Passwords::WpLogin.new(target)
else else
raise Error::NoLoginInterfaceDetected WPScan::Finders::Passwords::WpLogin.new(target)
end end
end end
@@ -124,6 +100,15 @@ module WPScan
acc << Model::User.new(elem.chomp) acc << Model::User.new(elem.chomp)
end end
end end
# @param [ String ] wordlist_path
#
# @return [ Array<String> ]
def passwords(wordlist_path)
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
acc << elem.chomp
end
end
end end
end end
end end

View File

@@ -1,35 +0,0 @@
# frozen_string_literal: true
module WPScan
module Controller
# Controller to handle the API token
class VulnApi < CMSScanner::Controller::Base
ENV_KEY = 'WPSCAN_API_TOKEN'
def cli_options
[
OptString.new(
['--api-token TOKEN',
'The WPScan API Token to display vulnerability data, available at https://wpscan.com/profile']
)
]
end
def before_scan
return unless ParsedCli.api_token || ENV.key?(ENV_KEY)
DB::VulnApi.token = ParsedCli.api_token || ENV[ENV_KEY]
api_status = DB::VulnApi.status
raise Error::InvalidApiToken if api_status['status'] == 'forbidden'
raise Error::ApiLimitReached if api_status['requests_remaining'] == 0
raise api_status['http_error'] if api_status['http_error']
end
def after_scan
output('status', status: DB::VulnApi.status, api_requests: WPScan.api_requests)
end
end
end
end

View File

@@ -17,7 +17,7 @@ module WPScan
end end
def before_scan def before_scan
DB::DynamicFinders::Wordpress.create_versions_finders WPScan::DB::DynamicFinders::Wordpress.create_versions_finders
end end
def run def run

View File

@@ -4,10 +4,11 @@ module WPScan
module Finders module Finders
module DbExports module DbExports
# DB Exports finder # DB Exports finder
# See https://github.com/wpscanteam/wpscan-v3/issues/62
class KnownLocations < CMSScanner::Finders::Finder class KnownLocations < CMSScanner::Finders::Finder
include CMSScanner::Finders::Finder::Enumerator include CMSScanner::Finders::Finder::Enumerator
SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE|ALTER) (?:TABLE|DATABASE)|INSERT INTO/.freeze SQL_PATTERN = /(?:DROP|(?:UN)?LOCK|CREATE) TABLE|INSERT INTO/.freeze
# @param [ Hash ] opts # @param [ Hash ] opts
# @option opts [ String ] :list # @option opts [ String ] :list
@@ -19,9 +20,9 @@ module WPScan
enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res| enumerate(potential_urls(opts), opts.merge(check_full_response: 200)) do |res|
if res.effective_url.end_with?('.zip') if res.effective_url.end_with?('.zip')
next unless %r{\Aapplication/zip}i.match?(res.headers['Content-Type']) next unless res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
else else
next unless SQL_PATTERN.match?(res.body) next unless res.body =~ SQL_PATTERN
end end
found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100) found << Model::DbExport.new(res.request.url, found_by: DIRECT_ACCESS, confidence: 100)
@@ -39,57 +40,18 @@ module WPScan
# #
# @return [ Hash ] # @return [ Hash ]
def potential_urls(opts = {}) def potential_urls(opts = {})
urls = {} urls = {}
index = 0 domain_name = target.uri.host[/(^[\w|-]+)/, 1]
File.open(opts[:list]).each do |path| File.open(opts[:list]).each_with_index do |path, index|
path.chomp! path.gsub!('{domain_name}', domain_name)
if path.include?('{domain_name}') urls[target.url(path.chomp)] = index
urls[target.url(path.gsub('{domain_name}', domain_name))] = index
if domain_name != domain_name_with_sub
urls[target.url(path.gsub('{domain_name}', domain_name_with_sub))] = index + 1
index += 1
end
else
urls[target.url(path)] = index
end
index += 1
end end
urls urls
end end
def domain_name
@domain_name ||= if Resolv::AddressRegex.match?(target.uri.host)
target.uri.host
else
(PublicSuffix.domain(target.uri.host) || target.uri.host)[/(^[\w|-]+)/, 1]
end
end
def domain_name_with_sub
@domain_name_with_sub ||=
if Resolv::AddressRegex.match?(target.uri.host)
target.uri.host
else
parsed = PublicSuffix.parse(target.uri.host)
if parsed.subdomain
parsed.subdomain.gsub(".#{parsed.tld}", '')
elsif parsed.domain
parsed.domain.gsub(".#{parsed.tld}", '')
else
target.uri.host
end
end
rescue PublicSuffix::DomainNotAllowed
@domain_name_with_sub = target.uri.host
end
def create_progress_bar(opts = {}) def create_progress_bar(opts = {})
super(opts.merge(title: ' Checking DB Exports -')) super(opts.merge(title: ' Checking DB Exports -'))
end end

View File

@@ -4,9 +4,8 @@ require_relative 'interesting_findings/readme'
require_relative 'interesting_findings/wp_cron' require_relative 'interesting_findings/wp_cron'
require_relative 'interesting_findings/multisite' require_relative 'interesting_findings/multisite'
require_relative 'interesting_findings/debug_log' require_relative 'interesting_findings/debug_log'
require_relative 'interesting_findings/backup_db' require_relative 'interesting_findings/plugin_backup_folders'
require_relative 'interesting_findings/mu_plugins' require_relative 'interesting_findings/mu_plugins'
require_relative 'interesting_findings/php_disabled'
require_relative 'interesting_findings/registration' require_relative 'interesting_findings/registration'
require_relative 'interesting_findings/tmm_db_migrate' require_relative 'interesting_findings/tmm_db_migrate'
require_relative 'interesting_findings/upload_sql_dump' require_relative 'interesting_findings/upload_sql_dump'
@@ -25,9 +24,9 @@ module WPScan
super(target) super(target)
%w[ %w[
Readme DebugLog FullPathDisclosure BackupDB DuplicatorInstallerLog Readme DebugLog FullPathDisclosure PluginBackupFolders DuplicatorInstallerLog
Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate Multisite MuPlugins Registration UploadDirectoryListing TmmDbMigrate
UploadSQLDump EmergencyPwdResetScript WPCron PHPDisabled UploadSQLDump EmergencyPwdResetScript WPCron
].each do |f| ].each do |f|
finders << InterestingFindings.const_get(f).new(target) finders << InterestingFindings.const_get(f).new(target)
end end

View File

@@ -1,25 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# BackupDB finder
class BackupDB < CMSScanner::Finders::Finder
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'wp-content/backup-db/'
res = target.head_and_get(path, [200, 403])
return unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
Model::BackupDB.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path)
)
end
end
end
end
end

View File

@@ -11,7 +11,11 @@ module WPScan
return unless target.debug_log?(path) return unless target.debug_log?(path)
Model::DebugLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS) Model::DebugLog.new(
target.url(path),
confidence: 100, found_by: DIRECT_ACCESS,
references: { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
)
end end
end end
end end

View File

@@ -9,9 +9,14 @@ module WPScan
def aggressive(_opts = {}) def aggressive(_opts = {})
path = 'installer-log.txt' path = 'installer-log.txt'
return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.match?(target.head_and_get(path).body) return unless target.head_and_get(path).body =~ /DUPLICATOR INSTALL-LOG/
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS) Model::DuplicatorInstallerLog.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS,
references: { url: 'https://www.exploit-db.com/ghdb/3981/' }
)
end end
end end
end end

View File

@@ -15,7 +15,10 @@ module WPScan
Model::EmergencyPwdResetScript.new( Model::EmergencyPwdResetScript.new(
target.url(path), target.url(path),
confidence: /password/i.match?(res.body) ? 100 : 40, confidence: /password/i.match?(res.body) ? 100 : 40,
found_by: DIRECT_ACCESS found_by: DIRECT_ACCESS,
references: {
url: 'https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script'
}
) )
end end
end end

View File

@@ -16,7 +16,8 @@ module WPScan
target.url(path), target.url(path),
confidence: 100, confidence: 100,
found_by: DIRECT_ACCESS, found_by: DIRECT_ACCESS,
interesting_entries: fpd_entries interesting_entries: fpd_entries,
references: { url: 'https://www.owasp.org/index.php/Full_Path_Disclosure' }
) )
end end
end end

View File

@@ -7,16 +7,22 @@ module WPScan
class MuPlugins < CMSScanner::Finders::Finder class MuPlugins < CMSScanner::Finders::Finder
# @return [ InterestingFinding ] # @return [ InterestingFinding ]
def passive(_opts = {}) def passive(_opts = {})
pattern = %r{#{target.content_dir}/mu-plugins/}i pattern = %r{#{target.content_dir}/mu\-plugins/}i
target.in_scope_uris(target.homepage_res, '(//@href|//@src)[contains(., "mu-plugins")]') do |uri| target.in_scope_uris(target.homepage_res) do |uri|
next unless uri.path&.match?(pattern) next unless uri.path =~ pattern
url = target.url('wp-content/mu-plugins/') url = target.url('wp-content/mu-plugins/')
target.mu_plugins = true target.mu_plugins = true
return Model::MuPlugins.new(url, confidence: 70, found_by: 'URLs In Homepage (Passive Detection)') return Model::MuPlugins.new(
url,
confidence: 70,
found_by: 'URLs In Homepage (Passive Detection)',
to_s: "This site has 'Must Use Plugins': #{url}",
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
)
end end
nil nil
end end
@@ -31,7 +37,13 @@ module WPScan
target.mu_plugins = true target.mu_plugins = true
Model::MuPlugins.new(url, confidence: 80, found_by: DIRECT_ACCESS) Model::MuPlugins.new(
url,
confidence: 80,
found_by: DIRECT_ACCESS,
to_s: "This site has 'Must Use Plugins': #{url}",
references: { url: 'http://codex.wordpress.org/Must_Use_Plugins' }
)
end end
end end
end end

View File

@@ -12,12 +12,18 @@ module WPScan
location = res.headers_hash['location'] location = res.headers_hash['location']
return unless [200, 302].include?(res.code) return unless [200, 302].include?(res.code)
return if res.code == 302 && location&.include?('wp-login.php?action=register') return if res.code == 302 && location =~ /wp-login\.php\?action=register/
return unless res.code == 200 || (res.code == 302 && location&.include?('wp-signup.php')) return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
target.multisite = true target.multisite = true
Model::Multisite.new(url, confidence: 100, found_by: DIRECT_ACCESS) Model::Multisite.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: 'This site seems to be a multisite',
references: { url: 'http://codex.wordpress.org/Glossary#Multisite' }
)
end end
end end
end end

View File

@@ -1,21 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# See https://github.com/wpscanteam/wpscan/issues/1593
class PHPDisabled < CMSScanner::Finders::Finder
PATTERN = /\$wp_version =/.freeze
# @return [ InterestingFinding ]
def aggressive(_opts = {})
path = 'wp-includes/version.php'
return unless PATTERN.match?(target.head_and_get(path).body)
Model::PHPDisabled.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
end
end
end
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
module WPScan
module Finders
module InterestingFindings
# Known Backup Folders from Plugin finder
class PluginBackupFolders < CMSScanner::Finders::Finder
PATHS = %w[wp-content/backup-db/ wp-content/backups-dup-pro/ wp-content/updraft/].freeze
# @return [ InterestingFinding ]
def aggressive(_opts = {})
found = []
PATHS.each do |path|
res = target.head_and_get(path, [200, 403])
next unless [200, 403].include?(res.code) && !target.homepage_or_404?(res)
found << Model::PluginBackupFolder.new(
target.url(path),
confidence: 70,
found_by: DIRECT_ACCESS,
interesting_entries: target.directory_listing_entries(path),
references: { url: ['https://github.com/wpscanteam/wpscan/issues/422',
'https://github.com/wpscanteam/wpscan/issues/1342'] }
)
end
found
end
end
end
end
end

View File

@@ -20,7 +20,12 @@ module WPScan
target.registration_enabled = true target.registration_enabled = true
Model::Registration.new(res.effective_url, confidence: 100, found_by: DIRECT_ACCESS) Model::Registration.new(
res.effective_url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: "Registration is enabled: #{res.effective_url}"
)
end end
end end
end end

View File

@@ -13,7 +13,12 @@ module WPScan
return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i return unless res.code == 200 && res.headers['Content-Type'] =~ %r{\Aapplication/zip}i
Model::TmmDbMigrate.new(url, confidence: 100, found_by: DIRECT_ACCESS) Model::TmmDbMigrate.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
references: { packetstorm: 131_957 }
)
end end
end end
end end

View File

@@ -13,7 +13,12 @@ module WPScan
url = target.url(path) url = target.url(path)
Model::UploadDirectoryListing.new(url, confidence: 100, found_by: DIRECT_ACCESS) Model::UploadDirectoryListing.new(
url,
confidence: 100,
found_by: DIRECT_ACCESS,
to_s: "Upload directory has listing enabled: #{url}"
)
end end
end end
end end

View File

@@ -12,9 +12,13 @@ module WPScan
path = 'wp-content/uploads/dump.sql' path = 'wp-content/uploads/dump.sql'
res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } }) res = target.head_and_get(path, [200], get: { headers: { 'Range' => 'bytes=0-3000' } })
return unless SQL_PATTERN.match?(res.body) return unless res.body =~ SQL_PATTERN
Model::UploadSQLDump.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS) Model::UploadSQLDump.new(
target.url(path),
confidence: 100,
found_by: DIRECT_ACCESS
)
end end
end end
end end

View File

@@ -11,7 +11,17 @@ module WPScan
return unless res.code == 200 return unless res.code == 200
Model::WPCron.new(wp_cron_url, confidence: 60, found_by: DIRECT_ACCESS) 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 end
def wp_cron_url def wp_cron_url

View File

@@ -1,10 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'main_theme/css_style_in_homepage' require_relative 'main_theme/css_style'
require_relative 'main_theme/css_style_in_404_page'
require_relative 'main_theme/woo_framework_meta_generator' require_relative 'main_theme/woo_framework_meta_generator'
require_relative 'main_theme/urls_in_homepage' require_relative 'main_theme/urls_in_homepage'
require_relative 'main_theme/urls_in_404_page'
module WPScan module WPScan
module Finders module Finders
@@ -16,11 +14,9 @@ module WPScan
# @param [ WPScan::Target ] target # @param [ WPScan::Target ] target
def initialize(target) def initialize(target)
finders << finders <<
MainTheme::CssStyleInHomepage.new(target) << MainTheme::CssStyle.new(target) <<
MainTheme::CssStyleIn404Page.new(target) <<
MainTheme::WooFrameworkMetaGenerator.new(target) << MainTheme::WooFrameworkMetaGenerator.new(target) <<
MainTheme::UrlsInHomepage.new(target) << MainTheme::UrlsInHomepage.new(target)
MainTheme::UrlsIn404Page.new(target)
end end
end end
end end

View File

@@ -3,9 +3,9 @@
module WPScan module WPScan
module Finders module Finders
module MainTheme module MainTheme
# From the CSS style in the homepage # From the css style
class CssStyleInHomepage < CMSScanner::Finders::Finder class CssStyle < CMSScanner::Finders::Finder
include Finders::WpItems::UrlsInPage # To have the item_code_pattern method available here include Finders::WpItems::URLsInHomepage
def create_theme(slug, style_url, opts) def create_theme(slug, style_url, opts)
Model::Theme.new( Model::Theme.new(
@@ -20,8 +20,8 @@ module WPScan
end end
def passive_from_css_href(res, opts) def passive_from_css_href(res, opts)
target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri| target.in_scope_uris(res, '//style/@src|//link/@href') do |uri|
next unless uri.path =~ %r{/themes/([^/]+)/style.css\z}i next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
return create_theme(Regexp.last_match[1], uri.to_s, opts) return create_theme(Regexp.last_match[1], uri.to_s, opts)
end end
@@ -33,7 +33,7 @@ module WPScan
code = tag.text.to_s code = tag.text.to_s
next if code.empty? next if code.empty?
next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'( ]*}i next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts) return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
end end

View File

@@ -1,14 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
# From the CSS style in the 404 page
class CssStyleIn404Page < CssStyleInHomepage
def passive(opts = {})
passive_from_css_href(target.error_404_res, opts) || passive_from_style_code(target.error_404_res, opts)
end
end
end
end
end

View File

@@ -1,15 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module MainTheme
# URLs In 404 Page Finder
class UrlsIn404Page < UrlsInHomepage
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.error_404_res
end
end
end
end
end

View File

@@ -5,7 +5,7 @@ module WPScan
module MainTheme module MainTheme
# URLs In Homepage Finder # URLs In Homepage Finder
class UrlsInHomepage < CMSScanner::Finders::Finder class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::UrlsInPage include WpItems::URLsInHomepage
# @param [ Hash ] opts # @param [ Hash ] opts
# #
@@ -13,7 +13,7 @@ module WPScan
def passive(opts = {}) def passive(opts = {})
found = [] found = []
slugs = items_from_links('themes', uniq: false) + items_from_codes('themes', uniq: false) 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| slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences)) found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
@@ -21,11 +21,6 @@ module WPScan
found found
end end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end end
end end
end end

View File

@@ -10,7 +10,7 @@ module WPScan
PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze PATTERN = /#{THEME_PATTERN}\s+#{FRAMEWORK_PATTERN}/i.freeze
def passive(opts = {}) def passive(opts = {})
return unless target.homepage_res.body =~ PATTERN || target.error_404_res.body =~ PATTERN return unless target.homepage_res.body =~ PATTERN
Model::Theme.new( Model::Theme.new(
Regexp.last_match[1], Regexp.last_match[1],

View File

@@ -13,7 +13,7 @@ module WPScan
def valid_credentials?(response) def valid_credentials?(response)
response.code == 302 && response.code == 302 &&
Array(response.headers['Set-Cookie'])&.any? { |cookie| cookie =~ /wordpress_logged_in_/i } response.headers['Set-Cookie']&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
end end
def errored_response?(response) def errored_response?(response)

View File

@@ -8,15 +8,15 @@ module WPScan
include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack include CMSScanner::Finders::Finder::BreadthFirstDictionaryAttack
def login_request(username, password) def login_request(username, password)
target.method_call('wp.getUsersBlogs', [username, password], cache_ttl: 0) target.method_call('wp.getUsersBlogs', [username, password])
end end
def valid_credentials?(response) def valid_credentials?(response)
response.code == 200 && response.body.include?('blogName') response.code == 200 && response.body =~ /blogName/
end end
def errored_response?(response) def errored_response?(response)
response.code != 200 && response.body !~ /Incorrect username or password/i response.code != 200 && response.body !~ /login_error/i
end end
end end
end end

View File

@@ -19,33 +19,11 @@ module WPScan
end end
end end
target.multi_call(methods, cache_ttl: 0).run target.multi_call(methods).run
end
# @param [ IO ] file
# @param [ Integer ] passwords_size
# @return [ Array<String> ] The passwords from the last checked position in the file until there are
# passwords_size passwords retrieved
def passwords_from_wordlist(file, passwords_size)
pwds = []
added_pwds = 0
return pwds if passwords_size.zero?
# Make sure that the main code does not call #sysseek or #count etc
# otherwise the file descriptor will be set to somwehere else
file.each_line(chomp: true) do |line|
pwds << line
added_pwds += 1
break if added_pwds == passwords_size
end
pwds
end end
# @param [ Array<Model::User> ] users # @param [ Array<Model::User> ] users
# @param [ String ] wordlist_path # @param [ Array<String> ] passwords
# @param [ Hash ] opts # @param [ Hash ] opts
# @option opts [ Boolean ] :show_progression # @option opts [ Boolean ] :show_progression
# @option opts [ Integer ] :multicall_max_passwords # @option opts [ Integer ] :multicall_max_passwords
@@ -55,22 +33,18 @@ module WPScan
# TODO: Make rubocop happy about metrics etc # TODO: Make rubocop happy about metrics etc
# #
# rubocop:disable all # rubocop:disable all
def attack(users, wordlist_path, opts = {}) def attack(users, passwords, opts = {})
checked_passwords = 0 wordlist_index = 0
wordlist = File.open(wordlist_path)
wordlist_size = wordlist.count
max_passwords = opts[:multicall_max_passwords] max_passwords = opts[:multicall_max_passwords]
current_passwords_size = passwords_size(max_passwords, users.size) current_passwords_size = passwords_size(max_passwords, users.size)
create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil, create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
show_progression: opts[:show_progression]) show_progression: opts[:show_progression])
wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
loop do loop do
current_users = users.select { |user| user.password.nil? } current_users = users.select { |user| user.password.nil? }
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size) current_passwords = passwords[wordlist_index, current_passwords_size]
checked_passwords += current_passwords_size wordlist_index += current_passwords_size
break if current_users.empty? || current_passwords.nil? || current_passwords.empty? break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
@@ -102,19 +76,16 @@ module WPScan
break break
end end
begin progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
rescue ProgressBar::InvalidProgressError
end
end end
end end
# Maybe a progress_bar.stop ? # Maybe a progress_bar.stop ?
end end
# rubocop:enable all # rubocop:disable all
def passwords_size(max_passwords, users_size) def passwords_size(max_passwords, users_size)
return 1 if max_passwords < users_size return 1 if max_passwords < users_size
return 0 if users_size.zero? return 0 if users_size == 0
max_passwords / users_size max_passwords / users_size
end end
@@ -123,13 +94,9 @@ module WPScan
def check_and_output_errors(res) def check_and_output_errors(res)
progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200 progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
if /parse error. not well formed/i.match?(res.body) progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
end
return unless /requested method [^ ]+ does not exist/i.match?(res.body) progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
progress_bar.log('The requested method is not supported')
end end
end end
end end

View File

@@ -13,15 +13,25 @@ module WPScan
def initialize(plugin) def initialize(plugin)
finders << PluginVersion::Readme.new(plugin) finders << PluginVersion::Readme.new(plugin)
create_and_load_dynamic_versions_finders(plugin) load_specific_finders(plugin)
end end
# Create the dynamic version finders related to the plugin and register them # Load the finders associated with the plugin
# #
# @param [ Model::Plugin ] plugin # @param [ Model::Plugin ] plugin
def create_and_load_dynamic_versions_finders(plugin) def load_specific_finders(plugin)
DB::DynamicFinders::Plugin.create_versions_finders(plugin.slug).each do |finder| module_name = plugin.classify
finders << finder.new(plugin)
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)
end end
end end
end end

View File

@@ -11,7 +11,7 @@ module WPScan
# The target(plugin)#readme_url can't be used directly here # The target(plugin)#readme_url can't be used directly here
# as if the --detection-mode is passive, it will always return nil # as if the --detection-mode is passive, it will always return nil
target.potential_readme_filenames.each do |file| Model::WpItem::READMES.each do |file|
res = target.head_and_get(file) res = target.head_and_get(file)
next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty? next unless res.code == 200 && !(numbers = version_numbers(res.body)).empty?
@@ -48,29 +48,31 @@ module WPScan
# #
# @return [ String, nil ] The version number detected from the stable tag # @return [ String, nil ] The version number detected from the stable tag
def from_stable_tag(body) def from_stable_tag(body)
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
number = Regexp.last_match[1] number = Regexp.last_match[1]
number if /[0-9]+/.match?(number) number if number =~ /[0-9]+/
end end
# @param [ String ] body # @param [ String ] body
# #
# @return [ String, nil ] The best version number detected from the changelog section # @return [ String, nil ] The best version number detected from the changelog section
def from_changelog_section(body) def from_changelog_section(body)
extracted_versions = body.scan(/^=+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[^=]*=+$/i) extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
return if extracted_versions.nil? || extracted_versions.empty? return if extracted_versions.nil? || extracted_versions.empty?
extracted_versions.flatten! extracted_versions.flatten!
# must contain at least one number # must contain at least one number
extracted_versions = extracted_versions.grep(/[0-9]+/) extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
sorted = extracted_versions.sort do |x, y| sorted = extracted_versions.sort do |x, y|
Gem::Version.new(x) <=> Gem::Version.new(y) begin
rescue StandardError Gem::Version.new(x) <=> Gem::Version.new(y)
0 rescue StandardError
0
end
end end
sorted.last sorted.last

View File

@@ -1,7 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'plugins/urls_in_homepage' require_relative 'plugins/urls_in_homepage'
require_relative 'plugins/urls_in_404_page'
require_relative 'plugins/known_locations' require_relative 'plugins/known_locations'
# From the DynamicFinders # From the DynamicFinders
require_relative 'plugins/comment' require_relative 'plugins/comment'
@@ -23,7 +22,6 @@ module WPScan
def initialize(target) def initialize(target)
finders << finders <<
Plugins::UrlsInHomepage.new(target) << Plugins::UrlsInHomepage.new(target) <<
Plugins::UrlsIn404Page.new(target) <<
Plugins::HeaderPattern.new(target) << Plugins::HeaderPattern.new(target) <<
Plugins::Comment.new(target) << Plugins::Comment.new(target) <<
Plugins::Xpath.new(target) << Plugins::Xpath.new(target) <<

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'BodyPattern' # Plugins finder from Dynamic Finder 'BodyPattern'
class BodyPattern < Finders::DynamicFinder::WpItems::Finder class BodyPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30 DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods
@@ -15,7 +15,7 @@ module WPScan
# #
# @return [ Plugin ] The detected plugin in the response, related to the config # @return [ Plugin ] The detected plugin in the response, related to the config
def process_response(opts, response, slug, klass, config) def process_response(opts, response, slug, klass, config)
return unless response.body&.match?(config['pattern']) return unless response.body =~ config['pattern']
Model::Plugin.new( Model::Plugin.new(
slug, slug,

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from the Dynamic Finder 'Comment' # Plugins finder from the Dynamic Finder 'Comment'
class Comment < Finders::DynamicFinder::WpItems::Finder class Comment < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30 DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods
@@ -18,7 +18,7 @@ module WPScan
response.html.xpath(config['xpath'] || '//comment()').each do |node| response.html.xpath(config['xpath'] || '//comment()').each do |node|
comment = node.text.to_s.strip comment = node.text.to_s.strip
next unless comment&.match?(config['pattern']) next unless comment =~ config['pattern']
return Model::Plugin.new( return Model::Plugin.new(
slug, slug,

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'ConfigParser' # Plugins finder from Dynamic Finder 'ConfigParser'
class ConfigParser < Finders::DynamicFinder::WpItems::Finder class ConfigParser < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 40 DEFAULT_CONFIDENCE = 40
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'HeaderPattern' # Plugins finder from Dynamic Finder 'HeaderPattern'
class HeaderPattern < Finders::DynamicFinder::WpItems::Finder class HeaderPattern < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 30 DEFAULT_CONFIDENCE = 30
# @param [ Hash ] opts # @param [ Hash ] opts

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from the Dynamic Finder 'JavascriptVar' # Plugins finder from the Dynamic Finder 'JavascriptVar'
class JavascriptVar < Finders::DynamicFinder::WpItems::Finder class JavascriptVar < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 60 DEFAULT_CONFIDENCE = 60
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -19,14 +19,8 @@ module WPScan
def aggressive(opts = {}) def aggressive(opts = {})
found = [] found = []
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug| enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
finding_opts = opts.merge(found_by: found_by, found << Model::Plugin.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
confidence: 80,
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
found << Model::Plugin.new(slug, target, finding_opts)
raise Error::PluginsThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
end end
found found

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from Dynamic Finder 'QueryParameter' # Plugins finder from Dynamic Finder 'QueryParameter'
class QueryParameter < Finders::DynamicFinder::WpItems::Finder class QueryParameter < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 10 DEFAULT_CONFIDENCE = 10
def passive(_opts = {}) def passive(_opts = {})

View File

@@ -1,16 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module Plugins
# URLs In 404 Page Finder
# Typically, the items detected from URLs like /wp-content/plugins/<slug>/
class UrlsIn404Page < UrlsInHomepage
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.error_404_res
end
end
end
end
end

View File

@@ -4,9 +4,10 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# URLs In Homepage Finder # URLs In Homepage Finder
# Typically, the items detected from URLs like /wp-content/plugins/<slug>/ # Typically, the items detected from URLs like
# /wp-content/plugins/<slug>/
class UrlsInHomepage < CMSScanner::Finders::Finder class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::UrlsInPage include WpItems::URLsInHomepage
# @param [ Hash ] opts # @param [ Hash ] opts
# #
@@ -20,11 +21,6 @@ module WPScan
found found
end end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end end
end end
end end

View File

@@ -4,7 +4,7 @@ module WPScan
module Finders module Finders
module Plugins module Plugins
# Plugins finder from the Dynamic Finder 'Xpath' # Plugins finder from the Dynamic Finder 'Xpath'
class Xpath < Finders::DynamicFinder::WpItems::Finder class Xpath < WPScan::Finders::DynamicFinder::WpItems::Finder
DEFAULT_CONFIDENCE = 40 DEFAULT_CONFIDENCE = 40
# @param [ Hash ] opts The options from the #passive, #aggressive methods # @param [ Hash ] opts The options from the #passive, #aggressive methods

View File

@@ -16,15 +16,25 @@ module WPScan
ThemeVersion::Style.new(theme) << ThemeVersion::Style.new(theme) <<
ThemeVersion::WooFrameworkMetaGenerator.new(theme) ThemeVersion::WooFrameworkMetaGenerator.new(theme)
create_and_load_dynamic_versions_finders(theme) load_specific_finders(theme)
end end
# Create the dynamic version finders related to the theme and register them # Load the finders associated with the theme
# #
# @param [ Model::Theme ] theme # @param [ Model::Theme ] theme
def create_and_load_dynamic_versions_finders(theme) def load_specific_finders(theme)
DB::DynamicFinders::Theme.create_versions_finders(theme.slug).each do |finder| module_name = theme.classify
finders << finder.new(theme)
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)
end end
end end
end end

View File

@@ -30,7 +30,7 @@ module WPScan
# @return [ Version ] # @return [ Version ]
def style_version def style_version
return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z.-]+)/i return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
Model::Version.new( Model::Version.new(
Regexp.last_match[1], Regexp.last_match[1],

View File

@@ -1,13 +1,12 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'themes/urls_in_homepage' require_relative 'themes/urls_in_homepage'
require_relative 'themes/urls_in_404_page'
require_relative 'themes/known_locations' require_relative 'themes/known_locations'
module WPScan module WPScan
module Finders module Finders
module Themes module Themes
# Themes Finder # themes Finder
class Base class Base
include CMSScanner::Finders::SameTypeFinder include CMSScanner::Finders::SameTypeFinder
@@ -15,7 +14,6 @@ module WPScan
def initialize(target) def initialize(target)
finders << finders <<
Themes::UrlsInHomepage.new(target) << Themes::UrlsInHomepage.new(target) <<
Themes::UrlsIn404Page.new(target) <<
Themes::KnownLocations.new(target) Themes::KnownLocations.new(target)
end end
end end

View File

@@ -19,14 +19,8 @@ module WPScan
def aggressive(opts = {}) def aggressive(opts = {})
found = [] found = []
enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |res, slug| enumerate(target_urls(opts), opts.merge(check_full_response: true)) do |_res, slug|
finding_opts = opts.merge(found_by: found_by, found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 80))
confidence: 80,
interesting_entries: ["#{res.effective_url}, status: #{res.code}"])
found << Model::Theme.new(slug, target, finding_opts)
raise Error::ThemesThresholdReached if opts[:threshold].positive? && found.size >= opts[:threshold]
end end
found found

View File

@@ -1,15 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module Themes
# URLs In 04 Page Finder
class UrlsIn404Page < UrlsInHomepage
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.error_404_res
end
end
end
end
end

View File

@@ -5,7 +5,7 @@ module WPScan
module Themes module Themes
# URLs In Homepage Finder # URLs In Homepage Finder
class UrlsInHomepage < CMSScanner::Finders::Finder class UrlsInHomepage < CMSScanner::Finders::Finder
include WpItems::UrlsInPage include WpItems::URLsInHomepage
# @param [ Hash ] opts # @param [ Hash ] opts
# #
@@ -19,11 +19,6 @@ module WPScan
found found
end end
# @return [ Typhoeus::Response ]
def page_res
@page_res ||= target.homepage_res
end
end end
end end
end end

View File

@@ -22,7 +22,7 @@ module WPScan
found = [] found = []
enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res| enumerate(target_urls(opts), opts.merge(check_full_response: 400)) do |res|
next unless /no image specified/i.match?(res.body) next unless res.body =~ /no image specified/i
found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100)) found << Model::Timthumb.new(res.request.url, opts.merge(found_by: found_by, confidence: 100))
end end

View File

@@ -6,21 +6,10 @@ require_relative 'users/oembed_api'
require_relative 'users/rss_generator' require_relative 'users/rss_generator'
require_relative 'users/author_id_brute_forcing' require_relative 'users/author_id_brute_forcing'
require_relative 'users/login_error_messages' require_relative 'users/login_error_messages'
require_relative 'users/author_sitemap' require_relative 'users/yoast_seo_author_sitemap.rb'
require_relative 'users/yoast_seo_author_sitemap'
module WPScan module WPScan
module Finders module Finders
# Specific Finders container to filter the usernames found
# and remove the ones matching ParsedCli.exclude_username if supplied
class UsersFinders < SameTypeFinders
def filter_findings
findings.delete_if { |user| ParsedCli.exclude_usernames.match?(user.username) } if ParsedCli.exclude_usernames
findings
end
end
module Users module Users
# Users Finder # Users Finder
class Base class Base
@@ -33,15 +22,10 @@ module WPScan
Users::WpJsonApi.new(target) << Users::WpJsonApi.new(target) <<
Users::OembedApi.new(target) << Users::OembedApi.new(target) <<
Users::RSSGenerator.new(target) << Users::RSSGenerator.new(target) <<
Users::AuthorSitemap.new(target) <<
Users::YoastSeoAuthorSitemap.new(target) << Users::YoastSeoAuthorSitemap.new(target) <<
Users::AuthorIdBruteForcing.new(target) << Users::AuthorIdBruteForcing.new(target) <<
Users::LoginErrorMessages.new(target) Users::LoginErrorMessages.new(target)
end end
def finders
@finders ||= Finders::UsersFinders.new
end
end end
end end
end end

View File

@@ -71,13 +71,11 @@ module WPScan
return username, 'Display Name', 50 if username return username, 'Display Name', 50 if username
end end
# @param [ String, Addressable::URI ] uri # @param [ String ] url
# #
# @return [ String, nil ] # @return [ String, nil ]
def username_from_author_url(uri) def username_from_author_url(url)
uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI) url[%r{/author/([^/\b]+)/?}i, 1]
uri.path[%r{/author/([^/\b]+)/?}i, 1]
end end
# @param [ Typhoeus::Response ] res # @param [ Typhoeus::Response ] res
@@ -85,12 +83,12 @@ module WPScan
# @return [ String, nil ] The username found # @return [ String, nil ] The username found
def username_from_response(res) def username_from_response(res)
# Permalink enabled # Permalink enabled
target.in_scope_uris(res, '//@href[contains(., "author/")]') do |uri| target.in_scope_uris(res, '//link/@href|//a/@href') do |uri|
username = username_from_author_url(uri) username = username_from_author_url(uri.to_s)
return username if username return username if username
end end
# No permalink, TODO Maybe use xpath to extract the classes ? # No permalink
res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1] res.body[/<body class="archive author author-([^\s]+)[ "]/i, 1]
end end
@@ -99,12 +97,9 @@ module WPScan
# @return [ String, nil ] # @return [ String, nil ]
def display_name_from_body(body) def display_name_from_body(body)
page = Nokogiri::HTML.parse(body) page = Nokogiri::HTML.parse(body)
# WP >= 3.0 # WP >= 3.0
page.css('h1.page-title span').each do |node| page.css('h1.page-title span').each do |node|
text = node.text.to_s.strip return node.text.to_s
return text unless text.empty?
end end
# WP < 3.0 # WP < 3.0

View File

@@ -45,7 +45,7 @@ module WPScan
def potential_usernames(res) def potential_usernames(res)
usernames = [] usernames = []
target.in_scope_uris(res, '//a/@href[contains(., "author")]') do |uri, node| target.in_scope_uris(res, '//a/@href') do |uri, node|
if uri.path =~ %r{/author/([^/\b]+)/?\z}i if uri.path =~ %r{/author/([^/\b]+)/?\z}i
usernames << [Regexp.last_match[1], 'Author Pattern', 100] usernames << [Regexp.last_match[1], 'Author Pattern', 100]
elsif /author=[0-9]+/.match?(uri.query) elsif /author=[0-9]+/.match?(uri.query)

View File

@@ -1,36 +0,0 @@
# frozen_string_literal: true
module WPScan
module Finders
module Users
# Since WP 5.5, /wp-sitemap-users-1.xml is generated and contains
# the usernames of accounts who made a post
class AuthorSitemap < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Array<User> ]
def aggressive(_opts = {})
found = []
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
next unless username && !username.strip.empty?
found << Model::User.new(username,
found_by: found_by,
confidence: 100,
interesting_entries: [sitemap_url])
end
found
end
# @return [ String ] The URL of the sitemap
def sitemap_url
@sitemap_url ||= target.url('wp-sitemap-users-1.xml')
end
end
end
end
end

View File

@@ -24,7 +24,7 @@ module WPScan
return found if error.empty? # Protection plugin / error disabled return found if error.empty? # Protection plugin / error disabled
next unless /The password you entered for the username|Incorrect Password/i.match?(error) next unless error =~ /The password you entered for the username|Incorrect Password/i
found << Model::User.new(username, found_by: found_by, confidence: 100) found << Model::User.new(username, found_by: found_by, confidence: 100)
end end
@@ -37,7 +37,7 @@ module WPScan
# usernames from the potential Users found # usernames from the potential Users found
unames = opts[:found].map(&:username) unames = opts[:found].map(&:username)
Array(opts[:list]).each { |uname| unames << uname.chomp } [*opts[:list]].each { |uname| unames << uname.chomp }
unames.uniq unames.uniq
end end

View File

@@ -34,8 +34,6 @@ module WPScan
def user_details_from_oembed_data(oembed_data) def user_details_from_oembed_data(oembed_data)
return unless oembed_data return unless oembed_data
oembed_data = oembed_data.first if oembed_data.is_a?(Array)
if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z} if oembed_data['author_url'] =~ %r{/author/([^/]+)/?\z}
details = [Regexp.last_match[1], 'Author URL', 90] details = [Regexp.last_match[1], 'Author URL', 90]
elsif oembed_data['author_name'] && !oembed_data['author_name'].empty? elsif oembed_data['author_name'] && !oembed_data['author_name'].empty?

View File

@@ -6,14 +6,14 @@ module WPScan
# Users disclosed from the dc:creator field in the RSS # Users disclosed from the dc:creator field in the RSS
# The names disclosed are display names, however depending on the configuration of the blog, # The names disclosed are display names, however depending on the configuration of the blog,
# they can be the same than usernames # they can be the same than usernames
class RSSGenerator < Finders::WpVersion::RSSGenerator class RSSGenerator < WPScan::Finders::WpVersion::RSSGenerator
def process_urls(urls, _opts = {}) def process_urls(urls, _opts = {})
found = [] found = []
urls.each do |url| urls.each do |url|
res = Browser.get_and_follow_location(url) res = Browser.get_and_follow_location(url)
next unless res.code == 200 && res.body =~ /<dc:creator>/i next unless res.code == 200 && res.body =~ /<dc\:creator>/i
potential_usernames = [] potential_usernames = []

View File

@@ -21,7 +21,7 @@ module WPScan
loop do loop do
current_page += 1 current_page += 1
res = Browser.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page }) res = Typhoeus.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
total_pages ||= res.headers['X-WP-TotalPages'].to_i total_pages ||= res.headers['X-WP-TotalPages'].to_i

View File

@@ -5,7 +5,27 @@ module WPScan
module Users module Users
# The YOAST SEO plugin has an author-sitemap.xml which can leak usernames # The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
# See https://github.com/wpscanteam/wpscan/issues/1228 # See https://github.com/wpscanteam/wpscan/issues/1228
class YoastSeoAuthorSitemap < AuthorSitemap class YoastSeoAuthorSitemap < CMSScanner::Finders::Finder
# @param [ Hash ] opts
#
# @return [ Array<User> ]
def aggressive(_opts = {})
found = []
Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
next unless username && !username.strip.empty?
found << Model::User.new(username,
found_by: found_by,
confidence: 100,
interesting_entries: [sitemap_url])
end
found
end
# @return [ String ] The URL of the author-sitemap # @return [ String ] The URL of the author-sitemap
def sitemap_url def sitemap_url
@sitemap_url ||= target.url('author-sitemap.xml') @sitemap_url ||= target.url('author-sitemap.xml')

View File

@@ -1,3 +1,3 @@
# frozen_string_literal: true # frozen_string_literal: true
require_relative 'wp_items/urls_in_page' require_relative 'wp_items/urls_in_homepage'

View File

@@ -4,24 +4,18 @@ module WPScan
module Finders module Finders
module WpItems module WpItems
# URLs In Homepage Module to use in plugins & themes finders # URLs In Homepage Module to use in plugins & themes finders
module UrlsInPage module URLsInHomepage
# @param [ String ] type plugins / themes # @param [ String ] type plugins / themes
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results # @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
# #
# @return [ Array<String> ] The plugins/themes detected in the href, src attributes of the page # @return [Array<String> ] The plugins/themes detected in the href, src attributes of the homepage
def items_from_links(type, uniq: true) def items_from_links(type, uniq = true)
found = [] found = []
xpath = format(
'(//@href|//@src|//@data-src)[contains(., "%s")]',
type == 'plugins' ? target.plugins_dir : target.content_dir
)
target.in_scope_uris(page_res, xpath) do |uri| target.in_scope_uris(target.homepage_res) do |uri|
next unless uri.to_s =~ item_attribute_pattern(type) next unless uri.to_s =~ item_attribute_pattern(type)
slug = Regexp.last_match[1]&.strip found << Regexp.last_match[1]
found << slug unless slug&.empty?
end end
uniq ? found.uniq.sort : found.sort uniq ? found.uniq.sort : found.sort
@@ -31,10 +25,10 @@ module WPScan
# @param [ Boolean ] uniq Wether or not to apply the #uniq on the results # @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
# #
# @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage # @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
def items_from_codes(type, uniq: true) def items_from_codes(type, uniq = true)
found = [] found = []
page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag| target.homepage_res.html.css('script,style').each do |tag|
code = tag.text.to_s code = tag.text.to_s
next if code.empty? next if code.empty?
@@ -48,14 +42,14 @@ module WPScan
# #
# @return [ Regexp ] # @return [ Regexp ]
def item_attribute_pattern(type) def item_attribute_pattern(type)
@item_attribute_pattern ||= %r{#{item_url_pattern(type)}([^/]+)/}i @item_attribute_pattern ||= %r{\A#{item_url_pattern(type)}([^/]+)/}i
end end
# @param [ String ] type # @param [ String ] type
# #
# @return [ Regexp ] # @return [ Regexp ]
def item_code_pattern(type) def item_code_pattern(type)
@item_code_pattern ||= %r{["'( ]#{item_url_pattern(type)}([^\\/)"']+)}i @item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
end end
# @param [ String ] type # @param [ String ] type
@@ -65,10 +59,10 @@ module WPScan
item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir item_dir = type == 'plugins' ? target.plugins_dir : target.content_dir
item_url = type == 'plugins' ? target.plugins_url : target.content_url item_url = type == 'plugins' ? target.plugins_url : target.content_url
url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i url = /#{item_url.gsub(/\A(?:http|https)/i, 'https?').gsub('/', '\\\\\?\/')}/i
item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?/}i type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
end end
end end
end end

View File

@@ -10,7 +10,7 @@ module WPScan
module Finders module Finders
# Specific Finders container to filter the version detected # Specific Finders container to filter the version detected
# and remove the one with low confidence to avoid false # and remove the one with low confidence to avoid false
# positive when there is not enough information to accurately # positive when there is not enought information to accurately
# determine it. # determine it.
class WpVersionFinders < UniqueFinders class WpVersionFinders < UniqueFinders
def filter_findings def filter_findings
@@ -28,7 +28,7 @@ module WPScan
# @param [ WPScan::Target ] target # @param [ WPScan::Target ] target
def initialize(target) def initialize(target)
(%w[RSSGenerator AtomGenerator RDFGenerator] + (%w[RSSGenerator AtomGenerator RDFGenerator] +
DB::DynamicFinders::Wordpress.versions_finders_configs.keys + WPScan::DB::DynamicFinders::Wordpress.versions_finders_configs.keys +
%w[Readme UniqueFingerprinting] %w[Readme UniqueFingerprinting]
).each do |finder_name| ).each do |finder_name|
finders << WpVersion.const_get(finder_name.to_sym).new(target) finders << WpVersion.const_get(finder_name.to_sym).new(target)

View File

@@ -28,7 +28,7 @@ module WPScan
end end
def passive_urls_xpath def passive_urls_xpath
'//a[contains(@href, "/rdf")]/@href' '//a[contains(@href, "rdf")]/@href'
end end
def aggressive_urls(_opts = {}) def aggressive_urls(_opts = {})

View File

@@ -7,144 +7,46 @@ module WPScan
include References include References
end end
class BackupDB < InterestingFinding #
def to_s # Empty classes for the #type to be correctly displayed (as taken from the self.class from the parent)
@to_s ||= "A backup directory has been found: #{url}" #
end class PluginBackupFolder < InterestingFinding
# @return [ Hash ]
def references
@references ||= { url: ['https://github.com/wpscanteam/wpscan/issues/422'] }
end
end end
class DebugLog < InterestingFinding class DebugLog < InterestingFinding
def to_s
@to_s ||= "Debug Log found: #{url}"
end
# @ return [ Hash ]
def references
@references ||= { url: ['https://codex.wordpress.org/Debugging_in_WordPress'] }
end
end end
class DuplicatorInstallerLog < InterestingFinding class DuplicatorInstallerLog < InterestingFinding
# @return [ Hash ]
def references
@references ||= { url: ['https://www.exploit-db.com/ghdb/3981/'] }
end
end end
class EmergencyPwdResetScript < InterestingFinding class EmergencyPwdResetScript < InterestingFinding
def references
@references ||= {
url: ['https://codex.wordpress.org/Resetting_Your_Password#Using_the_Emergency_Password_Reset_Script']
}
end
end end
class FullPathDisclosure < InterestingFinding class FullPathDisclosure < InterestingFinding
def to_s
@to_s ||= "Full Path Disclosure found: #{url}"
end
# @return [ Hash ]
def references
@references ||= { url: ['https://www.owasp.org/index.php/Full_Path_Disclosure'] }
end
end end
class MuPlugins < InterestingFinding class MuPlugins < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "This site has 'Must Use Plugins': #{url}"
end
# @return [ Hash ]
def references
@references ||= { url: ['http://codex.wordpress.org/Must_Use_Plugins'] }
end
end end
class Multisite < InterestingFinding class Multisite < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= 'This site seems to be a multisite'
end
# @return [ Hash ]
def references
@references ||= { url: ['http://codex.wordpress.org/Glossary#Multisite'] }
end
end end
class Readme < InterestingFinding class Readme < InterestingFinding
def to_s
@to_s ||= "WordPress readme found: #{url}"
end
end end
class Registration < InterestingFinding class Registration < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "Registration is enabled: #{url}"
end
end end
class TmmDbMigrate < InterestingFinding class TmmDbMigrate < InterestingFinding
def to_s
@to_s ||= "ThemeMakers migration file found: #{url}"
end
# @return [ Hash ]
def references
@references ||= { packetstorm: [131_957] }
end
end end
class UploadDirectoryListing < InterestingFinding class UploadDirectoryListing < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "Upload directory has listing enabled: #{url}"
end
end end
class UploadSQLDump < InterestingFinding class UploadSQLDump < InterestingFinding
def to_s
@to_s ||= "SQL Dump found: #{url}"
end
end end
class WPCron < InterestingFinding class WPCron < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= "The external WP-Cron seems to be enabled: #{url}"
end
# @return [ Hash ]
def references
@references ||= {
url: [
'https://www.iplocation.net/defend-wordpress-from-ddos',
'https://github.com/wpscanteam/wpscan/issues/1299'
]
}
end
end
class PHPDisabled < InterestingFinding
# @return [ String ]
def to_s
@to_s ||= 'PHP seems to be disabled'
end
# @return [ Hash ]
def references
@references ||= {
url: ['https://github.com/wpscanteam/wpscan/issues/1593']
}
end
end end
end end
end end

View File

@@ -15,16 +15,9 @@ module WPScan
@uri = Addressable::URI.parse(blog.url(path_from_blog)) @uri = Addressable::URI.parse(blog.url(path_from_blog))
end end
# Retrieve the metadata from the vuln API if available (and a valid token is given), # @return [ JSON ]
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= db_data.empty? ? DB::Plugin.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data def db_data
@db_data ||= DB::VulnApi.plugin_data(slug) @db_data ||= DB::Plugin.db_data(slug)
end end
# @param [ Hash ] opts # @param [ Hash ] opts
@@ -35,11 +28,6 @@ module WPScan
@version @version
end end
# @return [ Array<String> ]
def potential_readme_filenames
@potential_readme_filenames ||= Array((DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super))
end
end end
end end
end end

View File

@@ -21,16 +21,9 @@ module WPScan
parse_style parse_style
end end
# Retrieve the metadata from the vuln API if available (and a valid token is given),
# or the local metadata db otherwise
# @return [ JSON ] # @return [ JSON ]
def metadata
@metadata ||= db_data.empty? ? DB::Theme.metadata_at(slug) : db_data
end
# @return [ Hash ]
def db_data def db_data
@db_data ||= DB::VulnApi.theme_data(slug) @db_data ||= DB::Theme.db_data(slug)
end end
# @param [ Hash ] opts # @param [ Hash ] opts
@@ -45,7 +38,7 @@ module WPScan
# @return [ Theme ] # @return [ Theme ]
def parent_theme def parent_theme
return unless template return unless template
return unless style_body =~ /^@import\surl\(["']?([^"')]+)["']?\);\s*$/i return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
opts = detection_opts.merge( opts = detection_opts.merge(
style_url: url(Regexp.last_match[1]), style_url: url(Regexp.last_match[1]),
@@ -101,7 +94,7 @@ module WPScan
# #
# @return [ String ] # @return [ String ]
def parse_style_tag(body, tag) def parse_style_tag(body, tag)
value = body[/\b#{Regexp.escape(tag)}:[\t ]*([^\r\n*]+)/, 1] value = body[/^\s*#{Regexp.escape(tag)}:[\t ]*([^\r\n]+)/i, 1]
value && !value.strip.empty? ? value.strip : nil value && !value.strip.empty? ? value.strip : nil
end end

View File

@@ -30,7 +30,7 @@ module WPScan
def vulnerabilities def vulnerabilities
vulns = [] vulns = []
vulns << rce_webshot_vuln if version == false || (version > '1.35' && version < '2.8.14' && webshot_enabled?) vulns << rce_webshot_vuln if version == false || version > '1.35' && version < '2.8.14' && webshot_enabled?
vulns << rce_132_vuln if version == false || version < '1.33' vulns << rce_132_vuln if version == false || version < '1.33'
vulns vulns
@@ -40,9 +40,9 @@ module WPScan
def rce_132_vuln def rce_132_vuln
Vulnerability.new( Vulnerability.new(
'Timthumb <= 1.32 Remote Code Execution', 'Timthumb <= 1.32 Remote Code Execution',
references: { exploitdb: ['17602'] }, { exploitdb: ['17602'] },
type: 'RCE', 'RCE',
fixed_in: '1.33' '1.33'
) )
end end
@@ -50,12 +50,12 @@ module WPScan
def rce_webshot_vuln def rce_webshot_vuln
Vulnerability.new( Vulnerability.new(
'Timthumb <= 2.8.13 WebShot Remote Code Execution', 'Timthumb <= 2.8.13 WebShot Remote Code Execution',
references: { {
url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'], url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
cve: '2014-4663' cve: '2014-4663'
}, },
type: 'RCE', 'RCE',
fixed_in: '2.8.14' '2.8.14'
) )
end end
@@ -63,7 +63,7 @@ module WPScan
def webshot_enabled? def webshot_enabled?
res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" }) res = Browser.get(url, params: { webshot: 1, src: "http://#{default_allowed_domains.sample}" })
!/WEBSHOT_ENABLED == true/.match?(res.body) /WEBSHOT_ENABLED == true/.match?(res.body) ? false : true
end end
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13) # @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)

View File

@@ -9,12 +9,11 @@ module WPScan
include CMSScanner::Target::Platform::PHP include CMSScanner::Target::Platform::PHP
include CMSScanner::Target::Server::Generic include CMSScanner::Target::Server::Generic
# 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 READMES = %w[readme.txt README.txt README.md readme.md Readme.txt].freeze
attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data attr_reader :uri, :slug, :detection_opts, :version_detection_opts, :blog, :path_from_blog, :db_data
delegate :homepage_res, :error_404_res, :xpath_pattern_from_page, :in_scope_uris, :head_or_get_params, 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 [ String ] slug The plugin/theme slug
# @param [ Target ] blog The targeted blog # @param [ Target ] blog The targeted blog
@@ -23,7 +22,7 @@ module WPScan
# @option opts [ Hash ] :version_detection The options to use when looking for the version # @option opts [ Hash ] :version_detection The options to use when looking for the version
# @option opts [ String ] :url The URL of the item # @option opts [ String ] :url The URL of the item
def initialize(slug, blog, opts = {}) def initialize(slug, blog, opts = {})
@slug = Addressable::URI.unencode(slug) @slug = URI.decode(slug)
@blog = blog @blog = blog
@uri = Addressable::URI.parse(opts[:url]) if opts[:url] @uri = Addressable::URI.parse(opts[:url]) if opts[:url]
@@ -39,7 +38,7 @@ module WPScan
@vulnerabilities = [] @vulnerabilities = []
Array(db_data['vulnerabilities']).each do |json_vuln| [*db_data['vulnerabilities']].each do |json_vuln|
vulnerability = Vulnerability.load_from_json(json_vuln) vulnerability = Vulnerability.load_from_json(json_vuln)
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability) @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
end end
@@ -53,27 +52,25 @@ module WPScan
# #
# @return [ Boolean ] # @return [ Boolean ]
def vulnerable_to?(vuln) def vulnerable_to?(vuln)
return false if version && vuln&.introduced_in && version < vuln.introduced_in return true unless version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
return true unless version && vuln&.fixed_in && !vuln.fixed_in.empty?
version < vuln.fixed_in version < vuln.fixed_in
end end
# @return [ String ] # @return [ String ]
def latest_version def latest_version
@latest_version ||= metadata['latest_version'] ? Model::Version.new(metadata['latest_version']) : nil @latest_version ||= db_data['latest_version'] ? Model::Version.new(db_data['latest_version']) : nil
end end
# Not used anywhere ATM # Not used anywhere ATM
# @return [ Boolean ] # @return [ Boolean ]
def popular? def popular?
@popular ||= metadata['popular'] ? true : false @popular ||= db_data['popular']
end end
# @return [ String ] # @return [ String ]
def last_updated def last_updated
@last_updated ||= metadata['last_updated'] @last_updated ||= db_data['last_updated']
end end
# @return [ Boolean ] # @return [ Boolean ]
@@ -85,6 +82,11 @@ module WPScan
end end
end end
# URI.encode is preferered over Addressable::URI.encode as it will encode
# leading # character:
# URI.encode('#t#') => %23t%23
# Addressable::URI.encode('#t#') => #t%23
#
# @param [ String ] path Optional path to merge with the uri # @param [ String ] path Optional path to merge with the uri
# #
# @return [ String ] # @return [ String ]
@@ -92,7 +94,7 @@ module WPScan
return unless @uri return unless @uri
return @uri.to_s unless path return @uri.to_s unless path
@uri.join(Addressable::URI.encode(path)).to_s @uri.join(URI.encode(path)).to_s
end end
# @return [ Boolean ] # @return [ Boolean ]
@@ -115,7 +117,7 @@ module WPScan
return @readme_url unless @readme_url.nil? return @readme_url unless @readme_url.nil?
potential_readme_filenames.each do |path| READMES.each do |path|
t_url = url(path) t_url = url(path)
return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200 return @readme_url = t_url if Browser.forge_request(t_url, blog.head_or_get_params).run.code == 200
@@ -124,10 +126,6 @@ module WPScan
@readme_url = false @readme_url = false
end end
def potential_readme_filenames
@potential_readme_filenames ||= READMES
end
# @param [ String ] path # @param [ String ] path
# @param [ Hash ] params The request params # @param [ Hash ] params The request params
# #
@@ -162,8 +160,8 @@ module WPScan
# #
# @return [ Typhoeus::Response ] # @return [ Typhoeus::Response ]
def head_and_get(path, codes = [200], params = {}) def head_and_get(path, codes = [200], params = {})
final_path = @path_from_blog.dup # @path_from_blog is set in the plugin/theme final_path = +@path_from_blog
final_path << path unless path.nil? final_path << URI.encode(path) unless path.nil?
blog.head_and_get(final_path, codes, params) blog.head_and_get(final_path, codes, params)
end end

View File

@@ -35,16 +35,9 @@ module WPScan
@all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) } @all_numbers.sort! { |a, b| Gem::Version.new(b) <=> Gem::Version.new(a) }
end end
# Retrieve the metadata from the vuln API if available (and a valid token is given), # @return [ JSON ]
# or the local metadata db otherwise
# @return [ Hash ]
def metadata
@metadata ||= db_data.empty? ? DB::Version.metadata_at(number) : db_data
end
# @return [ Hash ]
def db_data def db_data
@db_data ||= DB::VulnApi.wordpress_data(number) @db_data ||= DB::Version.db_data(number)
end end
# @return [ Array<Vulnerability> ] # @return [ Array<Vulnerability> ]
@@ -53,7 +46,7 @@ module WPScan
@vulnerabilities = [] @vulnerabilities = []
Array(db_data['vulnerabilities']).each do |json_vuln| [*db_data['vulnerabilities']].each do |json_vuln|
@vulnerabilities << Vulnerability.load_from_json(json_vuln) @vulnerabilities << Vulnerability.load_from_json(json_vuln)
end end
@@ -62,12 +55,12 @@ module WPScan
# @return [ String ] # @return [ String ]
def release_date def release_date
@release_date ||= metadata['release_date'] || 'Unknown' @release_date ||= db_data['release_date'] || 'Unknown'
end end
# @return [ String ] # @return [ String ]
def status def status
@status ||= metadata['status'] || 'Unknown' @status ||= db_data['status'] || 'Unknown'
end end
end end
end end

View File

@@ -8,7 +8,7 @@ module WPScan
# @return [ Hash ] # @return [ Hash ]
def references def references
@references ||= { {
url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'], url: ['http://codex.wordpress.org/XML-RPC_Pingback_API'],
metasploit: [ metasploit: [
'auxiliary/scanner/http/wordpress_ghost_scanner', 'auxiliary/scanner/http/wordpress_ghost_scanner',

View File

@@ -1,14 +1,14 @@
_______________________________________________________________ _______________________________________________________________
__ _______ _____ __ _______ _____
\ \ / / __ \ / ____| \ \ / / __ \ / ____|
\ \ /\ / /| |__) | (___ ___ __ _ _ __ ® \ \ /\ / /| |__) | (___ ___ __ _ _ __ ®
\ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \ \ \/ \/ / | ___/ \___ \ / __|/ _` | '_ \
\ /\ / | | ____) | (__| (_| | | | | \ /\ / | | ____) | (__| (_| | | | |
\/ \/ |_| |_____/ \___|\__,_|_| |_| \/ \/ |_| |_____/ \___|\__,_|_| |_|
WordPress Security Scanner by the WPScan Team WordPress Security Scanner by the WPScan Team
Version <%= WPScan::VERSION %> Version <%= WPScan::VERSION %>
<%= ' ' * ((63 - WPScan::DB::Sponsor.text.length)/2) + WPScan::DB::Sponsor.text %> Sponsored by Sucuri - https://sucuri.net
@_WPScan_, @ethicalhack3r, @erwan_lr, @firefart @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_
_______________________________________________________________ _______________________________________________________________

View File

@@ -5,7 +5,7 @@
<%= notice_icon %> Config Backup(s) Identified: <%= notice_icon %> Config Backup(s) Identified:
<% @config_backups.each do |config_backup| -%> <% @config_backups.each do |config_backup| -%>
<%= critical_icon %> <%= config_backup %> <%= info_icon %> <%= config_backup %>
<%= render('@finding', item: config_backup) -%> <%= render('@finding', item: config_backup) -%>
<% end -%> <% end -%>
<% end %> <% end %>

View File

@@ -5,7 +5,7 @@
<%= notice_icon %> Db Export(s) Identified: <%= notice_icon %> Db Export(s) Identified:
<% @db_exports.each do |db_export| -%> <% @db_exports.each do |db_export| -%>
<%= critical_icon %> <%= db_export %> <%= info_icon %> <%= db_export %>
<%= render('@finding', item: db_export) -%> <%= render('@finding', item: db_export) -%>
<% end -%> <% end -%>
<% end %> <% end %>

View File

@@ -1,4 +1,4 @@
| Found By: <%= @item.found_by %> | Detected By: <%= @item.found_by %>
<% @item.interesting_entries.each do |entry| -%> <% @item.interesting_entries.each do |entry| -%>
| - <%= entry %> | - <%= entry %>
<% end -%> <% end -%>

View File

@@ -2,7 +2,7 @@
<% if @users.empty? -%> <% if @users.empty? -%>
<%= notice_icon %> No Valid Passwords Found. <%= notice_icon %> No Valid Passwords Found.
<% else -%> <% else -%>
<%= critical_icon %> Valid Combinations Found: <%= notice_icon %> Valid Combinations Found:
<% @users.each do |user| -%> <% @users.each do |user| -%>
| Username: <%= user.username %>, Password: <%= user.password %> | Username: <%= user.username %>, Password: <%= user.password %>
<% end -%> <% end -%>

View File

@@ -1,13 +0,0 @@
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
<%= critical_icon %> WPScan DB API, <%= @status['http_error'].to_s %>
<% else -%>
<%= info_icon %> WPScan DB API OK
| Plan: <%= @status['plan'] %>
| Requests Done (during the scan): <%= @api_requests %>
| Requests Remaining: <%= @status['requests_remaining'] %>
<% end -%>
<% else -%>
<%= warning_icon %> No WPScan API Token given, as a result vulnerability data has not been output.
<%= warning_icon %> You can get a free API token with 25 daily requests by registering at https://wpscan.com/register
<% end -%>

View File

@@ -1,7 +1,4 @@
| <%= critical_icon %> Title: <%= @v.title %> | <%= critical_icon %> Title: <%= @v.title %>
<% if @v.cvss -%>
| CVSS: <%= @v.cvss[:score] %> (<%= @v.cvss[:vector] %>)
<% end -%>
<% if @v.fixed_in -%> <% if @v.fixed_in -%>
| Fixed in: <%= @v.fixed_in %> | Fixed in: <%= @v.fixed_in %>
<% end -%> <% end -%>

View File

@@ -1,5 +1,5 @@
<% if @version -%> <% if @version -%>
<%= info_icon %> WordPress version <%= @version.number %> identified (<%= @version.status.tr('-', '_').humanize %>, released on <%= @version.release_date %>). <%= info_icon %> WordPress version <%= @version.number %> identified (<%= @version.status.capitalize %>, released on <%= @version.release_date %>).
<%= render('@finding', item: @version) -%> <%= render('@finding', item: @version) -%>
<% else -%> <% else -%>
<%= notice_icon %> The WordPress version could not be detected. <%= notice_icon %> The WordPress version could not be detected.

View File

@@ -5,7 +5,7 @@
"@_WPScan_", "@_WPScan_",
"@ethicalhack3r", "@ethicalhack3r",
"@erwan_lr", "@erwan_lr",
"@firefart" "@_FireFart_"
], ],
"sponsor": <%= WPScan::DB::Sponsor.text.to_json %> "sponsored_by": "Sucuri - https://sucuri.net"
}, },

View File

@@ -11,21 +11,16 @@
}<% unless index == last_index %>,<% end -%> }<% unless index == last_index %>,<% end -%>
<% end -%> <% end -%>
<% end -%> <% end -%>
} },
<% if @item.respond_to?(:vulnerabilities) -%> "vulnerabilities": [
,"vulnerabilities": [ <% if @item.respond_to?(:vulnerabilities) && !(vulns = @item.vulnerabilities).empty? -%>
<% unless (vulns = @item.vulnerabilities).empty? -%>
<% last_index = vulns.size - 1 -%> <% last_index = vulns.size - 1 -%>
<% vulns.each_with_index do |v, index| -%> <% vulns.each_with_index do |v, index| -%>
{ {
"title": <%= v.title.to_json %>, "title": <%= v.title.to_json %>,
<% if v.cvss -%>
"cvss": <%= v.cvss.to_json %>,
<% end -%>
"fixed_in": <%= v.fixed_in.to_json %>, "fixed_in": <%= v.fixed_in.to_json %>,
"references": <%= v.references.to_json %> "references": <%= v.references.to_json %>
}<% unless index == last_index -%>,<% end -%> }<% unless index == last_index -%>,<% end -%>
<% end -%> <% end -%>
<% end -%> <% end -%>
] ]
<% end -%>

View File

@@ -1,13 +0,0 @@
"vuln_api": {
<% unless @status.empty? -%>
<% if @status['http_error'] -%>
"http_error": <%= @status['http_error'].to_s.to_json %>
<% else -%>
"plan": <%= @status['plan'].to_json %>,
"requests_done_during_scan": <%= @api_requests.to_json %>,
"requests_remaining": <%= @status['requests_remaining'].to_json %>
<% end -%>
<% else -%>
"error": "No WPScan API Token given, as a result vulnerability data has not been output.\nYou can get a free API token with 25 daily requests by registering at https://wpscan.com/register"
<% end -%>
},

Some files were not shown because too many files have changed in this diff Show More