Compare commits

...

52 Commits
2.9 ... 2.9.1

Author SHA1 Message Date
Christian Mehlmauer
f832e27b49 correct stats an correct data files 2016-05-06 11:52:05 +02:00
ethicalhack3r
6ce29f73c5 Update with correct stat #935 2016-05-06 11:35:57 +02:00
ethicalhack3r
920338fb62 Prepare 2.9.1 release #935 2016-05-06 00:15:53 +02:00
Christian Mehlmauer
49d0a9e6d9 check directory listing in wp-includes 2016-05-05 00:01:52 +02:00
Christian Mehlmauer
fe401e622b add stats 2016-05-04 23:09:00 +02:00
Christian Mehlmauer
6e32cb0db2 changelog 2016-05-04 22:46:02 +02:00
Ryan Dewhurst
73171eb39d Merge pull request #929 from wpscanteam/wp_metadata
WP Metadata Integration
2016-04-28 14:35:43 +02:00
ethicalhack3r
2e05f4171e Update to Ruby 2.3.1 2016-04-28 14:04:54 +02:00
Christian Mehlmauer
75b8c303e2 more verbose error 2016-04-27 15:19:07 +02:00
Christian Mehlmauer
bd7a493f1c travis errors 2016-04-20 20:49:17 +02:00
Christian Mehlmauer
9dada7c8f4 travis errors 2016-04-20 20:41:46 +02:00
ethicalhack3r
fe7aede458 Better output 2016-04-20 13:39:05 +02:00
ethicalhack3r
cdf2b38780 Only show changelog if verbose 2016-04-20 13:09:02 +02:00
ethicalhack3r
a09dbab6a8 Use db_file 2016-04-20 12:43:56 +02:00
ethicalhack3r
49a6d275d2 Update comment 2016-04-20 12:37:46 +02:00
ethicalhack3r
8192a4a215 Fix typo 2016-04-20 12:27:09 +02:00
ethicalhack3r
1d6593fd4d Add WP metadata #704 2016-04-20 12:02:15 +02:00
Christian Mehlmauer
bf99e31e70 higher update timeout 2016-04-20 09:33:56 +02:00
Christian Mehlmauer
5386496bdc move wordpress check to the top 2016-04-06 14:13:56 +02:00
Christian Mehlmauer
6451510449 new ruby version with security bugfixes released 2016-04-03 00:34:52 +02:00
Christian Mehlmauer
cd68aa719c possible fix for timeouts 2016-04-01 11:52:13 +02:00
Christian Mehlmauer
b328dc4ff9 possible fix for #912 2016-03-11 09:28:42 +01:00
Christian Mehlmauer
1e1c79aa56 Merge pull request #909 from wpscanteam/ruby_version
drop ruby 1.9 and 2.0 support, whitespaces
2016-02-26 14:08:38 +01:00
Christian Mehlmauer
08650ce156 fix travis 2016-02-25 06:39:47 +01:00
Christian Mehlmauer
a1929719f3 version 2.1.8 minimum requirement 2016-02-24 23:48:50 +01:00
Christian Mehlmauer
d34da72cd3 ruby 2.0.0 is EOL 2016-02-24 23:41:32 +01:00
Christian Mehlmauer
816b18b604 drop ruby 1.9 support, whitespaces 2016-02-23 18:07:20 +01:00
Christian Mehlmauer
a78a13bf3f revert change 2016-02-18 00:02:55 +01:00
Christian Mehlmauer
33f8aaf1dc Merge branch 'master' of github.com:wpscanteam/wpscan 2016-02-17 23:30:45 +01:00
Christian Mehlmauer
26ab95d822 more actual gems 2016-02-17 23:30:28 +01:00
erwanlr
cea01d8aa0 Improves brute forcer output to avoid confustions 2016-02-13 16:44:29 +00:00
Ryan Dewhurst
0e61f1e284 Merge pull request #901 from wpscanteam/new_urls
add new urls
2016-02-06 22:26:25 +01:00
Christian Mehlmauer
ddef061b90 add new urls 2016-02-05 22:25:18 +01:00
erwanlr
addeab8947 Fixes #900 2016-02-04 20:37:13 +01:00
erwanlr
55dc665404 Better specs 2016-01-11 16:33:29 +00:00
erwanlr
8f8538e9e9 Changes the order of the WP version from stylesheets check - Fixes #865 2016-01-11 16:27:22 +00:00
Christian Mehlmauer
348ca55bee copyright 2016-01-08 23:54:04 +01:00
Christian Mehlmauer
1bb5bc7f33 fix rspec 2016-01-03 21:28:02 +01:00
ethicalhack3r
3be5e1fcf5 Add Windows OS detection 2016-01-03 20:15:11 +01:00
Christian Mehlmauer
9df8cc9243 Update README.md 2016-01-02 10:57:55 +01:00
Christian Mehlmauer
e28c84aa34 Update fedore install instructions
See #886
2016-01-02 10:52:23 +01:00
Christian Mehlmauer
7db6b54761 Merge pull request #894 from nonmadden/update-ruby
Update to Ruby 2.3.0
2015-12-31 10:22:47 +01:00
nonmadden
e3a06f5694 Update to Ruby 2.3.0 2015-12-31 10:41:04 +07:00
erwanlr
7c5d15e098 Updates Nokogiri dep 2015-12-18 18:59:32 +01:00
ethicalhack3r
d683c0f151 Update to Ruby 2.2.4 2015-12-18 11:13:41 +01:00
erwanlr
1e67fa26ff Fixes #890 2015-11-26 14:12:04 +00:00
erwanlr
0ae6ef59ec Fixes an issue with --cache-ttl being a Strig instead of an integer 2015-11-26 13:52:12 +00:00
erwanlr
e27ef40e0f Updates Nokogiri dep version 2015-11-26 11:53:13 +00:00
ethicalhack3r
380760d028 Onlt shoe theme description when there is one 2015-10-26 16:06:13 +01:00
ethicalhack3r
18cfdafc19 Fix typo in options 2015-10-15 16:28:42 +02:00
ethicalhack3r
0934a2e329 Recommend RVM in readme 2015-10-15 15:51:38 +02:00
ethicalhack3r
d1a320324e Update reame CLI options 2015-10-15 15:49:18 +02:00
45 changed files with 883 additions and 858 deletions

View File

@@ -1 +1 @@
2.2.3
2.3.1

View File

@@ -2,28 +2,21 @@ language: ruby
sudo: false
cache: bundler
rvm:
- 1.9.2
- 1.9.3
- 2.0.0
- 2.1.0
- 2.1.1
- 2.1.2
- 2.1.3
- 2.1.4
- 2.1.5
# Still not in Travis :(
# - 2.1.9
- 2.2.0
- 2.2.1
- 2.2.2
- 2.2.3
- 2.2.4
- 2.3.0
- 2.3.1
before_install:
- "echo 'gem: --no-ri --no-rdoc' > ~/.gemrc"
script: bundle exec rspec
notifications:
email:
- team@wpscan.org
matrix:
allow_failures:
- rvm: 1.9.2
# do not build gh-pages branch
branches:
except:

View File

@@ -1,6 +1,23 @@
# Changelog
## Master
[Work in progress](https://github.com/wpscanteam/wpscan/compare/2.9...master)
[Work in progress](https://github.com/wpscanteam/wpscan/compare/2.9.1...master)
## Version 2.9.1
Released: 2016-05-06
* Update to Ruby 2.3.1, drop older ruby support
* New data file location
* Added experimental Windows support
* Display WordPress metadata on the detected version
* Several small fixes
WPScan Database Statistics:
* Total vulnerable versions: 156
* Total vulnerable plugins: 1324
* Total vulnerable themes: 376
* Total version vulnerabilities: 1998
* Total plugin vulnerabilities: 2057
* Total theme vulnerabilities: 449
## Version 2.9
Released: 2015-10-15
@@ -137,7 +154,7 @@ New
* Add Sucuri sponsor to banner
* Add protocol to sucuri url in banner
* Add response code to proxy error output
* Add a statement about mendatory newlines at the end of list
* Add a statement about mandatory newlines at the end of list
* Give warning if default username 'admin' is still used
* License amendment to make it more clear about value added usage
@@ -493,4 +510,3 @@ Fixed issues
## Version 2.1
Released 2013-3-4

View File

@@ -1,10 +1,11 @@
source 'https://rubygems.org'
gem 'typhoeus', '~>0.8.0'
gem 'nokogiri'
gem 'typhoeus', '>=0.8.0'
gem 'nokogiri', '>=1.6.7.1'
gem 'addressable'
gem 'yajl-ruby' # Better JSON parser regarding memory usage
# TODO: update the below when terminal-table 1.5.3+ is released.
# See issue #841 for details
# (and delete the Terminal module in lib/common/hacks.rb)
gem 'terminal-table', '~>1.4.5'
gem 'ruby-progressbar', '>=1.6.0'

View File

@@ -1,6 +1,6 @@
WPScan Public Source License
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2015 WPScan Team.
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2016 WPScan Team.
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.

105
README.md
View File

@@ -9,7 +9,7 @@
#### WPScan Public Source License
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2015 WPScan Team.
The WPScan software (henceforth referred to simply as "WPScan") is dual-licensed - Copyright 2011-2016 WPScan Team.
Cases that include commercialization of WPScan require a commercial, non-free license. Otherwise, WPScan can be used without charge under the terms set out below.
@@ -92,7 +92,7 @@ WPScan comes pre-installed on the following Linux distributions:
Prerequisites:
- Ruby >= 1.9.2 - Recommended: 2.2.3
- Ruby >= 2.1.9 - Recommended: 2.3.1
- Curl >= 7.21 - Recommended: latest - FYI the 7.29 has a segfault
- RubyGems - Recommended: latest
- Git
@@ -118,7 +118,7 @@ From Ubuntu 14.04:
####Installing on Debian:
sudo apt-get install git ruby ruby-dev libcurl4-openssl-dev make
sudo apt-get install git ruby ruby-dev libcurl4-openssl-dev make zlib1g-dev
git clone https://github.com/wpscanteam/wpscan.git
cd wpscan
sudo gem install bundler
@@ -126,7 +126,7 @@ From Ubuntu 14.04:
####Installing on Fedora:
sudo yum install gcc ruby-devel libxml2 libxml2-devel libxslt libxslt-devel libcurl-devel patch
sudo dnf install gcc ruby-devel libxml2 libxml2-devel libxslt libxslt-devel libcurl-devel patch rpm-build
git clone https://github.com/wpscanteam/wpscan.git
cd wpscan
sudo gem install bundler && bundle install --without test
@@ -149,14 +149,15 @@ Apple Xcode, Command Line Tools and the libffi are needed (to be able to install
cd wpscan
sudo gem install bundler && sudo bundle install --without test
####Installing with RVM:
####Installing with RVM (recommended):
# Install all prerequisites for your OS (look above)
cd ~
curl -sSL https://get.rvm.io | bash -s stable
source ~/.rvm/scripts/rvm
echo "source ~/.rvm/scripts/rvm" >> ~/.bashrc
rvm install 2.2.3
rvm use 2.2.3 --default
rvm install 2.3.1
rvm use 2.3.1 --default
echo "gem: --no-ri --no-rdoc" > ~/.gemrc
gem install bundler
git clone https://github.com/wpscanteam/wpscan.git
@@ -191,7 +192,7 @@ Apple Xcode, Command Line Tools and the libffi are needed (to be able to install
Then, open the directory of the readline gem (you have to locate it)
cd ~/.rvm/src/ruby-1.9.2-p180/ext/readline
cd ~/.rvm/src/ruby-XXXX/ext/readline
ruby extconf.rb
make
make install
@@ -209,13 +210,10 @@ Apple Xcode, Command Line Tools and the libffi are needed (to be able to install
#### WPSCAN ARGUMENTS
--update Update the databases.
--url | -u <target url> The WordPress URL/domain to scan.
--force | -f Forces WPScan to not check if the remote site is running WordPress.
--enumerate | -e [option(s)] Enumeration.
--update Update the database to the latest version.
--url | -u <target url> The WordPress URL/domain to scan.
--force | -f Forces WPScan to not check if the remote site is running WordPress.
--enumerate | -e [option(s)] Enumeration.
option :
u usernames from id 1 to 10
u[10-20] usernames from id 10 to 20 (you must write [] chars)
@@ -229,53 +227,36 @@ Apple Xcode, Command Line Tools and the libffi are needed (to be able to install
Multiple values are allowed : "-e tt,p" will enumerate timthumbs and plugins
If no option is supplied, the default is "vt,tt,u,vp"
--exclude-content-based "<regexp or string>" Used with the enumeration option, will exclude all occurrences based on the regexp or string supplied
You do not need to provide the regexp delimiters, but you must write the quotes (simple or double)
--config-file | -c <config file> Use the specified config file, see the example.conf.json
--user-agent | -a <User-Agent> Use the specified User-Agent
--random-agent | -r Use a random User-Agent
--follow-redirection If the target url has a redirection, it will be followed without asking if you wanted to do so or not
--wp-content-dir <wp content dir> WPScan try to find the content directory (ie wp-content) by scanning the index page, however you can specified it. Subdirectories are allowed
--wp-plugins-dir <wp plugins dir> Same thing than --wp-content-dir but for the plugins directory. If not supplied, WPScan will use wp-content-dir/plugins. Subdirectories are allowed
--proxy <[protocol://]host:port> Supply a proxy (will override the one from conf/browser.conf.json).
HTTP, SOCKS4 SOCKS4A and SOCKS5 are supported. If no protocol is given (format host:port), HTTP will be used
--proxy-auth <username:password> Supply the proxy login credentials.
--basic-auth <username:password> Set the HTTP Basic authentication.
--wordlist | -w <wordlist> Supply a wordlist for the password brute forcer.
--threads | -t <number of threads> The number of threads to use when multi-threading requests.
--username | -U <username> Only brute force the supplied username.
--usernames <path-to-file> Only brute force the usernames from the file.
--cache-ttl <cache-ttl> Typhoeus cache TTL.
--request-timeout <request-timeout> Request Timeout.
--connect-timeout <connect-timeout> Connect Timeout.
--max-threads <max-threads> Maximum Threads.
--help | -h This help screen.
--verbose | -v Verbose output.
--batch Never ask for user input, use the default behavior.
--no-color Do not use colors in the output.
--log Save STDOUT to log.txt
--exclude-content-based "<regexp or string>"
Used with the enumeration option, will exclude all occurrences based on the regexp or string supplied.
You do not need to provide the regexp delimiters, but you must write the quotes (simple or double).
--config-file | -c <config file> Use the specified config file, see the example.conf.json.
--user-agent | -a <User-Agent> Use the specified User-Agent.
--cookie <String> String to read cookies from.
--random-agent | -r Use a random User-Agent.
--follow-redirection If the target url has a redirection, it will be followed without asking if you wanted to do so or not
--batch Never ask for user input, use the default behaviour.
--no-color Do not use colors in the output.
--wp-content-dir <wp content dir> WPScan try to find the content directory (ie wp-content) by scanning the index page, however you can specified it.
Subdirectories are allowed.
--wp-plugins-dir <wp plugins dir> Same thing than --wp-content-dir but for the plugins directory.
If not supplied, WPScan will use wp-content-dir/plugins. Subdirectories are allowed
--proxy <[protocol://]host:port> Supply a proxy. HTTP, SOCKS4 SOCKS4A and SOCKS5 are supported.
If no protocol is given (format host:port), HTTP will be used.
--proxy-auth <username:password> Supply the proxy login credentials.
--basic-auth <username:password> Set the HTTP Basic authentication.
--wordlist | -w <wordlist> Supply a wordlist for the password brute forcer.
--username | -U <username> Only brute force the supplied username.
--usernames <path-to-file> Only brute force the usernames from the file.
--threads | -t <number of threads> The number of threads to use when multi-threading requests.
--cache-ttl <cache-ttl> Typhoeus cache TTL.
--request-timeout <request-timeout> Request Timeout.
--connect-timeout <connect-timeout> Connect Timeout.
--max-threads <max-threads> Maximum Threads.
--throttle <milliseconds> Milliseconds to wait before doing another web request. If used, the --threads should be set to 1.
--help | -h This help screen.
--verbose | -v Verbose output.
--version Output the current version and exit.
#### WPSCAN EXAMPLES

BIN
data.zip

Binary file not shown.

19
dev/stats.rb Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env ruby
# encoding: UTF-8
require File.dirname(__FILE__) + '/../lib/wpscan/wpscan_helper'
wordpress_json = json(WORDPRESSES_FILE)
plugins_json = json(PLUGINS_FILE)
themes_json = json(THEMES_FILE)
puts 'WPScan Database Statistics:'
puts "* Total tracked wordpresses: #{wordpress_json.count}"
puts "* Total tracked plugins: #{plugins_json.count}"
puts "* Total tracked themes: #{themes_json.count}"
puts "* Total vulnerable wordpresses: #{wordpress_json.select { |item| !wordpress_json[item]['vulnerabilities'].empty? }.count}"
puts "* Total vulnerable plugins: #{plugins_json.select { |item| !plugins_json[item]['vulnerabilities'].empty? }.count}"
puts "* Total vulnerable themes: #{themes_json.select { |item| !themes_json[item]['vulnerabilities'].empty? }.count}"
puts "* Total wordpress vulnerabilities: #{}"
puts "* Total plugin vulnerabilities: #{}"
puts "* Total theme vulnerabilities: #{}"

View File

@@ -143,8 +143,8 @@ class Browser
end
params.merge!(referer: referer)
params.merge!(timeout: @request_timeout) if @request_timeout
params.merge!(connecttimeout: @connect_timeout) if @connect_timeout
params.merge!(timeout: @request_timeout) if @request_timeout && !params.key?(:timeout)
params.merge!(connecttimeout: @connect_timeout) if @connect_timeout && !params.key?(:connecttimeout)
# Used to enable the cache system if :cache_ttl > 0
params.merge!(cache_ttl: @cache_ttl) unless params.key?(:cache_ttl)
@@ -153,8 +153,8 @@ class Browser
params.merge!(maxredirs: 3) unless params.key?(:maxredirs)
# Disable SSL-Certificate checks
params.merge!(ssl_verifypeer: false)
params.merge!(ssl_verifyhost: 0)
params.merge!(ssl_verifypeer: false) unless params.key?(:ssl_verifypeer)
params.merge!(ssl_verifyhost: 0) unless params.key?(:ssl_verifyhost)
params.merge!(cookiejar: @cache_dir + '/cookie-jar')
params.merge!(cookiefile: @cache_dir + '/cookie-jar')

View File

@@ -3,8 +3,8 @@
class Browser
module Options
attr_accessor :cache_ttl, :request_timeout, :connect_timeout
attr_reader :basic_auth, :proxy, :proxy_auth, :throttle
attr_accessor :request_timeout, :connect_timeout
attr_reader :basic_auth, :cache_ttl, :proxy, :proxy_auth, :throttle
attr_writer :user_agent
# Sets the Basic Authentification credentials
@@ -25,6 +25,10 @@ class Browser
end
end
def cache_ttl=(ttl)
@cache_ttl = ttl.to_i
end
# @return [ Integer ]
def max_threads
@max_threads || 1

View File

@@ -23,9 +23,7 @@ class CacheFileStore
@storage_path = File.expand_path(File.join(storage_path, storage_dir))
@serializer = serializer
# File.directory? for ruby <= 1.9 otherwise,
# it makes more sense to do Dir.exist? :/
unless File.directory?(@storage_path)
unless Dir.exist?(@storage_path)
FileUtils.mkdir_p(@storage_path)
end
end
@@ -51,7 +49,7 @@ class CacheFileStore
end
def write_entry(key, data_to_store, cache_ttl)
if cache_ttl > 0
if cache_ttl && cache_ttl > 0
File.open(get_entry_file_path(key), 'w') do |f|
begin
f.write(@serializer.dump(data_to_store))

View File

@@ -67,7 +67,7 @@ class WpItems < Array
end
protected
# @return [ Class ]
def item_class
Object.const_get(self.class.to_s.gsub(/.$/, ''))

View File

@@ -1,8 +1,8 @@
# encoding: UTF-8
require 'common/collections/wp_plugins/detectable'
class WpPlugins < WpItems
extend WpPlugins::Detectable
end
# encoding: UTF-8
require 'common/collections/wp_plugins/detectable'
class WpPlugins < WpItems
extend WpPlugins::Detectable
end

View File

@@ -1,8 +1,8 @@
# encoding: UTF-8
require 'common/collections/wp_themes/detectable'
class WpThemes < WpItems
extend WpThemes::Detectable
end
# encoding: UTF-8
require 'common/collections/wp_themes/detectable'
class WpThemes < WpItems
extend WpThemes::Detectable
end

View File

@@ -1,8 +1,8 @@
# encoding: UTF-8
require 'common/collections/wp_timthumbs/detectable'
class WpTimthumbs < WpItems
extend WpTimthumbs::Detectable
end
# encoding: UTF-8
require 'common/collections/wp_timthumbs/detectable'
class WpTimthumbs < WpItems
extend WpTimthumbs::Detectable
end

View File

@@ -1,11 +1,11 @@
# encoding: UTF-8
require 'common/collections/wp_users/detectable'
require 'common/collections/wp_users/output'
require 'common/collections/wp_users/brute_forcable'
class WpUsers < WpItems
extend WpUsers::Detectable
include WpUsers::Output
include WpUsers::BruteForcable
end
# encoding: UTF-8
require 'common/collections/wp_users/detectable'
require 'common/collections/wp_users/output'
require 'common/collections/wp_users/brute_forcable'
class WpUsers < WpItems
extend WpUsers::Detectable
include WpUsers::Output
include WpUsers::BruteForcable
end

View File

@@ -1,34 +1,34 @@
# encoding: UTF-8
class WpUsers < WpItems
module Detectable
# @return [ Hash ]
def request_params; {} end
# No passive detection
#
# @return [ WpUsers ]
def passive_detection(wp_target, options = {})
new
end
protected
# @param [ WpTarget ] wp_target
# @param [ Hash ] options
# @option options [ Range ] :range ((1..10))
#
# @return [ Array<WpUser> ]
def targets_items(wp_target, options = {})
range = options[:range] || (1..10)
targets = []
range.each do |user_id|
targets << WpUser.new(wp_target.uri, id: user_id)
end
targets
end
end
end
# encoding: UTF-8
class WpUsers < WpItems
module Detectable
# @return [ Hash ]
def request_params; {} end
# No passive detection
#
# @return [ WpUsers ]
def passive_detection(wp_target, options = {})
new
end
protected
# @param [ WpTarget ] wp_target
# @param [ Hash ] options
# @option options [ Range ] :range ((1..10))
#
# @return [ Array<WpUser> ]
def targets_items(wp_target, options = {})
range = options[:range] || (1..10)
targets = []
range.each do |user_id|
targets << WpUser.new(wp_target.uri, id: user_id)
end
targets
end
end
end

View File

@@ -28,7 +28,9 @@ LOCAL_FILES_XSD = File.join(DATA_DIR, 'local_vulnerable_files.xsd')
USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt')
LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update')
WPSCAN_VERSION = '2.9'
MIN_RUBY_VERSION = '2.1.9'
WPSCAN_VERSION = '2.9.1'
$LOAD_PATH.unshift(LIB_DIR)
$LOAD_PATH.unshift(WPSCAN_LIB_DIR)
@@ -42,6 +44,11 @@ def kali_linux?
end
end
# Determins if installed on Windows OS
def windows?
Gem.win_platform?
end
require 'environment'
def escape_glob(s)
@@ -223,7 +230,11 @@ end
#
# @return [ Integer ] The number of lines in the given file
def count_file_lines(file)
`wc -l #{file.shellescape}`.split[0].to_i
if windows?
`findstr /R /N "^" #{file.shellescape} | find /C ":"`.split[0].to_i
else
`wc -l #{file.shellescape}`.split[0].to_i
end
end
# Truncates a string to a specific length and adds ... at the end
@@ -257,3 +268,7 @@ end
def directory_listing_enabled?(url)
Browser.get(url.to_s).body[%r{<title>Index of}] ? true : false
end
def url_encode(str)
CGI.escape(str).gsub("+", "%20")
end

View File

@@ -22,13 +22,15 @@ class DbUpdater
{
ssl_verifyhost: 2,
ssl_verifypeer: true,
accept_encoding: 'gzip, deflate'
accept_encoding: 'gzip, deflate',
timeout: 300,
connecttimeout: 20
}
end
# @return [ String ] The raw file URL associated with the given filename
def remote_file_url(filename)
"https://wpvulndb.com/data/#{filename}"
"https://data.wpscan.org/#{filename}"
end
# @return [ String ] The checksum of the associated remote filename
@@ -99,7 +101,7 @@ class DbUpdater
puts " [i] Database File Checksum : #{db_checksum}" if verbose
unless dl_checksum == db_checksum
fail "#{filename}: checksums do not match"
fail "#{filename}: checksums do not match (local: #{dl_checksum} remote: #{db_checksum})"
end
rescue => e
puts ' [i] Restoring Backup due to error' if verbose

View File

@@ -1,35 +1,5 @@
# encoding: UTF-8
# Since ruby 1.9.2, URI::escape is obsolete
# See http://rosettacode.org/wiki/URL_encoding#Ruby and http://www.ruby-forum.com/topic/207489
if RUBY_VERSION >= '1.9.2'
module URI
extend self
def escape(str)
URI::Parser.new.escape(str)
end
alias :encode :escape
end
end
if RUBY_VERSION < '1.9'
class Array
# Fix for grep with symbols in ruby <= 1.8.7
def _grep_(regexp)
matches = []
self.each do |value|
value = value.to_s
matches << value if value.match(regexp)
end
matches
end
alias_method :grep, :_grep_
end
end
# This is used in WpItem::Existable
module Typhoeus
class Response
@@ -101,8 +71,8 @@ end
class Numeric
def bytes_to_human
units = %w{B KB MB GB TB}
e = (Math.log(self)/Math.log(1024)).floor
s = '%.3f' % (to_f / 1024**e)
e = (Math.log(abs)/Math.log(1024)).floor
s = '%.3f' % (abs.to_f / 1024**e)
s.sub(/\.?0*$/, ' ' + units[e])
end
end

View File

@@ -1,62 +1,62 @@
# encoding: UTF-8
require 'vulnerability/output'
require 'vulnerability/urls'
class Vulnerability
include Vulnerability::Output
include Vulnerability::Urls
attr_accessor :title, :references, :type, :fixed_in
#
# @param [ String ] title The title of the vulnerability
# @param [ String ] type The type of the vulnerability
# @param [ Hash ] references References
# @param [ String ] fixed_in Vuln fixed in Version X
#
# @return [ Vulnerability ]
def initialize(title, type, references = {}, fixed_in = '')
@title = title
@type = type
@references = references
@fixed_in = fixed_in
end
# @param [ Vulnerability ] other
#
# @return [ Boolean ]
# :nocov:
def ==(other)
title == other.title &&
type == other.type &&
references == other.references &&
fixed_in == other.fixed_in
end
# :nocov:
# Create the Vulnerability from the json_item
#
# @param [ Hash ] json_item
#
# @return [ Vulnerability ]
def self.load_from_json_item(json_item)
references = {}
references['id'] = [json_item['id']]
%w(url cve secunia osvdb metasploit exploitdb).each do |key|
if json_item['references'][key]
json_item['references'][key] = [json_item['references'][key]] if json_item['references'][key].class != Array
references[key] = json_item['references'][key]
end
end
new(
json_item['title'],
json_item['type'],
references,
json_item['fixed_in']
)
end
end
# encoding: UTF-8
require 'vulnerability/output'
require 'vulnerability/urls'
class Vulnerability
include Vulnerability::Output
include Vulnerability::Urls
attr_accessor :title, :references, :type, :fixed_in
#
# @param [ String ] title The title of the vulnerability
# @param [ String ] type The type of the vulnerability
# @param [ Hash ] references References
# @param [ String ] fixed_in Vuln fixed in Version X
#
# @return [ Vulnerability ]
def initialize(title, type, references = {}, fixed_in = '')
@title = title
@type = type
@references = references
@fixed_in = fixed_in
end
# @param [ Vulnerability ] other
#
# @return [ Boolean ]
# :nocov:
def ==(other)
title == other.title &&
type == other.type &&
references == other.references &&
fixed_in == other.fixed_in
end
# :nocov:
# Create the Vulnerability from the json_item
#
# @param [ Hash ] json_item
#
# @return [ Vulnerability ]
def self.load_from_json_item(json_item)
references = {}
references['id'] = [json_item['id']]
%w(url cve secunia osvdb metasploit exploitdb).each do |key|
if json_item['references'][key]
json_item['references'][key] = [json_item['references'][key]] if json_item['references'][key].class != Array
references[key] = json_item['references'][key]
end
end
new(
json_item['title'],
json_item['type'],
references,
json_item['fixed_in']
)
end
end

View File

@@ -1,123 +1,121 @@
# encoding: UTF-8
require 'wp_item/findable'
require 'wp_item/versionable'
require 'wp_item/vulnerable'
require 'wp_item/existable'
require 'wp_item/infos'
require 'wp_item/output'
class WpItem
extend WpItem::Findable
include WpItem::Versionable
include WpItem::Vulnerable
include WpItem::Existable
include WpItem::Infos
include WpItem::Output
attr_reader :path
attr_accessor :name, :wp_content_dir, :wp_plugins_dir
# @return [ Array ]
# Make it private ?
def allowed_options
[:name, :wp_content_dir, :wp_plugins_dir, :path, :version, :db_file]
end
# @param [ URI ] target_base_uri
# @param [ Hash ] options See allowed_option
#
# @return [ WpItem ]
def initialize(target_base_uri, options = {})
options[:wp_content_dir] ||= 'wp-content'
options[:wp_plugins_dir] ||= options[:wp_content_dir] + '/plugins'
set_options(options)
forge_uri(target_base_uri)
end
def identifier
@identifier ||= name
end
# @return [ Hash ]
def db_data
@db_data ||= json(db_file)[identifier] || {}
end
def latest_version
db_data['latest_version']
end
def last_updated
db_data['last_ipdated']
end
def popular?
db_data['popular']
end
# @param [ Hash ] options
#
# @return [ void ]
def set_options(options)
allowed_options.each do |allowed_option|
if options.has_key?(allowed_option)
method = :"#{allowed_option}="
if self.respond_to?(method)
self.send(method, options[allowed_option])
else
raise "#{self.class} does not respond to #{method}"
end
end
end
end
private :set_options
# @param [ URI ] target_base_uri
#
# @return [ void ]
def forge_uri(target_base_uri)
@uri = target_base_uri
end
# @return [ URI ] The uri to the WpItem, with the path if present
def uri
path ? @uri.merge(path) : @uri
end
# @return [ String ] The url to the WpItem
def url; uri.to_s end
# Sets the path
#
# Variable, such as $wp-plugins$ and $wp-content$ can be used
# and will be replace by their value
#
# @param [ String ] path
#
# @return [ void ]
def path=(path)
@path = URI.encode(
path.gsub(/\$wp-plugins\$/i, wp_plugins_dir).gsub(/\$wp-content\$/i, wp_content_dir)
)
end
# @param [ WpItem ] other
def <=>(other)
name <=> other.name
end
# @param [ WpItem ] other
def ==(other)
name === other.name
end
# @param [ WpItem ] other
def ===(other)
self == other && version === other.version
end
end
# encoding: UTF-8
require 'wp_item/findable'
require 'wp_item/versionable'
require 'wp_item/vulnerable'
require 'wp_item/existable'
require 'wp_item/infos'
require 'wp_item/output'
class WpItem
extend WpItem::Findable
include WpItem::Versionable
include WpItem::Vulnerable
include WpItem::Existable
include WpItem::Infos
include WpItem::Output
attr_reader :path
attr_accessor :name, :wp_content_dir, :wp_plugins_dir
# @return [ Array ]
# Make it private ?
def allowed_options
[:name, :wp_content_dir, :wp_plugins_dir, :path, :version, :db_file]
end
# @param [ URI ] target_base_uri
# @param [ Hash ] options See allowed_option
#
# @return [ WpItem ]
def initialize(target_base_uri, options = {})
options[:wp_content_dir] ||= 'wp-content'
options[:wp_plugins_dir] ||= options[:wp_content_dir] + '/plugins'
set_options(options)
forge_uri(target_base_uri)
end
def identifier
@identifier ||= name
end
# @return [ Hash ]
def db_data
@db_data ||= json(db_file)[identifier] || {}
end
def latest_version
db_data['latest_version']
end
def last_updated
db_data['last_ipdated']
end
def popular?
db_data['popular']
end
# @param [ Hash ] options
#
# @return [ void ]
def set_options(options)
allowed_options.each do |allowed_option|
if options.has_key?(allowed_option)
method = :"#{allowed_option}="
if self.respond_to?(method)
self.send(method, options[allowed_option])
else
raise "#{self.class} does not respond to #{method}"
end
end
end
end
private :set_options
# @param [ URI ] target_base_uri
#
# @return [ void ]
def forge_uri(target_base_uri)
@uri = target_base_uri
end
# @return [ URI ] The uri to the WpItem, with the path if present
def uri
path ? @uri.merge(path) : @uri
end
# @return [ String ] The url to the WpItem
def url; uri.to_s end
# Sets the path
#
# Variable, such as $wp-plugins$ and $wp-content$ can be used
# and will be replace by their value
#
# @param [ String ] path
#
# @return [ void ]
def path=(path)
@path = path.gsub(/\$wp-plugins\$/i, wp_plugins_dir).gsub(/\$wp-content\$/i, wp_content_dir)
end
# @param [ WpItem ] other
def <=>(other)
name <=> other.name
end
# @param [ WpItem ] other
def ==(other)
name === other.name
end
# @param [ WpItem ] other
def ===(other)
self == other && version === other.version
end
end

View File

@@ -1,50 +1,50 @@
# encoding: UTF-8
class WpItem
module Existable
# Check the existence of the WpItem
# If the response is supplied, it's used for the verification
# Otherwise a new request is done
#
# @param [ Hash ] options See exists_from_response?
# @param [ Typhoeus::Response ] response
#
# @return [ Boolean ]
def exists?(options = {}, response = nil)
unless response
response = Browser.get(url)
end
exists_from_response?(response, options)
end
protected
# @param [ Typhoeus::Response ] response
# @param [ options ] options
#
# @option options [ Hash ] :error_404_hash The hash of the error 404 page
# @option options [ Hash ] :homepage_hash The hash of the homepage
# @option options [ Hash ] :exclude_content A regexp with the pattern to exclude from the body of the response
#
# @return [ Boolean ]
def exists_from_response?(response, options = {})
# 301 included as some items do a self-redirect
# Redirects to the 404 and homepage should be ignored (unless dynamic content is used)
# by the page hashes (error_404_hash & homepage_hash)
if [200, 401, 403, 301].include?(response.code)
if response.has_valid_hash?(options[:error_404_hash], options[:homepage_hash])
if options[:exclude_content]
unless response.body.match(options[:exclude_content])
return true
end
else
return true
end
end
end
false
end
end
end
# encoding: UTF-8
class WpItem
module Existable
# Check the existence of the WpItem
# If the response is supplied, it's used for the verification
# Otherwise a new request is done
#
# @param [ Hash ] options See exists_from_response?
# @param [ Typhoeus::Response ] response
#
# @return [ Boolean ]
def exists?(options = {}, response = nil)
unless response
response = Browser.get(url)
end
exists_from_response?(response, options)
end
protected
# @param [ Typhoeus::Response ] response
# @param [ options ] options
#
# @option options [ Hash ] :error_404_hash The hash of the error 404 page
# @option options [ Hash ] :homepage_hash The hash of the homepage
# @option options [ Hash ] :exclude_content A regexp with the pattern to exclude from the body of the response
#
# @return [ Boolean ]
def exists_from_response?(response, options = {})
# 301 included as some items do a self-redirect
# Redirects to the 404 and homepage should be ignored (unless dynamic content is used)
# by the page hashes (error_404_hash & homepage_hash)
if [200, 401, 403, 301].include?(response.code)
if response.has_valid_hash?(options[:error_404_hash], options[:homepage_hash])
if options[:exclude_content]
unless response.body.match(options[:exclude_content])
return true
end
else
return true
end
end
end
false
end
end
end

View File

@@ -1,19 +1,19 @@
# encoding: UTF-8
class WpItem
attr_reader :found_from
# Sets the found_from attribute
#
# @param [ String ] method The method which found the WpItem
#
# @return [ void ]
def found_from=(method)
found = method[%r{find_from_(.*)}, 1]
@found_from = found.gsub('_', ' ') if found
end
module Findable
end
end
# encoding: UTF-8
class WpItem
attr_reader :found_from
# Sets the found_from attribute
#
# @param [ String ] method The method which found the WpItem
#
# @return [ void ]
def found_from=(method)
found = method[%r{find_from_(.*)}, 1]
@found_from = found.gsub('_', ' ') if found
end
module Findable
end
end

View File

@@ -1,44 +1,44 @@
# encoding: UTF-8
class WpItem
module Vulnerable
attr_accessor :db_file, :identifier
# Get the vulnerabilities associated to the WpItem
# Filters out already fixed vulnerabilities
#
# @return [ Vulnerabilities ]
def vulnerabilities
return @vulnerabilities if @vulnerabilities
@vulnerabilities = Vulnerabilities.new
[*db_data['vulnerabilities']].each do |vulnerability|
vulnerability = Vulnerability.load_from_json_item(vulnerability)
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
end
@vulnerabilities
end
def vulnerable?
vulnerabilities.empty? ? false : true
end
# Checks if a item is vulnerable to a specific vulnerability
#
# @param [ Vulnerability ] vuln Vulnerability to check the item against
#
# @return [ Boolean ]
def vulnerable_to?(vuln)
if version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
unless VersionCompare::lesser_or_equal?(vuln.fixed_in, version)
return true
end
else
return true
end
return false
end
end
end
# encoding: UTF-8
class WpItem
module Vulnerable
attr_accessor :db_file, :identifier
# Get the vulnerabilities associated to the WpItem
# Filters out already fixed vulnerabilities
#
# @return [ Vulnerabilities ]
def vulnerabilities
return @vulnerabilities if @vulnerabilities
@vulnerabilities = Vulnerabilities.new
[*db_data['vulnerabilities']].each do |vulnerability|
vulnerability = Vulnerability.load_from_json_item(vulnerability)
@vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
end
@vulnerabilities
end
def vulnerable?
vulnerabilities.empty? ? false : true
end
# Checks if a item is vulnerable to a specific vulnerability
#
# @param [ Vulnerability ] vuln Vulnerability to check the item against
#
# @return [ Boolean ]
def vulnerable_to?(vuln)
if version && vuln && vuln.fixed_in && !vuln.fixed_in.empty?
unless VersionCompare::lesser_or_equal?(vuln.fixed_in, version)
return true
end
else
return true
end
return false
end
end
end

View File

@@ -1,16 +1,16 @@
# encoding: UTF-8
class WpPlugin < WpItem
# Sets the @uri
#
# @param [ URI ] target_base_uri The URI of the wordpress blog
#
# @return [ void ]
def forge_uri(target_base_uri)
@uri = target_base_uri.merge(URI.encode(wp_plugins_dir + '/' + name + '/'))
end
def db_file
@db_file ||= PLUGINS_FILE
end
end
# encoding: UTF-8
class WpPlugin < WpItem
# Sets the @uri
#
# @param [ URI ] target_base_uri The URI of the wordpress blog
#
# @return [ void ]
def forge_uri(target_base_uri)
@uri = target_base_uri.merge("#{wp_plugins_dir}/#{url_encode(name)}/")
end
def db_file
@db_file ||= PLUGINS_FILE
end
end

View File

@@ -1,37 +1,37 @@
# encoding: UTF-8
require 'wp_theme/findable'
require 'wp_theme/versionable'
require 'wp_theme/info'
require 'wp_theme/output'
require 'wp_theme/childtheme'
class WpTheme < WpItem
extend WpTheme::Findable
include WpTheme::Versionable
include WpTheme::Info
include WpTheme::Output
include WpTheme::Childtheme
attr_accessor :referenced_url
def allowed_options; super << :referenced_url end
# Sets the @uri
#
# @param [ URI ] target_base_uri The URI of the wordpress blog
#
# @return [ void ]
def forge_uri(target_base_uri)
@uri = target_base_uri.merge(URI.encode(wp_content_dir + '/themes/' + name + '/'))
end
# @return [ String ] The url to the theme stylesheet
def style_url
@uri.merge('style.css').to_s
end
def db_file
@db_file ||= THEMES_FILE
end
end
# encoding: UTF-8
require 'wp_theme/findable'
require 'wp_theme/versionable'
require 'wp_theme/info'
require 'wp_theme/output'
require 'wp_theme/childtheme'
class WpTheme < WpItem
extend WpTheme::Findable
include WpTheme::Versionable
include WpTheme::Info
include WpTheme::Output
include WpTheme::Childtheme
attr_accessor :referenced_url
def allowed_options; super << :referenced_url end
# Sets the @uri
#
# @param [ URI ] target_base_uri The URI of the wordpress blog
#
# @return [ void ]
def forge_uri(target_base_uri)
@uri = target_base_uri.merge("#{wp_content_dir}/themes/#{url_encode(name)}/")
end
# @return [ String ] The url to the theme stylesheet
def style_url
@uri.merge('style.css').to_s
end
def db_file
@db_file ||= THEMES_FILE
end
end

View File

@@ -1,64 +1,64 @@
# encoding: UTF-8
class WpTheme < WpItem
module Findable
# Find the main theme of the blog
#
# @param [ URI ] target_uri
#
# @return [ WpTheme ]
def find(target_uri)
methods.grep(/^find_from_/).each do |method|
if wp_theme = self.send(method, target_uri)
wp_theme.found_from = method
return wp_theme
end
end
nil
end
protected
# Discover the wordpress theme by parsing the css link rel
#
# @param [ URI ] target_uri
#
# @return [ WpTheme ]
def find_from_css_link(target_uri)
response = Browser.get_and_follow_location(target_uri.to_s)
# https + domain is optional because of relative links
return unless response.body =~ %r{(?:https?://[^"']+/)?([^/\s]+)/themes/([^"'/]+)[^"']*/style.css}i
new(
target_uri,
name: Regexp.last_match[2],
referenced_url: Regexp.last_match[0],
wp_content_dir: Regexp.last_match[1]
)
end
# @param [ URI ] target_uri
#
# @return [ WpTheme ]
def find_from_wooframework(target_uri)
body = Browser.get(target_uri.to_s).body
regexp = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?" />\s+<meta name="generator" content="WooFramework\s?([^"]+)?" />}
if matches = regexp.match(body)
woo_theme_name = matches[1]
woo_theme_version = matches[2]
#woo_framework_version = matches[3] # Not used at this time
return new(
target_uri,
name: woo_theme_name,
version: woo_theme_version
)
end
end
end
end
# encoding: UTF-8
class WpTheme < WpItem
module Findable
# Find the main theme of the blog
#
# @param [ URI ] target_uri
#
# @return [ WpTheme ]
def find(target_uri)
methods.grep(/^find_from_/).each do |method|
if wp_theme = self.send(method, target_uri)
wp_theme.found_from = method
return wp_theme
end
end
nil
end
protected
# Discover the wordpress theme by parsing the css link rel
#
# @param [ URI ] target_uri
#
# @return [ WpTheme ]
def find_from_css_link(target_uri)
response = Browser.get_and_follow_location(target_uri.to_s)
# https + domain is optional because of relative links
return unless response.body =~ %r{(?:https?://[^"']+/)?([^/\s]+)/themes/([^"'/]+)[^"']*/style.css}i
new(
target_uri,
name: Regexp.last_match[2],
referenced_url: Regexp.last_match[0],
wp_content_dir: Regexp.last_match[1]
)
end
# @param [ URI ] target_uri
#
# @return [ WpTheme ]
def find_from_wooframework(target_uri)
body = Browser.get(target_uri.to_s).body
regexp = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?" />\s+<meta name="generator" content="WooFramework\s?([^"]+)?" />}
if matches = regexp.match(body)
woo_theme_name = matches[1]
woo_theme_version = matches[2]
#woo_framework_version = matches[3] # Not used at this time
return new(
target_uri,
name: woo_theme_name,
version: woo_theme_version
)
end
end
end
end

View File

@@ -6,13 +6,13 @@ class WpTheme
# @return [ Void ]
def additional_output(verbose = false)
parse_style
theme_desc = verbose ? @theme_description : truncate(@theme_description, 100)
puts " | Style URL: #{style_url}"
puts " | Referenced style.css: #{referenced_url}" if referenced_url && referenced_url != style_url
puts " | Theme Name: #{@theme_name}" if @theme_name
puts " | Theme URI: #{@theme_uri}" if @theme_uri
puts " | Description: #{theme_desc}"
puts " | Description: #{theme_desc}" if theme_desc
puts " | Author: #{@theme_author}" if @theme_author
puts " | Author URI: #{@theme_author_uri}" if @theme_author_uri
puts " | Template: #{@theme_template}" if @theme_template and verbose

View File

@@ -1,9 +1,9 @@
# encoding: UTF-8
class WpTheme < WpItem
module Versionable
def version
@version ||= Browser.get(style_url).body[%r{Version:\s*(?!trunk)([0-9a-z\.-]+)}i, 1]
end
end
end
# encoding: UTF-8
class WpTheme < WpItem
module Versionable
def version
@version ||= Browser.get(style_url).body[%r{Version:\s*(?!trunk)([0-9a-z\.-]+)}i, 1]
end
end
end

View File

@@ -1,20 +1,20 @@
# encoding: UTF-8
require 'wp_timthumb/versionable'
require 'wp_timthumb/existable'
require 'wp_timthumb/output'
require 'wp_timthumb/vulnerable'
class WpTimthumb < WpItem
include WpTimthumb::Versionable
include WpTimthumb::Existable
include WpTimthumb::Output
include WpTimthumb::Vulnerable
# @param [ WpTimthumb ] other
#
# @return [ Boolean ]
def ==(other)
url == other.url
end
end
# encoding: UTF-8
require 'wp_timthumb/versionable'
require 'wp_timthumb/existable'
require 'wp_timthumb/output'
require 'wp_timthumb/vulnerable'
class WpTimthumb < WpItem
include WpTimthumb::Versionable
include WpTimthumb::Existable
include WpTimthumb::Output
include WpTimthumb::Vulnerable
# @param [ WpTimthumb ] other
#
# @return [ Boolean ]
def ==(other)
url == other.url
end
end

View File

@@ -1,24 +1,24 @@
# encoding: UTF-8
class WpTimthumb < WpItem
module Versionable
# Get the version from the body of an invalid request
# See https://code.google.com/p/timthumb/source/browse/trunk/timthumb.php#426
#
# @return [ String ] The version
def version
unless @version
response = Browser.get(url)
@version = response.body[%r{TimThumb version\s*: ([^<]+)} , 1]
end
@version
end
# @return [ String ]
def to_s
"#{url}#{ ' v' + version if version}"
end
end
end
# encoding: UTF-8
class WpTimthumb < WpItem
module Versionable
# Get the version from the body of an invalid request
# See https://code.google.com/p/timthumb/source/browse/trunk/timthumb.php#426
#
# @return [ String ] The version
def version
unless @version
response = Browser.get(url)
@version = response.body[%r{TimThumb version\s*: ([^<]+)} , 1]
end
@version
end
# @return [ String ]
def to_s
"#{url}#{ ' v' + version if version}"
end
end
end

View File

@@ -3,6 +3,8 @@
class WpUser < WpItem
module BruteForcable
attr_reader :progress_bar
# Brute force the user with the wordlist supplied
#
# It can take a long time to queue 2 million requests,
@@ -25,7 +27,8 @@ class WpUser < WpItem
hydra = browser.hydra
queue_count = 0
found = false
progress_bar = self.progress_bar(count_file_lines(wordlist)+1, options)
create_progress_bar(count_file_lines(wordlist)+1, options)
File.open(wordlist).each do |password|
password.chomp!
@@ -42,7 +45,7 @@ class WpUser < WpItem
request.on_complete do |response|
progress_bar.progress += 1 if options[:show_progression] && !found
puts "\n Trying Username : #{login} Password : #{password}" if options[:verbose]
progress_bar.log(" Trying Username: #{login} Password: #{password}") if options[:verbose]
if valid_password?(response, password, redirect_url, options)
found = true
@@ -57,7 +60,7 @@ class WpUser < WpItem
if queue_count >= browser.max_threads
hydra.run
queue_count = 0
puts "Sent #{browser.max_threads} requests ..." if options[:verbose]
progress_bar.log(" Sent #{browser.max_threads} request/s ...") if options[:verbose]
end
end
@@ -71,9 +74,9 @@ class WpUser < WpItem
#
# @return [ ProgressBar ]
# :nocov:
def progress_bar(passwords_size, options)
def create_progress_bar(passwords_size, options)
if options[:show_progression]
ProgressBar.create(
@progress_bar = ProgressBar.create(
format: '%t %a <%B> (%c / %C) %P%% %e',
title: " Brute Forcing '#{login}'",
total: passwords_size
@@ -107,20 +110,20 @@ class WpUser < WpItem
progression = "#{info('[SUCCESS]')} Login : #{login} Password : #{password}\n\n"
valid = true
elsif response.body =~ /login_error/i
verbose = "\n Incorrect login and/or password."
verbose = "Incorrect login and/or password."
elsif response.timed_out?
progression = critical('ERROR: Request timed out.')
elsif response.code == 0
progression = critical("ERROR: No response from remote server. WAF/IPS? (#{response.return_message})")
elsif response.code.to_s =~ /^50/
progression = critical('ERROR: Server error, try reducing the number of threads.')
progression = critical('ERROR: Server error, try reducing the number of threads or use the --throttle option.')
else
progression = critical("ERROR: We received an unknown response for #{password}...")
verbose = critical(" Code: #{response.code}\n Body: #{response.body}\n")
verbose = critical(" Code: #{response.code}\n Body: #{response.body}\n")
end
puts "\n " + progression if progression && options[:show_progression]
puts verbose if verbose && options[:verbose]
progress_bar.log(" #{progression}") if progression && options[:show_progression]
progress_bar.log(" #{verbose}") if verbose && options[:verbose]
valid || false
end

View File

@@ -1,86 +1,86 @@
# encoding: UTF-8
class WpUser < WpItem
module Existable
# @param [ Typhoeus::Response ] response
# @param [ Hash ] options
#
# @return [ Boolean ]
def exists_from_response?(response, options = {})
load_from_response(response)
@login ? true : false
end
# Load the login and display_name from the response
#
# @param [ Typhoeus::Response ] response
#
# @return [ void ]
def load_from_response(response)
if response.code == 301 # login in location?
location = response.headers_hash['Location']
return if location.nil? || location.empty?
@login = Existable.login_from_author_pattern(location)
@display_name = Existable.display_name_from_body(
Browser.get(location).body
)
elsif response.code == 200 # login in body?
@login = Existable.login_from_body(response.body)
@display_name = Existable.display_name_from_body(response.body)
end
end
private :load_from_response
# @param [ String ] text
#
# @return [ String ] The login
def self.login_from_author_pattern(text)
return unless text =~ %r{/author/([^/\b"']+)/?}i
Regexp.last_match[1].force_encoding('UTF-8')
end
# @param [ String ] body
#
# @return [ String ] The login
def self.login_from_body(body)
# Feed URL with Permalinks
login = WpUser::Existable.login_from_author_pattern(body)
unless login
# No Permalinks
login = body[%r{<body class="archive author author-([^\s]+)[ "]}i, 1]
login ? login.force_encoding('UTF-8') : nil
end
login
end
# @note Some bodies are encoded in ASCII-8BIT, and Nokogiri doesn't support it
# So it's forced to UTF-8 when this encoding is detected
#
# @param [ String ] body
#
# @return [ String ] The display_name
def self.display_name_from_body(body)
if title_tag = body[%r{<title>([^<]+)</title>}i, 1]
title_tag.force_encoding('UTF-8') if title_tag.encoding == Encoding::ASCII_8BIT
title_tag = Nokogiri::HTML::DocumentFragment.parse(title_tag).to_s
# &amp; are not decoded with Nokogiri
title_tag.gsub!('&amp;', '&')
# replace UTF chars like &#187; with dummy character
title_tag.gsub!(/&#(\d+);/, '|')
name = title_tag[%r{([^|«»]+) }, 1]
return name.strip if name
end
end
end
end
# encoding: UTF-8
class WpUser < WpItem
module Existable
# @param [ Typhoeus::Response ] response
# @param [ Hash ] options
#
# @return [ Boolean ]
def exists_from_response?(response, options = {})
load_from_response(response)
@login ? true : false
end
# Load the login and display_name from the response
#
# @param [ Typhoeus::Response ] response
#
# @return [ void ]
def load_from_response(response)
if response.code == 301 # login in location?
location = response.headers_hash['Location']
return if location.nil? || location.empty?
@login = Existable.login_from_author_pattern(location)
@display_name = Existable.display_name_from_body(
Browser.get(location).body
)
elsif response.code == 200 # login in body?
@login = Existable.login_from_body(response.body)
@display_name = Existable.display_name_from_body(response.body)
end
end
private :load_from_response
# @param [ String ] text
#
# @return [ String ] The login
def self.login_from_author_pattern(text)
return unless text =~ %r{/author/([^/\b"']+)/?}i
Regexp.last_match[1].force_encoding('UTF-8')
end
# @param [ String ] body
#
# @return [ String ] The login
def self.login_from_body(body)
# Feed URL with Permalinks
login = WpUser::Existable.login_from_author_pattern(body)
unless login
# No Permalinks
login = body[%r{<body class="archive author author-([^\s]+)[ "]}i, 1]
login ? login.force_encoding('UTF-8') : nil
end
login
end
# @note Some bodies are encoded in ASCII-8BIT, and Nokogiri doesn't support it
# So it's forced to UTF-8 when this encoding is detected
#
# @param [ String ] body
#
# @return [ String ] The display_name
def self.display_name_from_body(body)
if title_tag = body[%r{<title>([^<]+)</title>}i, 1]
title_tag.force_encoding('UTF-8') if title_tag.encoding == Encoding::ASCII_8BIT
title_tag = Nokogiri::HTML::DocumentFragment.parse(title_tag).to_s
# &amp; are not decoded with Nokogiri
title_tag.gsub!('&amp;', '&')
# replace UTF chars like &#187; with dummy character
title_tag.gsub!(/&#(\d+);/, '|')
name = title_tag[%r{([^|«»]+) }, 1]
return name.strip if name
end
end
end
end

View File

@@ -1,38 +1,48 @@
# encoding: UTF-8
require 'wp_version/findable'
require 'wp_version/output'
class WpVersion < WpItem
extend WpVersion::Findable
include WpVersion::Output
# The version number
attr_accessor :number
alias_method :version, :number # Needed to have the right behaviour in Vulnerable#vulnerable_to?
# @return [ Array ]
def allowed_options; super << :number << :found_from end
def identifier
@identifier ||= number
end
def db_file
@db_file ||= WORDPRESSES_FILE
end
# @param [ WpVersion ] other
#
# @return [ Boolean ]
def ==(other)
number == other.number
end
# @return [ Array<String> ] All the stable versions from version_file
def self.all(versions_file = WP_VERSIONS_FILE)
Nokogiri.XML(File.open(versions_file)).css('version').reduce([]) do |a, node|
a << node.text.to_s
end
end
end
# encoding: UTF-8
require 'wp_version/findable'
require 'wp_version/output'
class WpVersion < WpItem
extend WpVersion::Findable
include WpVersion::Output
# The version number
attr_accessor :number, :metadata
alias_method :version, :number # Needed to have the right behaviour in Vulnerable#vulnerable_to?
# @return [ Array ]
def allowed_options; super << :number << :found_from end
def identifier
@identifier ||= number
end
def db_file
@db_file ||= WORDPRESSES_FILE
end
# @param [ WpVersion ] other
#
# @return [ Boolean ]
def ==(other)
number == other.number
end
# @return [ Array<String> ] All the stable versions from version_file
def self.all(versions_file = WP_VERSIONS_FILE)
Nokogiri.XML(File.open(versions_file)).css('version').reduce([]) do |a, node|
a << node.text.to_s
end
end
# @return [ Hash ] Metadata for specific WP version from WORDPRESSES_FILE
def metadata(version)
json = json(db_file)
metadata = {}
metadata[:release_date] = json[version]['release_date']
metadata[:changelog_url] = json[version]['changelog_url']
metadata
end
end

View File

@@ -114,34 +114,6 @@ class WpVersion < WpItem
)
end
def find_from_stylesheets_numbers(target_uri)
wp_versions = WpVersion.all
found = {}
pattern = /\bver=([0-9\.]+)/i
Nokogiri::HTML(Browser.get(target_uri.to_s).body).css('link,script').each do |tag|
%w(href src).each do |attribute|
attr_value = tag.attribute(attribute).to_s
next if attr_value.nil? || attr_value.empty?
uri = Addressable::URI.parse(attr_value)
next unless uri.query && uri.query.match(pattern)
version = Regexp.last_match[1].to_s
found[version] ||= 0
found[version] += 1
end
end
found.delete_if { |v, _| !wp_versions.include?(v) }
best_guess = found.sort_by(&:last).last
# best_guess[0]: version number, [1] numbers of occurences
best_guess && best_guess[1] > 1 ? best_guess[0] : nil
end
# Uses data/wp_versions.xml to try to identify a
# wordpress version.
#
@@ -158,8 +130,6 @@ class WpVersion < WpItem
def find_from_advanced_fingerprinting(target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
xml = xml(versions_xml)
# This wp_item will take care of encoding the path
# and replace variables like $wp-content$ & $wp-plugins$
wp_item = WpItem.new(target_uri,
wp_content_dir: wp_content_dir,
wp_plugins_dir: wp_plugins_dir)
@@ -218,5 +188,32 @@ class WpVersion < WpItem
)
end
def find_from_stylesheets_numbers(target_uri)
wp_versions = WpVersion.all
found = {}
pattern = /\bver=([0-9\.]+)/i
Nokogiri::HTML(Browser.get(target_uri.to_s).body).css('link,script').each do |tag|
%w(href src).each do |attribute|
attr_value = tag.attribute(attribute).to_s
next if attr_value.nil? || attr_value.empty?
uri = Addressable::URI.parse(attr_value)
next unless uri.query && uri.query.match(pattern)
version = Regexp.last_match[1].to_s
found[version] ||= 0
found[version] += 1
end
end
found.delete_if { |v, _| !wp_versions.include?(v) }
best_guess = found.sort_by(&:last).last
# best_guess[0]: version number, [1] numbers of occurences
best_guess && best_guess[1] > 1 ? best_guess[0] : nil
end
end
end

View File

@@ -4,8 +4,16 @@ class WpVersion < WpItem
module Output
def output(verbose = false)
metadata = self.metadata(self.number)
puts
puts info("WordPress version #{self.number} identified from #{self.found_from}")
if verbose
puts info("WordPress version #{self.number} identified from #{self.found_from}")
puts " | Released: #{metadata[:release_date]}"
puts " | Changelog: #{metadata[:changelog_url]}"
else
puts info("WordPress version #{self.number} identified from #{self.found_from} #{"(Released on #{metadata[:release_date]})" if metadata[:release_date]}")
end
vulnerabilities = self.vulnerabilities

View File

@@ -3,8 +3,9 @@
require 'rubygems'
version = RUBY_VERSION.dup
if Gem::Version.create(version) < Gem::Version.create(1.9)
puts "Ruby >= 1.9 required to run wpscan (You have #{version})"
if Gem::Version.create(version) < Gem::Version.create(MIN_RUBY_VERSION)
puts "Ruby >= #{MIN_RUBY_VERSION} required to run wpscan (You have #{version})"
exit(1)
end
@@ -29,6 +30,7 @@ begin
require 'shellwords'
require 'fileutils'
require 'pathname'
require 'cgi'
# Third party libs
require 'typhoeus'
require 'yajl/json_gem'

View File

@@ -135,6 +135,11 @@ class WpTarget < WebSite
@uri.merge("#{wp_content_dir}/uploads/").to_s
end
# @return [ String ]
def includes_dir_url
@uri.merge("wp-includes/").to_s
end
# Script for replacing strings in wordpress databases
# reveals database credentials after hitting submit
# http://interconnectit.com/124/search-and-replace-for-wordpress-databases/
@@ -153,4 +158,8 @@ class WpTarget < WebSite
def upload_directory_listing_enabled?
directory_listing_enabled?(upload_dir_url)
end
def include_directory_listing_enabled?
directory_listing_enabled?(includes_dir_url)
end
end

View File

@@ -14,7 +14,7 @@ class WpTarget < WebSite
queue_count = 0
backups.each do |file|
file_url = @uri.merge(URI.escape(file)).to_s
file_url = @uri.merge(url_encode(file)).to_s
request = browser.forge_request(file_url)
request.on_complete do |response|

View File

@@ -62,7 +62,7 @@ def help
puts
puts 'Some values are settable in a config file, see the example.conf.json'
puts
puts '--update Update to the database to the latest version.'
puts '--update Update the database to the latest version.'
puts '--url | -u <target url> The WordPress URL/domain to scan.'
puts '--force | -f Forces WPScan to not check if the remote site is running WordPress.'
puts '--enumerate | -e [option(s)] Enumeration.'

View File

@@ -26,10 +26,10 @@ describe 'WpTimthumbs::Detectable' do
def expected_targets_from_theme(theme_name)
expected = []
%w{
%w(
timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
scripts/timthumb.php tools/timthumb.php functions/timthumb.php
}.each do |file|
).each do |file|
path = "$wp-content$/themes/#{theme_name}/#{file}"
expected << WpTimthumb.new(uri, path: path)
end
@@ -46,7 +46,7 @@ describe 'WpTimthumbs::Detectable' do
after do
targets = subject.send(:targets_items_from_file, file, wp_target)
expect(targets.map { |t| t.url }).to eq @expected.map { |t| t.url }
expect(targets.map(&:url)).to eq @expected.map(&:url)
end
context 'when an empty file' do
@@ -71,7 +71,7 @@ describe 'WpTimthumbs::Detectable' do
theme = 'hello-world'
targets = subject.send(:theme_timthumbs, theme, wp_target)
expect(targets.map { |t| t.url }).to eq expected_targets_from_theme(theme).map { |t| t.url }
expect(targets.map(&:url)).to eq expected_targets_from_theme(theme).map(&:url)
end
end
@@ -81,7 +81,7 @@ describe 'WpTimthumbs::Detectable' do
after do
targets = subject.send(:targets_items, wp_target, options)
expect(targets.map { |t| t.url }).to eq @expected.sort.map { |t| t.url }
expect(targets.map(&:url)).to match_array(@expected.map(&:url))
end
context 'when no :theme_name' do
@@ -101,7 +101,7 @@ describe 'WpTimthumbs::Detectable' do
end
context 'when :theme_name' do
let(:theme) { 'theme-name'}
let(:theme) { 'theme-name' }
context 'when no :file' do
let(:options) { { theme_name: theme } }

View File

@@ -105,11 +105,6 @@ describe WpItem do
@expected = 'plugins/readme.txt'
end
end
it 'also encodes chars' do
@path = 'some dir with spaces'
@expected = 'some%20dir%20with%20spaces'
end
end
describe '#uri' do

View File

@@ -10,7 +10,7 @@ shared_examples 'WpTarget::WpConfigBackup' do
# set all @config_backup_files to point to a 404
before :each do
config_backup_files.each do |backup_file|
file_url = wp_target.uri.merge(URI.escape(backup_file)).to_s
file_url = wp_target.uri.merge(url_encode(backup_file)).to_s
stub_request(:get, file_url).to_return(status: 404)
end
@@ -24,7 +24,7 @@ shared_examples 'WpTarget::WpConfigBackup' do
expected = []
config_backup_files.sample(1).each do |backup_file|
file_url = wp_target.uri.merge(URI.escape(backup_file)).to_s
file_url = wp_target.uri.merge(url_encode(backup_file)).to_s
expected << file_url
stub_request_to_fixture(url: file_url, fixture: fixtures_dir + '/wp-config.php')
@@ -40,7 +40,7 @@ shared_examples 'WpTarget::WpConfigBackup' do
expected = []
config_backup_files.sample(2).each do |backup_file|
file_url = wp_target.uri.merge(URI.escape(backup_file)).to_s
file_url = wp_target.uri.merge(url_encode(backup_file)).to_s
expected << file_url
stub_request_to_fixture(url: file_url, fixture: fixtures_dir + '/wp-config.php')

View File

@@ -82,6 +82,10 @@ def main
wp_target = WpTarget.new(wpscan_options.url, wpscan_options.to_h)
if wp_target.wordpress_hosted?
raise 'We do not support scanning *.wordpress.com hosted blogs'
end
# Remote website up?
unless wp_target.online?
raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down."
@@ -152,15 +156,11 @@ def main
# Output runtime data
start_time = Time.now
start_memory = get_memory_usage
start_memory = get_memory_usage unless windows?
puts info("URL: #{wp_target.url}")
puts info("Started: #{start_time.asctime}")
puts
if wp_target.wordpress_hosted?
puts critical('We do not support scanning *.wordpress.com hosted blogs')
end
if wp_target.has_robots?
puts info("robots.txt available under: '#{wp_target.robots_url}'")
@@ -221,6 +221,10 @@ def main
puts warning("Upload directory has directory listing enabled: #{wp_target.upload_dir_url}")
end
if wp_target.include_directory_listing_enabled?
puts warning("Includes directory has directory listing enabled: #{wp_target.includes_dir_url}")
end
enum_options = {
show_progression: true,
exclude_content: wpscan_options.exclude_content_based
@@ -440,12 +444,12 @@ def main
stop_time = Time.now
elapsed = stop_time - start_time
used_memory = get_memory_usage - start_memory
used_memory = get_memory_usage - start_memory unless windows?
puts
puts info("Finished: #{stop_time.asctime}")
puts info("Requests Done: #{@total_requests_done}")
puts info("Memory used: #{used_memory.bytes_to_human}")
puts info("Memory used: #{used_memory.bytes_to_human}") unless windows?
puts info("Elapsed time: #{Time.at(elapsed).utc.strftime('%H:%M:%S')}")
rescue Interrupt