So long hombre
This commit is contained in:
@@ -1,204 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/typhoeus_cache'
|
||||
require 'common/browser/actions'
|
||||
require 'common/browser/options'
|
||||
|
||||
class Browser
|
||||
extend Browser::Actions
|
||||
include Browser::Options
|
||||
|
||||
OPTIONS = [
|
||||
:basic_auth,
|
||||
:cache_ttl,
|
||||
:max_threads,
|
||||
:user_agent,
|
||||
:proxy,
|
||||
:proxy_auth,
|
||||
:request_timeout,
|
||||
:connect_timeout,
|
||||
:cookie,
|
||||
:throttle,
|
||||
:disable_accept_header,
|
||||
:disable_referer,
|
||||
:disable_tls_checks
|
||||
]
|
||||
|
||||
@@instance = nil
|
||||
|
||||
attr_reader :hydra, :cache_dir
|
||||
|
||||
attr_accessor :referer, :cookie, :vhost
|
||||
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Browser ]
|
||||
def initialize(options = {})
|
||||
@cache_dir = options[:cache_dir] || CACHE_DIR + '/browser'
|
||||
|
||||
# sets browser defaults
|
||||
browser_defaults
|
||||
# load config file
|
||||
conf = options[:config_file]
|
||||
load_config(conf) if conf
|
||||
# overrides defaults with user supplied values (overwrite values from config)
|
||||
override_config(options)
|
||||
|
||||
unless @hydra
|
||||
@hydra = Typhoeus::Hydra.new(max_concurrency: self.max_threads)
|
||||
end
|
||||
|
||||
@cache = TyphoeusCache.new(@cache_dir)
|
||||
@cache.clean
|
||||
|
||||
Typhoeus::Config.cache = @cache
|
||||
end
|
||||
|
||||
private_class_method :new
|
||||
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Browser ]
|
||||
def self.instance(options = {})
|
||||
unless @@instance
|
||||
@@instance = new(options)
|
||||
end
|
||||
@@instance
|
||||
end
|
||||
|
||||
def self.reset
|
||||
@@instance = nil
|
||||
end
|
||||
|
||||
# Override for setting the User-Agent
|
||||
# @param [ String ] user_agent
|
||||
def user_agent=(user_agent)
|
||||
Typhoeus::Config.user_agent = user_agent
|
||||
end
|
||||
|
||||
#
|
||||
# sets browser default values
|
||||
#
|
||||
def browser_defaults
|
||||
Typhoeus::Config.user_agent = "WPScan v#{WPSCAN_VERSION} (http://wpscan.org)"
|
||||
@max_threads = 20
|
||||
# 10 minutes, at this time the cache is cleaned before each scan.
|
||||
# If this value is set to 0, the cache will be disabled
|
||||
@cache_ttl = 600
|
||||
@request_timeout = 60 # 60s
|
||||
@connect_timeout = 10 # 10s
|
||||
@throttle = 0
|
||||
end
|
||||
|
||||
#
|
||||
# If an option was set but is not in the new config_file
|
||||
# it's value is kept
|
||||
#
|
||||
# @param [ String ] config_file
|
||||
#
|
||||
# @return [ void ]
|
||||
def load_config(config_file = nil)
|
||||
if File.symlink?(config_file)
|
||||
raise '[ERROR] Config file is a symlink.'
|
||||
else
|
||||
data = JSON.parse(File.read(config_file))
|
||||
end
|
||||
|
||||
OPTIONS.each do |option|
|
||||
option_name = option.to_s
|
||||
unless data[option_name].nil?
|
||||
self.send(:"#{option_name}=", data[option_name])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Request ]
|
||||
def forge_request(url, params = {})
|
||||
Typhoeus::Request.new(url, merge_request_params(params))
|
||||
end
|
||||
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def merge_request_params(params = {})
|
||||
if @proxy
|
||||
params.merge!(proxy: @proxy)
|
||||
params.merge!(proxyuserpwd: @proxy_auth) if @proxy_auth
|
||||
end
|
||||
|
||||
if @basic_auth
|
||||
params = Browser.append_params_header_field(
|
||||
params,
|
||||
'Authorization',
|
||||
@basic_auth
|
||||
)
|
||||
end
|
||||
|
||||
if vhost
|
||||
params = Browser.append_params_header_field(
|
||||
params,
|
||||
'Host',
|
||||
vhost
|
||||
)
|
||||
end
|
||||
|
||||
params.merge!(referer: referer)
|
||||
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)
|
||||
|
||||
# Prevent infinite self redirection
|
||||
params.merge!(maxredirs: 3) unless params.key?(:maxredirs)
|
||||
|
||||
# Disable SSL-Certificate checks
|
||||
if @disable_tls_checks
|
||||
# Cert validity check
|
||||
params.merge!(ssl_verifypeer: 0) unless params.key?(:ssl_verifypeer)
|
||||
# Cert hostname check
|
||||
params.merge!(ssl_verifyhost: 0) unless params.key?(:ssl_verifyhost)
|
||||
end
|
||||
|
||||
params.merge!(cookiejar: @cache_dir + '/cookie-jar')
|
||||
params.merge!(cookiefile: @cache_dir + '/cookie-jar')
|
||||
params.merge!(cookie: @cookie) if @cookie
|
||||
params = Browser.remove_params_header_field(params, 'Accept') if @disable_accept_header
|
||||
params = Browser.remove_params_header_field(params, 'Referer') if @disable_referer
|
||||
|
||||
params
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# @param [ Hash ] params
|
||||
# @param [ String ] field
|
||||
# @param [ Mixed ] field_value
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.append_params_header_field(params = {}, field, field_value)
|
||||
if !params.has_key?(:headers)
|
||||
params = params.merge(:headers => { field => field_value })
|
||||
elsif !params[:headers].has_key?(field)
|
||||
params[:headers][field] = field_value
|
||||
end
|
||||
params
|
||||
end
|
||||
|
||||
# @param [ Hash ] params
|
||||
# @param [ String ] field
|
||||
# @param [ Mixed ] field_value
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.remove_params_header_field(params = {}, field)
|
||||
if !params.has_key?(:headers)
|
||||
params = params.merge(:headers => { field => nil })
|
||||
elsif !params[:headers].has_key?(field)
|
||||
params[:headers][field] = nil
|
||||
end
|
||||
params
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Browser
|
||||
module Actions
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def get(url, params = {})
|
||||
process(url, params.merge(method: :get))
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def post(url, params = {})
|
||||
process(url, params.merge(method: :post))
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def head(url, params = {})
|
||||
process(url, params.merge(method: :head))
|
||||
end
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def get_and_follow_location(url, params = {})
|
||||
params[:maxredirs] ||= 2
|
||||
|
||||
get(url, params.merge(followlocation: true))
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ String ] url
|
||||
# @param [ Hash ] params
|
||||
#
|
||||
# @return [ Typhoeus::Response ]
|
||||
def process(url, params)
|
||||
Typhoeus::Request.new(url, Browser.instance.merge_request_params(params)).run
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Browser
|
||||
module Options
|
||||
|
||||
attr_accessor :request_timeout, :connect_timeout, :user_agent, :disable_accept_header, :disable_referer, :disable_tls_checks
|
||||
attr_reader :basic_auth, :cache_ttl, :proxy, :proxy_auth, :throttle
|
||||
|
||||
# Sets the Basic Authentification credentials
|
||||
# Accepted format:
|
||||
# login:password
|
||||
# Basic base_64_encoded
|
||||
#
|
||||
# @param [ String ] auth
|
||||
#
|
||||
# @return [ void ]
|
||||
def basic_auth=(auth)
|
||||
if auth.index(':')
|
||||
@basic_auth = "Basic #{Base64.encode64(auth).chomp}"
|
||||
elsif auth =~ /\ABasic [a-zA-Z0-9=]+\z/
|
||||
@basic_auth = auth
|
||||
else
|
||||
raise "Invalid basic authentication format, \"login:password\" or \"Basic base_64_encoded\" expected. Your input: #{auth}"
|
||||
end
|
||||
end
|
||||
|
||||
def cache_ttl=(ttl)
|
||||
@cache_ttl = ttl.to_i
|
||||
end
|
||||
|
||||
# @return [ Integer ]
|
||||
def max_threads
|
||||
@max_threads || 1
|
||||
end
|
||||
|
||||
def max_threads=(threads)
|
||||
if threads.is_a?(Integer) && threads > 0
|
||||
@max_threads = threads
|
||||
@hydra = Typhoeus::Hydra.new(max_concurrency: threads)
|
||||
else
|
||||
raise 'max_threads must be an Integer > 0'
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the proxy
|
||||
# Accepted format:
|
||||
# [protocol://]host:post
|
||||
#
|
||||
# Supported protocols:
|
||||
# Depends on the curl protocols, See curl --version
|
||||
#
|
||||
# @param [ String ] proxy
|
||||
#
|
||||
# @return [ void ]
|
||||
def proxy=(proxy)
|
||||
if proxy.index(':')
|
||||
@proxy = proxy
|
||||
else
|
||||
raise 'Invalid proxy format. Should be [protocol://]host:port.'
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the proxy credentials
|
||||
# Accepted format:
|
||||
# username:password
|
||||
# { proxy_username: username, :proxy_password: password }
|
||||
#
|
||||
# @param [ String ] auth
|
||||
#
|
||||
# @return [ void ]
|
||||
def proxy_auth=(auth)
|
||||
unless auth.nil?
|
||||
if auth.is_a?(Hash) && auth.include?(:proxy_username) && auth.include?(:proxy_password)
|
||||
@proxy_auth = auth[:proxy_username] + ':' + auth[:proxy_password]
|
||||
elsif auth.is_a?(String) && auth.index(':') != nil
|
||||
@proxy_auth = auth
|
||||
else
|
||||
raise invalid_proxy_auth_format
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the request timeout
|
||||
# @param [ Integer ] timeout Timeout in ms
|
||||
#
|
||||
# @return [ void ]
|
||||
def request_timeout=(timeout)
|
||||
@request_timeout = timeout.to_i
|
||||
end
|
||||
|
||||
# Sets the connect timeout
|
||||
# @param [ Integer ] timeout Timeout in ms
|
||||
#
|
||||
# @return [ void ]
|
||||
def connect_timeout=(timeout)
|
||||
@connect_timeout = timeout.to_i
|
||||
end
|
||||
|
||||
# @param [ String, Integer ] throttle
|
||||
def throttle=(throttle)
|
||||
@throttle = throttle.to_i.abs / 1000.0
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def invalid_proxy_auth_format
|
||||
'Invalid proxy auth format, expected username:password or {proxy_username: username, proxy_password: password}'
|
||||
end
|
||||
|
||||
# Override with the options if they are set
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ void ]
|
||||
def override_config(options = {})
|
||||
options.each do |option, value|
|
||||
if value != nil and OPTIONS.include?(option)
|
||||
self.send(:"#{option}=", value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,78 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
#
|
||||
# => @todo take consideration of the cache_timeout :
|
||||
# -> create 2 files per key : one for the data storage (key.store ?)
|
||||
# and the other for the cache timeout (key.expiration, key.timeout ?)
|
||||
# or 1 file for all timeouts ?
|
||||
# -> 2 dirs : 1 for storage, the other for cache_timeout ?
|
||||
#
|
||||
|
||||
require 'yaml'
|
||||
require 'fileutils'
|
||||
|
||||
class CacheFileStore
|
||||
attr_reader :storage_path, :cache_dir, :serializer
|
||||
|
||||
# The serializer must have the 2 methods .load and .dump
|
||||
# (Marshal and YAML have them)
|
||||
# YAML is Human Readable, contrary to Marshal which store in a binary format
|
||||
# Marshal does not need any "require"
|
||||
def initialize(storage_path, serializer = Marshal)
|
||||
@cache_dir = File.expand_path(storage_path)
|
||||
@storage_path = File.expand_path(File.join(storage_path, storage_dir))
|
||||
@serializer = serializer
|
||||
|
||||
unless Dir.exist?(@storage_path)
|
||||
FileUtils.mkdir_p(@storage_path)
|
||||
end
|
||||
|
||||
unless Pathname.new(@storage_path).writable?
|
||||
fail "#{@storage_path} is not writable"
|
||||
end
|
||||
end
|
||||
|
||||
def clean
|
||||
# clean old directories
|
||||
Dir[File.join(@cache_dir, '*')].each do |f|
|
||||
if File.directory?(f)
|
||||
# delete directory if create time is older than 4 hours
|
||||
FileUtils.rm_rf(f) if File.mtime(f) < (Time.now - (60*240))
|
||||
else
|
||||
File.delete(f) unless File.symlink?(f)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def read_entry(key)
|
||||
begin
|
||||
@serializer.load(File.read(get_entry_file_path(key)))
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def write_entry(key, data_to_store, cache_ttl)
|
||||
if cache_ttl && cache_ttl > 0
|
||||
File.open(get_entry_file_path(key), 'w') do |f|
|
||||
begin
|
||||
f.write(@serializer.dump(data_to_store))
|
||||
rescue
|
||||
nil # spec fix for "can't dump hash with default proc" when stub_request with response headers
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_entry_file_path(key)
|
||||
File::join(@storage_path, key)
|
||||
end
|
||||
|
||||
def storage_dir
|
||||
time = Time.now
|
||||
random = (0...8).map { (65 + rand(26)).chr }.join
|
||||
|
||||
Digest::MD5.hexdigest("#{time}#{random}")
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/vulnerabilities/output'
|
||||
|
||||
class Vulnerabilities < Array
|
||||
include Vulnerabilities::Output
|
||||
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Vulnerabilities < Array
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
self.each do |v|
|
||||
v.output(verbose)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_items/detectable'
|
||||
require 'common/collections/wp_items/output'
|
||||
|
||||
class WpItems < Array
|
||||
extend WpItems::Detectable
|
||||
include WpItems::Output
|
||||
|
||||
attr_accessor :wp_target
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
def initialize(wp_target = nil)
|
||||
self.wp_target = wp_target
|
||||
end
|
||||
|
||||
# @param [String] args
|
||||
#
|
||||
# @return [ void ]
|
||||
def add(*args)
|
||||
index = 0
|
||||
|
||||
until args[index].nil?
|
||||
arg = args[index]
|
||||
|
||||
if arg.is_a?(String)
|
||||
if (next_arg = args[index + 1]).is_a?(Hash)
|
||||
item = create_item(arg, next_arg)
|
||||
index += 1
|
||||
else
|
||||
item = create_item(arg)
|
||||
end
|
||||
elsif arg.is_a?(Item)
|
||||
item = arg
|
||||
else
|
||||
raise 'Invalid arguments'
|
||||
end
|
||||
|
||||
self << item
|
||||
index += 1
|
||||
end
|
||||
end
|
||||
|
||||
# @param [ String ] name
|
||||
# @param [ Hash ] attrs
|
||||
#
|
||||
# @return [ WpItem ]
|
||||
def create_item(name, attrs = {})
|
||||
raise 'wp_target must be set' unless wp_target
|
||||
|
||||
item_class.new(
|
||||
wp_target.uri,
|
||||
attrs.merge(
|
||||
name: name,
|
||||
wp_content_dir: wp_target.wp_content_dir,
|
||||
wp_plugins_dir: wp_target.wp_plugins_dir
|
||||
) { |key, oldval, newval| oldval }
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ WpItems ] other
|
||||
#
|
||||
# @return [ self ]
|
||||
def +(other)
|
||||
other.each { |item| self << item }
|
||||
self
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @return [ Class ]
|
||||
def item_class
|
||||
Object.const_get(self.class.to_s.gsub(/.$/, ''))
|
||||
end
|
||||
end
|
||||
@@ -1,240 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItems < Array
|
||||
module Detectable
|
||||
|
||||
attr_reader :vulns_file, :item_xpath
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Boolean ] :show_progression Whether or not output the progress bar
|
||||
# @option options [ Boolean ] :only_vulnerable Only check for vulnerable items
|
||||
# @option options [ String ] :exclude_content
|
||||
#
|
||||
# @return [ WpItems ]
|
||||
def aggressive_detection(wp_target, options = {})
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
targets = targets_items(wp_target, options)
|
||||
progress_bar = progress_bar(targets.size, options)
|
||||
queue_count = 0
|
||||
exist_options = {
|
||||
error_404_hash: wp_target.error_404_hash,
|
||||
homepage_hash: wp_target.homepage_hash,
|
||||
exclude_content: options[:exclude_content] ? %r{#{options[:exclude_content]}} : nil
|
||||
}
|
||||
results = passive_detection(wp_target, options)
|
||||
|
||||
targets.each do |target_item|
|
||||
request = browser.forge_request(target_item.url, request_params)
|
||||
|
||||
request.on_complete do |response|
|
||||
progress_bar.progress += 1 if options[:show_progression]
|
||||
|
||||
if target_item.exists?(exist_options, response)
|
||||
results << target_item unless results.include?(target_item)
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count >= browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
puts "Sent #{browser.max_threads} requests ..." if options[:verbose]
|
||||
end
|
||||
end
|
||||
|
||||
# run the remaining requests
|
||||
hydra.run
|
||||
|
||||
results.select!(&:vulnerable?) if options[:type] == :vulnerable
|
||||
results.sort!
|
||||
|
||||
results # can't just return results.sort as it would return an array, and we want a WpItems
|
||||
end
|
||||
|
||||
# @param [ Integer ] targets_size
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ ProgressBar ]
|
||||
# :nocov:
|
||||
def progress_bar(targets_size, options)
|
||||
if options[:show_progression]
|
||||
ProgressBar.create(
|
||||
format: '%t %a <%B> (%c / %C) %P%% %e',
|
||||
title: ' ', # Used to craete a left margin
|
||||
total: targets_size
|
||||
)
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ WpItems ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
results = new(wp_target)
|
||||
# improves speed
|
||||
body = remove_base64_images_from_html(Browser.get(wp_target.url).body)
|
||||
page = Nokogiri::HTML(body)
|
||||
names = []
|
||||
|
||||
page.css('link,script,style').each do |tag|
|
||||
%w(href src).each do |attribute|
|
||||
attr_value = tag.attribute(attribute).to_s
|
||||
next unless attr_value
|
||||
|
||||
names << Regexp.last_match[1] if attr_value.match(attribute_pattern(wp_target))
|
||||
end
|
||||
|
||||
next unless tag.name == 'script' || tag.name == 'style'
|
||||
|
||||
code = tag.text.to_s
|
||||
next if code.empty?
|
||||
|
||||
if !code.valid_encoding?
|
||||
code = code.encode('UTF-16be', :invalid => :replace, :replace => '?').encode('UTF-8')
|
||||
end
|
||||
|
||||
code.scan(code_pattern(wp_target)).flatten.uniq.each do |item_name|
|
||||
names << item_name
|
||||
end
|
||||
end
|
||||
|
||||
names.uniq.each { |name| results.add(name) }
|
||||
|
||||
results.sort!
|
||||
results
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Regex ]
|
||||
def item_pattern(wp_target)
|
||||
type = to_s.gsub(/Wp/, '').downcase
|
||||
wp_content_dir = wp_target.wp_content_dir
|
||||
wp_content_url = wp_target.uri.merge(wp_content_dir).to_s
|
||||
|
||||
url = wp_content_url.gsub(%r{\A(?:http|https)://}, '(?:https?:)?//').gsub('/', '\\\\\?\/')
|
||||
content_dir = %r{(?:#{url}|\\?\/\\?\/?#{wp_content_dir})}i
|
||||
|
||||
%r{#{content_dir}\\?/#{type}\\?/}
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Regex ]
|
||||
def attribute_pattern(wp_target)
|
||||
/\A#{item_pattern(wp_target)}([^\/]+)/i
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Regex ]
|
||||
def code_pattern(wp_target)
|
||||
/["'\(]#{item_pattern(wp_target)}([^\\\/\)"']+)/i
|
||||
end
|
||||
|
||||
# The default request parameters
|
||||
#
|
||||
# @return [ Hash ]
|
||||
def request_params; { cache_ttl: 0, followlocation: true } end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ options ] options
|
||||
# @option options [ Boolean ] :only_vulnerable
|
||||
# @option options [ String ] :file The path to the file containing the targets
|
||||
#
|
||||
# @return [ Array<WpItem> ]
|
||||
def targets_items(wp_target, options = {})
|
||||
item_class = self.item_class
|
||||
vulns_file = self.vulns_file
|
||||
|
||||
targets = target_items_from_type(wp_target, item_class, vulns_file, options[:type])
|
||||
|
||||
targets.uniq! { |t| t.name }
|
||||
targets.sort_by { rand }
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Class ] item_class
|
||||
# @param [ String ] vulns_file
|
||||
#
|
||||
# @return [ Array<WpItem> ]
|
||||
def target_items_from_type(wp_target, item_class, vulns_file, type)
|
||||
targets = []
|
||||
json = json(vulns_file)
|
||||
|
||||
case type
|
||||
when :vulnerable
|
||||
items = json.select { |item| !json[item]['vulnerabilities'].empty? }.keys
|
||||
when :popular
|
||||
items = json.select { |item| json[item]['popular'] == true }.keys
|
||||
when :all
|
||||
items = json.keys
|
||||
else
|
||||
raise "Unknown type #{type}"
|
||||
end
|
||||
|
||||
items.each do |item|
|
||||
targets << create_item(
|
||||
item_class,
|
||||
item,
|
||||
wp_target,
|
||||
vulns_file
|
||||
)
|
||||
end
|
||||
|
||||
targets
|
||||
end
|
||||
|
||||
# @param [ Class ] klass
|
||||
# @param [ String ] name
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @option [ String ] vulns_file
|
||||
#
|
||||
# @return [ WpItem ]
|
||||
def create_item(klass, name, wp_target, vulns_file = nil)
|
||||
klass.new(
|
||||
wp_target.uri,
|
||||
name: name,
|
||||
vulns_file: vulns_file,
|
||||
wp_content_dir: wp_target.wp_content_dir,
|
||||
wp_plugins_dir: wp_target.wp_plugins_dir
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ String ] file
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Class ] item_class
|
||||
# @param [ String ] vulns_file
|
||||
#
|
||||
# @return [ Array<WpItem> ]
|
||||
def targets_items_from_file(file, wp_target, item_class, vulns_file)
|
||||
targets = []
|
||||
|
||||
File.open(file, 'r') do |f|
|
||||
f.readlines.collect do |item_name|
|
||||
targets << create_item(
|
||||
item_class,
|
||||
item_name.strip,
|
||||
wp_target,
|
||||
vulns_file
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
targets
|
||||
end
|
||||
|
||||
# @return [ Class ]
|
||||
def item_class
|
||||
Object.const_get(self.to_s.gsub(/.$/, ''))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItems < Array
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
self.each { |item| item.output(verbose) }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_plugins/detectable'
|
||||
|
||||
class WpPlugins < WpItems
|
||||
extend WpPlugins::Detectable
|
||||
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpPlugins < WpItems
|
||||
module Detectable
|
||||
# @return [ String ]
|
||||
def vulns_file
|
||||
PLUGINS_FILE
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ WpPlugins ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
detected = super(wp_target, options)
|
||||
|
||||
detected += from_header(wp_target)
|
||||
detected += from_content(wp_target)
|
||||
|
||||
detected.uniq! { |i| i.name }
|
||||
detected
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# X-Powered-By: W3 Total Cache/0.9.2.5
|
||||
# WP-Super-Cache: Served supercache file from PHP
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ WpPlugins ]
|
||||
def from_header(wp_target)
|
||||
headers = Browser.get(wp_target.url).headers
|
||||
wp_plugins = WpPlugins.new(wp_target)
|
||||
|
||||
if headers
|
||||
powered_by = headers['X-Powered-By'].to_s
|
||||
wp_super_cache = headers['wp-super-cache'].to_s
|
||||
|
||||
if matches = /W3 Total Cache\/([0-9.]+)/i.match(powered_by)
|
||||
wp_plugins.add('w3-total-cache', version: matches[1])
|
||||
end
|
||||
|
||||
wp_plugins.add('wp-super-cache') if wp_super_cache =~ /supercache/i
|
||||
end
|
||||
|
||||
wp_plugins
|
||||
end
|
||||
|
||||
# <!-- Cached page generated by WP-Super-Cache on 2013-05-03 14:46:37 -->
|
||||
# <!-- Performance optimized by W3 Total Cache.
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ WpPlugins ]
|
||||
def from_content(wp_target)
|
||||
body = Browser.get(wp_target.url).body
|
||||
wp_plugins = WpPlugins.new(wp_target)
|
||||
|
||||
wp_plugins.add('wp-super-cache') if body =~ /wp-super-cache/i
|
||||
wp_plugins.add('w3-total-cache') if body =~ /w3 total cache/i
|
||||
|
||||
if body =~ /<!-- all in one seo pack ([^\s]+)/i
|
||||
wp_plugins.add('all-in-one-seo-pack', version: $1)
|
||||
end
|
||||
|
||||
if body =~ /<!-- This site is optimized with the Yoast (?:WordPress )?SEO plugin v([^\s]+) -/i
|
||||
wp_plugins.add('wordpress-seo', version: $1)
|
||||
end
|
||||
|
||||
if body =~ /<!-- Google Universal Analytics for WordPress v([^\s]+) -/i
|
||||
wp_plugins.add('google-universal-analytics', version: $1)
|
||||
end
|
||||
|
||||
wp_plugins
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_themes/detectable'
|
||||
|
||||
class WpThemes < WpItems
|
||||
extend WpThemes::Detectable
|
||||
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpThemes < WpItems
|
||||
module Detectable
|
||||
|
||||
# @return [ String ]
|
||||
def vulns_file
|
||||
THEMES_FILE
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/collections/wp_timthumbs/detectable'
|
||||
|
||||
class WpTimthumbs < WpItems
|
||||
extend WpTimthumbs::Detectable
|
||||
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumbs < WpItems
|
||||
module Detectable
|
||||
|
||||
# No passive detection
|
||||
#
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ WpTimthumbs ]
|
||||
def passive_detection(wp_target, options = {})
|
||||
new
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @param [ Hash ] options
|
||||
# @option options [ String ] :file The path to the file containing the targets
|
||||
# @option options [ String ] :theme_name
|
||||
#
|
||||
# @return [ Array<WpTimthumb> ]
|
||||
def targets_items(wp_target, options = {})
|
||||
targets = options[:theme_name] ? theme_timthumbs(options[:theme_name], wp_target) : []
|
||||
|
||||
if options[:file]
|
||||
targets += targets_items_from_file(options[:file], wp_target)
|
||||
end
|
||||
|
||||
targets.uniq { |i| i.url }
|
||||
end
|
||||
|
||||
# @param [ String ] theme_name
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Array<WpTimthumb> ]
|
||||
def theme_timthumbs(theme_name, wp_target)
|
||||
targets = []
|
||||
wp_timthumb = create_item(wp_target)
|
||||
|
||||
%w{
|
||||
timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
|
||||
scripts/timthumb.php tools/timthumb.php functions/timthumb.php thumb.php
|
||||
}.each do |path|
|
||||
wp_timthumb.path = "$wp-content$/themes/#{theme_name}/#{path}"
|
||||
|
||||
targets << wp_timthumb.dup
|
||||
end
|
||||
targets
|
||||
end
|
||||
|
||||
# @param [ String ] file
|
||||
# @param [ WpTarget ] wp_target
|
||||
#
|
||||
# @return [ Array<WpTimthumb> ]
|
||||
def targets_items_from_file(file, wp_target)
|
||||
targets = []
|
||||
|
||||
File.open(file, 'r') do |f|
|
||||
f.readlines.collect do |path|
|
||||
targets << create_item(wp_target, path.strip)
|
||||
end
|
||||
end
|
||||
targets
|
||||
end
|
||||
|
||||
# @param [ WpTarget ] wp_target
|
||||
# @option [ String ] path
|
||||
#
|
||||
# @return [ WpTimthumb ]
|
||||
def create_item(wp_target, path = nil)
|
||||
options = {
|
||||
wp_content_dir: wp_target.wp_content_dir,
|
||||
wp_plugins_dir: wp_target.wp_plugins_dir
|
||||
}
|
||||
|
||||
options.merge!(path: path) if path
|
||||
|
||||
WpTimthumb.new(wp_target.uri, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
# 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
|
||||
@@ -1,17 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUsers < WpItems
|
||||
module BruteForcable
|
||||
|
||||
# Brute force each wp_user
|
||||
#
|
||||
# @param [ String ] wordlist The path to the wordlist
|
||||
# @param [ Hash ] options See WpUser::BruteForcable#brute_force
|
||||
#
|
||||
# @return [ void ]
|
||||
def brute_force(wordlist, options = {})
|
||||
self.each { |wp_user| wp_user.brute_force(wordlist, options) }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
# 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
|
||||
@@ -1,48 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpUsers < WpItems
|
||||
module Output
|
||||
|
||||
# @param [ Hash ] options
|
||||
# @option options[ Boolean ] :show_password Output the password column
|
||||
#
|
||||
# @return [ void ]
|
||||
def output(options = {})
|
||||
rows = []
|
||||
headings = ['ID', 'Login', 'Name']
|
||||
headings << 'Password' if options[:show_password]
|
||||
|
||||
remove_junk_from_display_names
|
||||
|
||||
self.each do |wp_user|
|
||||
row = [wp_user.id, wp_user.login, wp_user.display_name]
|
||||
row << wp_user.password if options[:show_password]
|
||||
rows << row
|
||||
end
|
||||
|
||||
table = Terminal::Table.new(headings: headings,
|
||||
rows: rows,
|
||||
style: { margin_left: options[:margin_left] || '' }).to_s
|
||||
# variable needed for output
|
||||
puts table
|
||||
end
|
||||
|
||||
def remove_junk_from_display_names
|
||||
display_names = []
|
||||
self.each do |u|
|
||||
display_name = u.display_name
|
||||
unless display_name == 'empty'
|
||||
display_names << display_name
|
||||
end
|
||||
end
|
||||
junk = get_equal_string_end(display_names)
|
||||
unless junk.nil? or junk.empty?
|
||||
self.each do |u|
|
||||
u.display_name ||= ''
|
||||
u.display_name = u.display_name.sub(/#{Regexp.escape(junk)}$/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,332 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
# Location directories
|
||||
LIB_DIR = File.expand_path(File.join(__dir__, '..')) # wpscan/lib/
|
||||
ROOT_DIR = File.expand_path(File.join(LIB_DIR, '..')) # wpscan/ - expand_path is used to get "wpscan/" instead of "wpscan/lib/../"
|
||||
USER_DIR = File.expand_path(Dir.home) # ~/
|
||||
|
||||
# Core WPScan directories
|
||||
CACHE_DIR = File.join(USER_DIR, '.wpscan/cache') # ~/.wpscan/cache/
|
||||
DATA_DIR = File.join(USER_DIR, '.wpscan/data') # ~/.wpscan/data/
|
||||
CONF_DIR = File.join(USER_DIR, '.wpscan/conf') # ~/.wpscan/conf/ - Not used ATM (only ref via ./spec/ for travis)
|
||||
COMMON_LIB_DIR = File.join(LIB_DIR, 'common') # wpscan/lib/common/
|
||||
WPSCAN_LIB_DIR = File.join(LIB_DIR, 'wpscan') # wpscan/lib/wpscan/
|
||||
MODELS_LIB_DIR = File.join(COMMON_LIB_DIR, 'models') # wpscan/lib/common/models/
|
||||
|
||||
# Core WPScan files
|
||||
DEFAULT_LOG_FILE = File.join(USER_DIR, '.wpscan/log.txt') # ~/.wpscan/log.txt
|
||||
DATA_FILE = File.join(ROOT_DIR, 'data.zip') # wpscan/data.zip
|
||||
|
||||
# WPScan Data files (data.zip)
|
||||
LAST_UPDATE_FILE = File.join(DATA_DIR, '.last_update') # ~/.wpscan/data/.last_update
|
||||
PLUGINS_FILE = File.join(DATA_DIR, 'plugins.json') # ~/.wpscan/data/plugins.json
|
||||
THEMES_FILE = File.join(DATA_DIR, 'themes.json') # ~/.wpscan/data/themes.json
|
||||
TIMTHUMBS_FILE = File.join(DATA_DIR, 'timthumbs.txt') # ~/.wpscan/data/timthumbs.txt
|
||||
USER_AGENTS_FILE = File.join(DATA_DIR, 'user-agents.txt') # ~/.wpscan/data/user-agents.txt
|
||||
WORDPRESSES_FILE = File.join(DATA_DIR, 'wordpresses.json') # ~/.wpscan/data/wordpresses.json
|
||||
WP_VERSIONS_FILE = File.join(DATA_DIR, 'wp_versions.xml') # ~/.wpscan/data/wp_versions.xml
|
||||
|
||||
MIN_RUBY_VERSION = '2.1.9'
|
||||
|
||||
WPSCAN_VERSION = '2.9.5-dev'
|
||||
|
||||
$LOAD_PATH.unshift(LIB_DIR)
|
||||
$LOAD_PATH.unshift(WPSCAN_LIB_DIR)
|
||||
$LOAD_PATH.unshift(MODELS_LIB_DIR)
|
||||
|
||||
def kali_linux?
|
||||
begin
|
||||
File.readlines('/etc/debian_version').grep(/^kali/i).any?
|
||||
rescue
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Determins if installed on Windows OS
|
||||
def windows?
|
||||
Gem.win_platform?
|
||||
end
|
||||
|
||||
require 'environment'
|
||||
require 'zip'
|
||||
|
||||
def escape_glob(s)
|
||||
s.gsub(/[\\\{\}\[\]\*\?]/) { |x| '\\' + x }
|
||||
end
|
||||
|
||||
# TODO : add an exclude pattern ?
|
||||
def require_files_from_directory(absolute_dir_path, files_pattern = '*.rb')
|
||||
files = Dir[File.join(escape_glob(absolute_dir_path), files_pattern)]
|
||||
|
||||
# Files in the root dir are loaded first, then those in the subdirectories
|
||||
files.sort_by { |file| [file.count('/'), file] }.each do |f|
|
||||
f = File.expand_path(f)
|
||||
# puts "require #{f}" # Used for debug
|
||||
require f
|
||||
end
|
||||
end
|
||||
|
||||
require_files_from_directory(COMMON_LIB_DIR, '**/*.rb')
|
||||
|
||||
# Add protocol
|
||||
def add_http_protocol(url)
|
||||
url =~ /^https?:/ ? url : "http://#{url}"
|
||||
end
|
||||
|
||||
def add_trailing_slash(url)
|
||||
url =~ /\/$/ ? url : "#{url}/"
|
||||
end
|
||||
|
||||
def missing_db_files?
|
||||
DbUpdater::FILES.each do |db_file|
|
||||
return true unless File.exist?(File.join(DATA_DIR, db_file))
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# Find data.zip?
|
||||
def has_db_zip?
|
||||
return File.exist?(DATA_FILE)
|
||||
end
|
||||
|
||||
# Extract data.zip
|
||||
def extract_db_zip
|
||||
# Create data folder
|
||||
FileUtils.mkdir_p(DATA_DIR)
|
||||
|
||||
Zip::File.open(DATA_FILE) do |zip_file|
|
||||
zip_file.each do |f|
|
||||
# Feedback to the user
|
||||
#puts "[+] Extracting: #{File.basename(f.name)}"
|
||||
f_path = File.join(DATA_DIR, File.basename(f.name))
|
||||
|
||||
# Delete if already there
|
||||
#puts "[+] Deleting: #{File.basename(f.name)}" if File.exist?(f_path)
|
||||
FileUtils.rm(f_path) if File.exist?(f_path)
|
||||
|
||||
# Extract
|
||||
zip_file.extract(f, f_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def last_update
|
||||
date = nil
|
||||
if File.exists?(LAST_UPDATE_FILE)
|
||||
content = File.read(LAST_UPDATE_FILE)
|
||||
date = Time.parse(content) rescue nil
|
||||
end
|
||||
date
|
||||
end
|
||||
|
||||
# Was it 5 days ago?
|
||||
def update_required?
|
||||
date = last_update
|
||||
day_seconds = 24 * 60 * 60
|
||||
five_days_ago = Time.now - (5 * day_seconds)
|
||||
(true if date.nil?) or (date < five_days_ago)
|
||||
end
|
||||
|
||||
# Define colors
|
||||
def colorize(text, color_code)
|
||||
if $COLORSWITCH
|
||||
"#{text}"
|
||||
else
|
||||
"\e[#{color_code}m#{text}\e[0m"
|
||||
end
|
||||
end
|
||||
|
||||
def bold(text)
|
||||
colorize(text, 1)
|
||||
end
|
||||
|
||||
def red(text)
|
||||
colorize(text, 31)
|
||||
end
|
||||
|
||||
def green(text)
|
||||
colorize(text, 32)
|
||||
end
|
||||
|
||||
def amber(text)
|
||||
colorize(text, 33)
|
||||
end
|
||||
|
||||
def blue(text)
|
||||
colorize(text, 34)
|
||||
end
|
||||
|
||||
def critical(text)
|
||||
$exit_code += 1 if defined?($exit_code) # hack for undefined var via rspec
|
||||
"#{red('[!]')} #{text}"
|
||||
end
|
||||
|
||||
def warning(text)
|
||||
$exit_code += 1 if defined?($exit_code) # hack for undefined var via rspec
|
||||
"#{amber('[!]')} #{text}"
|
||||
end
|
||||
|
||||
def info(text)
|
||||
"#{green('[+]')} #{text}"
|
||||
end
|
||||
|
||||
def notice(text)
|
||||
"#{blue('[i]')} #{text}"
|
||||
end
|
||||
|
||||
# our 1337 banner
|
||||
def banner
|
||||
puts '_______________________________________________________________'
|
||||
puts ' __ _______ _____ '
|
||||
puts ' \\ \\ / / __ \\ / ____| '
|
||||
puts ' \\ \\ /\\ / /| |__) | (___ ___ __ _ _ __ ®'
|
||||
puts ' \\ \\/ \\/ / | ___/ \\___ \\ / __|/ _` | \'_ \\ '
|
||||
puts ' \\ /\\ / | | ____) | (__| (_| | | | |'
|
||||
puts ' \\/ \\/ |_| |_____/ \\___|\\__,_|_| |_|'
|
||||
puts
|
||||
puts ' WordPress Security Scanner by the WPScan Team '
|
||||
puts " Version #{WPSCAN_VERSION}"
|
||||
puts ' Sponsored by Sucuri - https://sucuri.net'
|
||||
puts ' @_WPScan_, @ethicalhack3r, @erwan_lr, @_FireFart_'
|
||||
puts '_______________________________________________________________'
|
||||
puts
|
||||
end
|
||||
|
||||
def xml(file)
|
||||
Nokogiri::XML(File.open(file)) do |config|
|
||||
config.noblanks
|
||||
end
|
||||
end
|
||||
|
||||
def json(file)
|
||||
content = File.open(file).read
|
||||
|
||||
begin
|
||||
JSON.parse(content)
|
||||
rescue => e
|
||||
puts "[ERROR] In JSON file parsing #{file} #{e}"
|
||||
raise
|
||||
end
|
||||
end
|
||||
|
||||
def redefine_constant(constant, value)
|
||||
Object.send(:remove_const, constant)
|
||||
Object.const_set(constant, value)
|
||||
end
|
||||
|
||||
# Gets the string all elements in stringarray ends with
|
||||
def get_equal_string_end(stringarray = [''])
|
||||
already_found = ''
|
||||
looping = true
|
||||
counter = -1
|
||||
# remove nils (# Issue #232)
|
||||
stringarray = stringarray.compact
|
||||
if stringarray.kind_of? Array and stringarray.length > 1
|
||||
base = stringarray.first
|
||||
while looping
|
||||
character = base[counter, 1]
|
||||
stringarray.each do |s|
|
||||
if s[counter, 1] != character
|
||||
looping = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if looping == false or (counter * -1) > base.length
|
||||
break
|
||||
end
|
||||
already_found = "#{character if character}#{already_found}"
|
||||
counter -= 1
|
||||
end
|
||||
end
|
||||
already_found
|
||||
end
|
||||
|
||||
def remove_base64_images_from_html(html)
|
||||
# remove data:image/png;base64, images
|
||||
base64regex = %r{(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?}
|
||||
imageregex = %r{data\s*:\s*image/[^\s;]+\s*;\s*base64\s*,\s*}
|
||||
html.gsub(/["']\s*#{imageregex}#{base64regex}\s*["']/, '""')
|
||||
end
|
||||
|
||||
# @return [ Integer ] The memory of the current process in Bytes
|
||||
def get_memory_usage
|
||||
`ps -o rss= -p #{Process.pid}`.to_i * 1024 # ps returns the value in KB
|
||||
end
|
||||
|
||||
# Use the wc system command to count the number of lines in the file
|
||||
# instead of using File.open which will use to much memory for large file (10 times the size of the file)
|
||||
#
|
||||
# @return [ Integer ] The number of lines in the given file
|
||||
def count_file_lines(file)
|
||||
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
|
||||
def truncate(input, size, trailing = '...')
|
||||
size = size.to_i
|
||||
trailing ||= ''
|
||||
return input if input.nil? or size <= 0 or input.length <= size or
|
||||
trailing.length >= input.length or size-trailing.length-1 >= input.length
|
||||
return "#{input[0..size-trailing.length-1]}#{trailing}"
|
||||
end
|
||||
|
||||
# Gets a random User-Agent
|
||||
#
|
||||
# @return [ String ] A random user-agent from data/user-agents.txt
|
||||
def get_random_user_agent
|
||||
user_agents = []
|
||||
|
||||
# If we can't access the file, die
|
||||
raise('[ERROR] Missing user-agent data. Please re-run with just --update.') unless File.exist?(USER_AGENTS_FILE)
|
||||
|
||||
# Read in file
|
||||
f = File.open(USER_AGENTS_FILE, 'r')
|
||||
|
||||
# Read every line in the file
|
||||
f.each_line do |line|
|
||||
# Remove any End of Line issues, and leading/trailing spaces
|
||||
line = line.strip.chomp
|
||||
# Ignore empty files and comments
|
||||
next if line.empty? or line =~ /^\s*(#|\/\/)/
|
||||
# Add to array
|
||||
user_agents << line.strip
|
||||
end
|
||||
# Close file handler
|
||||
f.close
|
||||
|
||||
# Return random user-agent
|
||||
user_agents.sample
|
||||
end
|
||||
|
||||
# Directory listing enabled on url?
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
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
|
||||
|
||||
# Check valid JSON?
|
||||
def valid_json?(json)
|
||||
JSON.parse(json)
|
||||
return true
|
||||
rescue JSON::ParserError => e
|
||||
return false
|
||||
end
|
||||
|
||||
# Get the HTTP response code
|
||||
def get_http_status(url)
|
||||
Browser.get(url.to_s).code
|
||||
end
|
||||
|
||||
# Check to see if we need a "s"
|
||||
def grammar_s(size)
|
||||
size.to_i >= 2 ? "s" : ""
|
||||
end
|
||||
@@ -1,74 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class CustomOptionParser < OptionParser
|
||||
|
||||
attr_reader :symbols_used
|
||||
|
||||
def initialize(banner = nil, width = 32, indent = ' ' * 4)
|
||||
@results = {}
|
||||
@symbols_used = []
|
||||
super(banner, width, indent)
|
||||
end
|
||||
|
||||
|
||||
# param Array(Array) or Array options
|
||||
def add(options)
|
||||
if options.is_a?(Array)
|
||||
if options[0].is_a?(Array)
|
||||
options.each do |option|
|
||||
add_option(option)
|
||||
end
|
||||
else
|
||||
add_option(options)
|
||||
end
|
||||
else
|
||||
raise "Options must be at least an Array, or an Array(Array). #{options.class} supplied"
|
||||
end
|
||||
end
|
||||
|
||||
# param Array option
|
||||
def add_option(option)
|
||||
if option.is_a?(Array)
|
||||
option_symbol = CustomOptionParser::option_to_symbol(option)
|
||||
|
||||
if !@symbols_used.include?(option_symbol)
|
||||
@symbols_used << option_symbol
|
||||
|
||||
self.on(*option) do |arg|
|
||||
@results[option_symbol] = arg
|
||||
end
|
||||
else
|
||||
raise "The option #{option_symbol} is already used !"
|
||||
end
|
||||
else
|
||||
raise "The option must be an array, #{option.class} supplied : '#{option}'"
|
||||
end
|
||||
end
|
||||
|
||||
# return Hash
|
||||
def results(argv = default_argv)
|
||||
self.parse!(argv) if @results.empty?
|
||||
|
||||
@results
|
||||
end
|
||||
|
||||
protected
|
||||
# param Array option
|
||||
def self.option_to_symbol(option)
|
||||
option_name = nil
|
||||
|
||||
option.each do |option_attr|
|
||||
if option_attr =~ /^--/
|
||||
option_name = option_attr
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if option_name
|
||||
option_name = option_name.gsub(/^--/, '').gsub(/-/, '_').gsub(/ .*$/, '')
|
||||
:"#{option_name}"
|
||||
else
|
||||
raise "Could not find the option name for #{option}"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,126 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
# DB Updater
|
||||
class DbUpdater
|
||||
FILES = %w(
|
||||
local_vulnerable_files.xml local_vulnerable_files.xsd
|
||||
timthumbs.txt user-agents.txt wp_versions.xml wp_versions.xsd
|
||||
wordpresses.json plugins.json themes.json LICENSE
|
||||
)
|
||||
|
||||
attr_reader :repo_directory
|
||||
|
||||
def initialize(repo_directory)
|
||||
@repo_directory = repo_directory
|
||||
|
||||
unless Dir.exist?(@repo_directory)
|
||||
FileUtils.mkdir_p(@repo_directory)
|
||||
end
|
||||
|
||||
unless Pathname.new(@repo_directory).writable?
|
||||
fail "#{@repo_directory} is not writable"
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ Hash ] The params for Typhoeus::Request
|
||||
def request_params
|
||||
{
|
||||
ssl_verifyhost: 2,
|
||||
ssl_verifypeer: true,
|
||||
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://data.wpscan.org/#{filename}"
|
||||
end
|
||||
|
||||
# @return [ String ] The checksum of the associated remote filename
|
||||
def remote_file_checksum(filename)
|
||||
url = "#{remote_file_url(filename)}.sha512"
|
||||
|
||||
res = Browser.get(url, request_params)
|
||||
fail DownloadError, res if res.timed_out? || res.code != 200
|
||||
res.body.chomp
|
||||
end
|
||||
|
||||
def local_file_path(filename)
|
||||
File.join(repo_directory, "#{filename}")
|
||||
end
|
||||
|
||||
def local_file_checksum(filename)
|
||||
Digest::SHA512.file(local_file_path(filename)).hexdigest
|
||||
end
|
||||
|
||||
def backup_file_path(filename)
|
||||
File.join(repo_directory, "#{filename}.back")
|
||||
end
|
||||
|
||||
def create_backup(filename)
|
||||
return unless File.exist?(local_file_path(filename))
|
||||
FileUtils.cp(local_file_path(filename), backup_file_path(filename))
|
||||
end
|
||||
|
||||
def restore_backup(filename)
|
||||
return unless File.exist?(backup_file_path(filename))
|
||||
FileUtils.cp(backup_file_path(filename), local_file_path(filename))
|
||||
end
|
||||
|
||||
def delete_backup(filename)
|
||||
FileUtils.rm(backup_file_path(filename))
|
||||
end
|
||||
|
||||
# @return [ String ] The checksum of the downloaded file
|
||||
def download(filename)
|
||||
file_path = local_file_path(filename)
|
||||
file_url = remote_file_url(filename)
|
||||
|
||||
res = Browser.get(file_url, request_params)
|
||||
fail DownloadError, res if res.timed_out? || res.code != 200
|
||||
File.open(file_path, 'wb') { |f| f.write(res.body) }
|
||||
|
||||
local_file_checksum(filename)
|
||||
end
|
||||
|
||||
def update(verbose = false)
|
||||
FILES.each do |filename|
|
||||
begin
|
||||
puts "[+] Checking: #{filename}" if verbose
|
||||
db_checksum = remote_file_checksum(filename)
|
||||
|
||||
# Checking if the file needs to be updated
|
||||
if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
|
||||
puts ' [i] Already Up-To-Date' if verbose
|
||||
next
|
||||
end
|
||||
|
||||
puts ' [i] Needs to be updated' if verbose
|
||||
create_backup(filename)
|
||||
puts ' [i] Backup Created' if verbose
|
||||
puts " [i] Downloading new file: #{remote_file_url(filename)}" if verbose
|
||||
dl_checksum = download(filename)
|
||||
puts " [i] Downloaded File Checksum: #{dl_checksum}" if verbose
|
||||
puts " [i] Database File Checksum : #{db_checksum}" if verbose
|
||||
|
||||
unless dl_checksum == db_checksum
|
||||
raise ChecksumError.new(File.read(local_file_path(filename))), "#{filename}: checksums do not match (local: #{dl_checksum} remote: #{db_checksum})"
|
||||
end
|
||||
rescue => e
|
||||
puts ' [i] Restoring Backup due to error' if verbose
|
||||
restore_backup(filename)
|
||||
raise e
|
||||
ensure
|
||||
if File.exist?(backup_file_path(filename))
|
||||
puts ' [i] Deleting Backup' if verbose
|
||||
delete_backup(filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# write last_update date to file
|
||||
File.write(LAST_UPDATE_FILE, Time.now)
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
# HTTP Error
|
||||
class HttpError < StandardError
|
||||
attr_reader :response
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
def initialize(response)
|
||||
@response = response
|
||||
end
|
||||
|
||||
def failure_details
|
||||
msg = response.effective_url
|
||||
|
||||
if response.code == 0 || response.timed_out?
|
||||
msg += " (#{response.return_message})"
|
||||
else
|
||||
msg += " (status: #{response.code})"
|
||||
end
|
||||
|
||||
msg
|
||||
end
|
||||
|
||||
def message
|
||||
"HTTP Error: #{failure_details}"
|
||||
end
|
||||
end
|
||||
|
||||
# Used in the Updater
|
||||
class DownloadError < HttpError
|
||||
def message
|
||||
"Unable to get #{failure_details}"
|
||||
end
|
||||
end
|
||||
|
||||
class ChecksumError < StandardError
|
||||
attr_reader :file
|
||||
|
||||
def initialize(file)
|
||||
@file = file
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
# This is used in WpItem::Existable
|
||||
module Typhoeus
|
||||
class Response
|
||||
|
||||
# Compare the body hash to error_404_hash and homepage_hash
|
||||
# returns true if they are different, false otherwise
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_valid_hash?(error_404_hash, homepage_hash)
|
||||
body_hash = WebSite.page_hash(self)
|
||||
|
||||
body_hash != error_404_hash && body_hash != homepage_hash
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
# Override for puts to enable logging
|
||||
def puts(o = '')
|
||||
if $log && o.respond_to?(:gsub)
|
||||
temp = o.gsub(/\e\[\d+m/, '') # remove color for logging
|
||||
File.open($log, 'a+') { |f| f.puts(temp) }
|
||||
end
|
||||
|
||||
super(o)
|
||||
end
|
||||
|
||||
class Numeric
|
||||
def bytes_to_human
|
||||
units = %w{B KB MB GB TB}
|
||||
e = (Math.log(abs)/Math.log(1024)).floor
|
||||
s = '%.3f' % (abs.to_f / 1024**e)
|
||||
s.sub(/\.?0*$/, ' ' + units[e])
|
||||
end
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# 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
|
||||
@@ -1,23 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Vulnerability
|
||||
module Output
|
||||
# output the vulnerability
|
||||
def output(verbose = false)
|
||||
puts
|
||||
puts critical("Title: #{title}")
|
||||
|
||||
references.each do |key, urls|
|
||||
methodname = "url_#{key}"
|
||||
|
||||
urls.each do |u|
|
||||
next unless respond_to?(methodname)
|
||||
url = send(methodname, u)
|
||||
puts " Reference: #{url}" if url
|
||||
end
|
||||
end
|
||||
|
||||
puts notice("Fixed in: #{fixed_in}") if fixed_in
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Vulnerability
|
||||
module Urls
|
||||
# @return [ String ] The url to the metasploit module page
|
||||
def url_metasploit(module_path)
|
||||
# remove leading slash
|
||||
module_path = module_path.sub(/^\//, '')
|
||||
"https://www.rapid7.com/db/modules/#{module_path}"
|
||||
end
|
||||
|
||||
def url_url(url)
|
||||
url
|
||||
end
|
||||
|
||||
def url_cve(id)
|
||||
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-#{id}"
|
||||
end
|
||||
|
||||
def url_osvdb(id)
|
||||
"http://osvdb.org/show/osvdb/#{id}"
|
||||
end
|
||||
|
||||
def url_secunia(id)
|
||||
"https://secunia.com/advisories/#{id}/"
|
||||
end
|
||||
|
||||
def url_exploitdb(id)
|
||||
"https://www.exploit-db.com/exploits/#{id}/"
|
||||
end
|
||||
|
||||
def url_id(id)
|
||||
"https://wpvulndb.com/vulnerabilities/#{id}"
|
||||
end
|
||||
|
||||
def url_packetstorm(id)
|
||||
"http://packetstormsecurity.com/files/#{id}/"
|
||||
end
|
||||
|
||||
def url_securityfocus(id)
|
||||
"http://www.securityfocus.com/bid/#{id}/"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,121 +0,0 @@
|
||||
# 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_updated']
|
||||
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
|
||||
@@ -1,50 +0,0 @@
|
||||
# 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
|
||||
@@ -1,18 +0,0 @@
|
||||
# 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_from = method.to_s.gsub(/find_from_/, '').gsub(/_/, ' ')
|
||||
end
|
||||
|
||||
module Findable
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
|
||||
# @uri is used instead of #uri to avoid the presence of the :path into it
|
||||
module Infos
|
||||
|
||||
# Checks if the url status code is 200
|
||||
#
|
||||
# @param [ String ] url
|
||||
#
|
||||
# @return [ Boolean ] True if the url status is 200
|
||||
def url_is_200?(url)
|
||||
Browser.get(url).code == 200
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_readme?
|
||||
!readme_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String,nil ] The url to the readme file, nil if not found
|
||||
def readme_url
|
||||
# See https://github.com/wpscanteam/wpscan/pull/737#issuecomment-66375445
|
||||
# for any question about the order
|
||||
%w{readme.txt README.txt README.md readme.md Readme.txt}.each do |readme|
|
||||
url = @uri.merge(readme).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_changelog?
|
||||
!changelog_url.nil?
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the changelog file
|
||||
def changelog_url
|
||||
%w{changelog.txt CHANGELOG.md changelog.md}.each do |changelog|
|
||||
url = @uri.merge(changelog).to_s
|
||||
return url if url_is_200?(url)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_directory_listing?
|
||||
directory_listing_enabled?(@uri)
|
||||
end
|
||||
|
||||
# Discover any error_log files created by WordPress
|
||||
# These are created by the WordPress error_log() function
|
||||
# They are normally found in the /plugins/ directory,
|
||||
# however can also be found in their specific plugin dir.
|
||||
# http://www.exploit-db.com/ghdb/3714/
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_error_log?
|
||||
WebSite.has_log?(error_log_url, %r{PHP Fatal error}i)
|
||||
end
|
||||
|
||||
# @return [ String ] The url to the error_log file
|
||||
def error_log_url
|
||||
@uri.merge('error_log').to_s
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
module Output
|
||||
|
||||
# @return [ Void ]
|
||||
def output(verbose = false)
|
||||
outdated = VersionCompare.lesser?(version, latest_version) if latest_version
|
||||
|
||||
puts
|
||||
puts info("Name: #{self}") #this will also output the version number if detected
|
||||
puts " | Latest version: #{latest_version} #{'(up to date)' if version}" if latest_version && !outdated
|
||||
puts " | Last updated: #{last_updated}" if last_updated
|
||||
puts " | Location: #{url}"
|
||||
puts " | Readme: #{readme_url}" if has_readme?
|
||||
puts " | Changelog: #{changelog_url}" if has_changelog?
|
||||
puts warning("The version is out of date, the latest version is #{latest_version}") if latest_version && outdated
|
||||
|
||||
puts warning("Directory listing is enabled: #{url}") if has_directory_listing?
|
||||
puts warning("An error_log file has been found: #{error_log_url}") if has_error_log?
|
||||
|
||||
additional_output(verbose) if respond_to?(:additional_output)
|
||||
|
||||
if version.nil? && vulnerabilities.length > 0
|
||||
puts
|
||||
puts warning('We could not determine the version installed. All of the past known vulnerabilities will be output to allow you to do your own manual investigation.')
|
||||
end
|
||||
|
||||
vulnerabilities.output
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpItem
|
||||
attr_writer :version
|
||||
|
||||
module Versionable
|
||||
|
||||
# Get the version from the readme.txt
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def version
|
||||
unless @version
|
||||
# This check is needed because readme_url can return nil
|
||||
if has_readme?
|
||||
response = Browser.get(readme_url)
|
||||
@version = extract_version(response.body)
|
||||
end
|
||||
end
|
||||
@version
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
item_version = self.version
|
||||
"#{@name}#{' - v' + item_version.strip if item_version}"
|
||||
end
|
||||
|
||||
# Extracts the version number from a given string/body
|
||||
#
|
||||
# @return [ String ] detected version
|
||||
def extract_version(body)
|
||||
version = body[/\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i, 1]
|
||||
if version.nil? || version !~ /[0-9]+/
|
||||
extracted_versions = body.scan(/[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.-]*[=]+/i)
|
||||
return if extracted_versions.nil? || extracted_versions.length == 0
|
||||
extracted_versions.flatten!
|
||||
# must contain at least one number
|
||||
extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
|
||||
sorted = extracted_versions.sort { |x,y|
|
||||
begin
|
||||
Gem::Version.new(x) <=> Gem::Version.new(y)
|
||||
rescue
|
||||
0
|
||||
end
|
||||
}
|
||||
return sorted.last
|
||||
else
|
||||
return version
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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
|
||||
@@ -1,16 +0,0 @@
|
||||
# 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
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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
|
||||
@@ -1,37 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme < WpItem
|
||||
module Childtheme
|
||||
|
||||
def parent_theme_limit
|
||||
3
|
||||
end
|
||||
|
||||
def is_child_theme?
|
||||
return true unless @theme_template.nil?
|
||||
false
|
||||
end
|
||||
|
||||
def get_parent_theme_style_url
|
||||
if is_child_theme?
|
||||
return style_url.sub("/#{name}/style.css", "/#{@theme_template}/style.css")
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def get_parent_theme
|
||||
if is_child_theme?
|
||||
base_url = @uri.clone
|
||||
base_url.path = base_url.path.sub(/(?<url>.*\/)#{Regexp.escape(@wp_content_dir)}\/.+/, '\k<url>')
|
||||
return WpTheme.new(base_url,
|
||||
{
|
||||
name: @theme_template,
|
||||
style_url: get_parent_theme_style_url,
|
||||
wp_content_dir: @wp_content_dir
|
||||
})
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,64 +0,0 @@
|
||||
# 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.to_s
|
||||
|
||||
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
|
||||
@@ -1,34 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme < WpItem
|
||||
module Info
|
||||
|
||||
attr_reader :theme_name, :theme_uri, :theme_description,
|
||||
:theme_author, :theme_author_uri, :theme_template,
|
||||
:theme_license, :theme_license_uri, :theme_tags,
|
||||
:theme_text_domain
|
||||
|
||||
def parse_style
|
||||
style = Browser.get(style_url).body
|
||||
@theme_name = parse_style_tag(style, 'Theme Name')
|
||||
@theme_uri = parse_style_tag(style, 'Theme URI')
|
||||
@theme_description = parse_style_tag(style, 'Description')
|
||||
@theme_author = parse_style_tag(style, 'Author')
|
||||
@theme_author_uri = parse_style_tag(style, 'Author URI')
|
||||
@theme_template = parse_style_tag(style, 'Template')
|
||||
@theme_license = parse_style_tag(style, 'License')
|
||||
@theme_license_uri = parse_style_tag(style, 'License URI')
|
||||
@theme_tags = parse_style_tag(style, 'Tags')
|
||||
@theme_text_domain = parse_style_tag(style, 'Text Domain')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_style_tag(style, tag)
|
||||
value = style[/^\s*#{Regexp.escape(tag)}:\s*(.*)/i, 1]
|
||||
return value.strip if value
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTheme
|
||||
module Output
|
||||
|
||||
# @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}" 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
|
||||
puts " | License: #{@theme_license}" if @theme_license and verbose
|
||||
puts " | License URI: #{@theme_license_uri}" if @theme_license_uri and verbose
|
||||
puts " | Tags: #{@theme_tags}" if @theme_tags and verbose
|
||||
puts " | Text Domain: #{@theme_text_domain}" if @theme_text_domain and verbose
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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
|
||||
@@ -1,20 +0,0 @@
|
||||
# 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
|
||||
@@ -1,15 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Existable
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def exists_from_response?(response, options = {})
|
||||
response.code == 400 && response.body =~ /no image specified/i ? true : false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
puts
|
||||
puts info("#{self}") #this will also output the version number if detected
|
||||
|
||||
vulnerabilities.output
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# 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
|
||||
@@ -1,55 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTimthumb < WpItem
|
||||
module Vulnerable
|
||||
# @return [ Vulnerabilities ]
|
||||
def vulnerabilities
|
||||
vulns = Vulnerabilities.new
|
||||
|
||||
[:check_rce_132, :check_rce_webshot].each do |method|
|
||||
vuln = self.send(method)
|
||||
|
||||
vulns << vuln if vuln
|
||||
end
|
||||
vulns
|
||||
end
|
||||
|
||||
def check_rce_132
|
||||
rce_132_vuln unless VersionCompare.lesser_or_equal?('1.33', version)
|
||||
end
|
||||
|
||||
# Vulnerable versions : > 1.35 (or >= 2.0) and < 2.8.14
|
||||
def check_rce_webshot
|
||||
return if VersionCompare.lesser_or_equal?('2.8.14', version) || VersionCompare.lesser_or_equal?(version, '1.35')
|
||||
|
||||
response = Browser.get(uri.merge('?webshot=1&src=http://' + default_allowed_domains.sample))
|
||||
|
||||
rce_webshot_vuln unless response.body =~ /WEBSHOT_ENABLED == true/
|
||||
end
|
||||
|
||||
# @return [ Array<String> ] The default allowed domains (between the 2.0 and 2.8.13)
|
||||
def default_allowed_domains
|
||||
%w(flickr.com picasa.com img.youtube.com upload.wikimedia.org)
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE in the <= 1.32
|
||||
def rce_132_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 1.32 Remote Code Execution',
|
||||
'RCE',
|
||||
{ exploitdb: ['17602'] },
|
||||
'1.33'
|
||||
)
|
||||
end
|
||||
|
||||
# @return [ Vulnerability ] The RCE due to the WebShot in the <= 2.8.13
|
||||
def rce_webshot_vuln
|
||||
Vulnerability.new(
|
||||
'Timthumb <= 2.8.13 WebShot Remote Code Execution',
|
||||
'RCE',
|
||||
{ url: ['http://seclists.org/fulldisclosure/2014/Jun/117'] },
|
||||
'2.8.14'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,81 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'wp_user/existable'
|
||||
require 'wp_user/brute_forcable'
|
||||
|
||||
class WpUser < WpItem
|
||||
include WpUser::Existable
|
||||
include WpUser::BruteForcable
|
||||
|
||||
attr_accessor :id, :login, :display_name, :password
|
||||
|
||||
# @return [ Array<Symbol> ]
|
||||
def allowed_options; [:id, :login, :display_name, :password] end
|
||||
|
||||
# @return [ URI ] The uri to the author page
|
||||
def uri
|
||||
if id
|
||||
@uri.merge("?author=#{id}")
|
||||
else
|
||||
raise 'The id is nil'
|
||||
end
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def login_url
|
||||
unless @login_url
|
||||
@login_url = @uri.merge('wp-login.php').to_s
|
||||
|
||||
# Let's check if the login url is redirected (to https url for example)
|
||||
if redirection = redirection(@login_url)
|
||||
@login_url = redirection
|
||||
end
|
||||
end
|
||||
|
||||
@login_url
|
||||
end
|
||||
|
||||
def redirection(url)
|
||||
redirection = nil
|
||||
response = Browser.get(url)
|
||||
|
||||
if response.code == 301 || response.code == 302
|
||||
redirection = response.headers_hash['location']
|
||||
|
||||
# Let's check if there is a redirection in the redirection
|
||||
if other_redirection = redirection(redirection)
|
||||
redirection = other_redirection
|
||||
end
|
||||
end
|
||||
|
||||
redirection
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def to_s
|
||||
s = "#{id}"
|
||||
s << " | #{login}" if login
|
||||
s << " | #{display_name}" if display_name
|
||||
s
|
||||
end
|
||||
|
||||
# @param [ WpUser ] other
|
||||
def <=>(other)
|
||||
id <=> other.id
|
||||
end
|
||||
|
||||
# @param [ WpUser ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def ==(other)
|
||||
self === other
|
||||
end
|
||||
|
||||
# @param [ WpUser ] other
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def ===(other)
|
||||
id === other.id && login === other.login
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,148 +0,0 @@
|
||||
# encoding: UTF-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,
|
||||
# for that reason, we queue browser.max_threads, send browser.max_threads,
|
||||
# queue browser.max_threads and so on.
|
||||
#
|
||||
# hydra.run only returns when it has recieved all of its, responses.
|
||||
# This means that while we are waiting for browser.max_threads,
|
||||
# responses, we are waiting...
|
||||
#
|
||||
# @param [ String ] wordlist The wordlist path
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Boolean ] :verbose
|
||||
# @option options [ Boolean ] :show_progression
|
||||
# @param [ String ] redirect_url Override for redirect_url
|
||||
#
|
||||
# @return [ void ]
|
||||
def brute_force(wordlist, options = {}, redirect_url = nil)
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
queue_count = 0
|
||||
found = false
|
||||
|
||||
if wordlist == '-'
|
||||
words = ARGF
|
||||
passwords_size = 10
|
||||
options[:starting_at] = 0
|
||||
else
|
||||
words = File.open(wordlist)
|
||||
passwords_size = count_file_lines(wordlist)+1
|
||||
end
|
||||
|
||||
create_progress_bar(passwords_size, options)
|
||||
|
||||
words.each do |password|
|
||||
password.chomp!
|
||||
|
||||
# A successfull login will redirect us to the redirect_to parameter
|
||||
# Generate a random one on each request
|
||||
unless redirect_url
|
||||
random = (0...8).map { 65.+(rand(26)).chr }.join
|
||||
redirect_url = "#{@uri}#{random}/"
|
||||
end
|
||||
|
||||
request = login_request(password, redirect_url)
|
||||
|
||||
request.on_complete do |response|
|
||||
if options[:show_progression] && !found
|
||||
progress_bar.progress += 1
|
||||
percentage = progress_bar.progress.fdiv(progress_bar.total)
|
||||
if options[:starting_at] && percentage >= 0.8
|
||||
progress_bar.total *= 2
|
||||
end
|
||||
end
|
||||
|
||||
progress_bar.log(" Trying Username: #{login} Password: #{password}") if options[:verbose]
|
||||
|
||||
if valid_password?(response, password, redirect_url, options)
|
||||
found = true
|
||||
self.password = password
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count >= browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
progress_bar.log(" Sent #{browser.max_threads} request/s ...") if options[:verbose]
|
||||
end
|
||||
end
|
||||
|
||||
# run all of the remaining requests
|
||||
hydra.run
|
||||
puts if options[:show_progression] # mandatory to avoid the output of the progressbar to be overriden
|
||||
end
|
||||
|
||||
# @param [ Integer ] passwords_size
|
||||
# @param [ Hash ] options
|
||||
#
|
||||
# @return [ ProgressBar ]
|
||||
# :nocov:
|
||||
def create_progress_bar(passwords_size, options)
|
||||
if options[:show_progression]
|
||||
@progress_bar = ProgressBar.create(
|
||||
format: '%t %a <%B> (%c / %C) %P%% %e',
|
||||
title: " Brute Forcing '#{login}'",
|
||||
total: passwords_size,
|
||||
starting_at: options[:starting_at]
|
||||
)
|
||||
end
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# @param [ String ] password
|
||||
# @param [ String ] redirect_url
|
||||
#
|
||||
# @return [ Typhoeus::Request ]
|
||||
def login_request(password, redirect_url)
|
||||
Browser.instance.forge_request(login_url,
|
||||
method: :post,
|
||||
body: { log: login, pwd: password, redirect_to: redirect_url },
|
||||
cache_ttl: 0
|
||||
)
|
||||
end
|
||||
|
||||
# @param [ Typhoeus::Response ] response
|
||||
# @param [ String ] password
|
||||
# @param [ String ] redirect_url
|
||||
# @param [ Hash ] options
|
||||
# @option options [ Boolean ] :verbose
|
||||
# @option options [ Boolean ] :show_progression
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def valid_password?(response, password, redirect_url, options = {})
|
||||
if response.code == 302 && response.headers_hash && response.headers_hash['Location'] == redirect_url
|
||||
progression = "#{info('[SUCCESS]')} Login : #{login} Password : #{password}\n\n"
|
||||
valid = true
|
||||
elsif response.body =~ /login_error/i
|
||||
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 or use the --throttle option.')
|
||||
else
|
||||
progression = critical("ERROR: We received an unknown response for login: #{login} and password: #{password}")
|
||||
verbose = critical(" Code: #{response.code}\n Body: #{response.body}\n")
|
||||
end
|
||||
|
||||
progress_bar.log(" #{progression}") if progression && options[:show_progression]
|
||||
progress_bar.log(" #{verbose}") if verbose && options[:verbose]
|
||||
|
||||
valid || false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,86 +0,0 @@
|
||||
# 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
|
||||
# & are not decoded with Nokogiri
|
||||
title_tag.gsub!('&', '&')
|
||||
|
||||
# replace UTF chars like » with dummy character
|
||||
title_tag.gsub!(/&#(\d+);/, '|')
|
||||
|
||||
name = title_tag[%r{([^|«»]+) }, 1]
|
||||
|
||||
return name.strip if name
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
# 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 = {}
|
||||
temp = json[version]
|
||||
if !temp.nil?
|
||||
metadata[:release_date] = temp['release_date']
|
||||
metadata[:changelog_url] = temp['changelog_url']
|
||||
end
|
||||
metadata
|
||||
end
|
||||
end
|
||||
@@ -1,240 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpVersion < WpItem
|
||||
|
||||
module Findable
|
||||
|
||||
# Find the version of the blog designated from target_uri
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
# @param [ String ] wp_content_dir
|
||||
# @param [ String ] wp_plugins_dir
|
||||
#
|
||||
# @return [ WpVersion ]
|
||||
def find(target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
versions = {}
|
||||
methods.grep(/^find_from_/).each do |method|
|
||||
|
||||
if method === :find_from_advanced_fingerprinting
|
||||
version = send(method, target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
else
|
||||
version = send(method, target_uri)
|
||||
end
|
||||
|
||||
if version
|
||||
if versions.key?(version)
|
||||
versions[version] << method.to_s
|
||||
else
|
||||
versions[version] = [ method.to_s ]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if versions.length > 0
|
||||
determined_version = versions.max_by { |k, v| v.length }
|
||||
if determined_version
|
||||
return new(target_uri, number: determined_version[0], found_from: determined_version[1].join(', '))
|
||||
end
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
# Used to check if the version is correct: must contain at least one dot.
|
||||
#
|
||||
# @return [ String ]
|
||||
def version_pattern
|
||||
'([^\r\n"\',]+\.[^\r\n"\',]+)'
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Returns the first match of <pattern> in the body of the url
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
# @param [ Regex ] pattern
|
||||
# @param [ String ] path
|
||||
#
|
||||
# @return [ String ]
|
||||
def scan_url(target_uri, pattern, path = nil)
|
||||
url = path ? target_uri.merge(path).to_s : target_uri.to_s
|
||||
response = Browser.get_and_follow_location(url)
|
||||
|
||||
response.body[pattern, 1]
|
||||
end
|
||||
|
||||
#
|
||||
# DO NOT Change the order of the following methods
|
||||
# unless you know what you are doing
|
||||
# See WpVersion.find
|
||||
#
|
||||
|
||||
# Attempts to find the wordpress version from,
|
||||
# the generator meta tag in the html source.
|
||||
#
|
||||
# The meta tag can be removed however it seems,
|
||||
# that it is reinstated on upgrade.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_meta_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{name="generator" content="wordpress #{version_pattern}.*"}i
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from,
|
||||
# the generator tag in the RSS feed source.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_rss_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{<generator>http://wordpress.org/\?v=#{version_pattern}</generator>}i,
|
||||
'feed/'
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find WordPress version from,
|
||||
# the generator tag in the RDF feed source.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_rdf_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{<admin:generatorAgent rdf:resource="http://wordpress.org/\?v=#{version_pattern}" />}i,
|
||||
'feed/rdf/'
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from,
|
||||
# the generator tag in the Atom source.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_atom_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{<generator uri="http://wordpress.org/" version="#{version_pattern}">WordPress</generator>}i,
|
||||
'feed/atom/'
|
||||
)
|
||||
end
|
||||
|
||||
# Uses data/wp_versions.xml to try to identify a
|
||||
# wordpress version.
|
||||
#
|
||||
# It does this by using client side file hashing
|
||||
#
|
||||
# /!\ Warning : this method might return false positive if the file used for fingerprinting is part of a theme (they can be updated)
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
# @param [ String ] wp_content_dir
|
||||
# @param [ String ] wp_plugins_dir
|
||||
# @param [ String ] versions_xml The path to the xml containing all versions
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_advanced_fingerprinting(target_uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
xml = xml(versions_xml)
|
||||
|
||||
wp_item = WpItem.new(target_uri,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir)
|
||||
|
||||
xml.xpath('//file').each do |node|
|
||||
wp_item.path = node.attribute('src').text
|
||||
|
||||
response = Browser.get(wp_item.url)
|
||||
md5sum = Digest::MD5.hexdigest(response.body)
|
||||
|
||||
node.search('hash').each do |hash|
|
||||
if hash.attribute('md5').text == md5sum
|
||||
return hash.search('version').text
|
||||
end
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the readme.html file.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_readme(target_uri)
|
||||
version = scan_url(
|
||||
target_uri,
|
||||
%r{<br />\sversion #{version_pattern}}i,
|
||||
'readme.html'
|
||||
)
|
||||
|
||||
# Since WP >= 4.7, the Readme only contains the major version
|
||||
VersionCompare.lesser?(version, '4.7') ? version : nil
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the sitemap.xml file.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_sitemap_generator(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{generator="wordpress/#{version_pattern}"}i,
|
||||
'sitemap.xml'
|
||||
)
|
||||
end
|
||||
|
||||
# Attempts to find the WordPress version from the p-links-opml.php file.
|
||||
#
|
||||
# @param [ URI ] target_uri
|
||||
#
|
||||
# @return [ String ] The version number
|
||||
def find_from_links_opml(target_uri)
|
||||
scan_url(
|
||||
target_uri,
|
||||
%r{generator="wordpress/#{version_pattern}"}i,
|
||||
'wp-links-opml.php'
|
||||
)
|
||||
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?
|
||||
|
||||
begin
|
||||
uri = Addressable::URI.parse(attr_value)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
next
|
||||
end
|
||||
|
||||
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
|
||||
@@ -1,31 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpVersion < WpItem
|
||||
module Output
|
||||
|
||||
def output(verbose = false)
|
||||
metadata = self.metadata(self.number)
|
||||
|
||||
puts
|
||||
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} #{"(Released on #{metadata[:release_date]}) identified from #{self.found_from}" if metadata[:release_date]}")
|
||||
end
|
||||
|
||||
vulnerabilities = self.vulnerabilities
|
||||
|
||||
unless vulnerabilities.empty?
|
||||
if vulnerabilities.size == 1
|
||||
puts critical("#{vulnerabilities.size} vulnerability identified from the version number")
|
||||
else
|
||||
puts critical("#{vulnerabilities.size} vulnerabilities identified from the version number")
|
||||
end
|
||||
vulnerabilities.output
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Plugin
|
||||
|
||||
attr_reader :author, :registered_options
|
||||
|
||||
def initialize(infos = {})
|
||||
@author = infos[:author]
|
||||
end
|
||||
|
||||
def run(options = {})
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# param Array options
|
||||
def register_options(*options)
|
||||
options.each do |option|
|
||||
unless option.is_a?(Array)
|
||||
raise "Each option must be an array, #{option.class} supplied"
|
||||
end
|
||||
end
|
||||
@registered_options = options
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class Plugins < Array
|
||||
|
||||
attr_reader :option_parser
|
||||
|
||||
def initialize(option_parser = nil)
|
||||
if option_parser
|
||||
if option_parser.is_a?(CustomOptionParser)
|
||||
@option_parser = option_parser
|
||||
else
|
||||
raise "The parser must be an instance of CustomOptionParser, #{option_parser.class} supplied"
|
||||
end
|
||||
else
|
||||
@option_parser = CustomOptionParser.new
|
||||
end
|
||||
end
|
||||
|
||||
# param Array(Plugin) plugins
|
||||
def register(*plugins)
|
||||
plugins.each do |plugin|
|
||||
register_plugin(plugin)
|
||||
end
|
||||
end
|
||||
|
||||
# param Plugin plugin
|
||||
def register_plugin(plugin)
|
||||
if plugin.is_a?(Plugin)
|
||||
self << plugin
|
||||
|
||||
# A plugin may not have options
|
||||
if plugin_options = plugin.registered_options
|
||||
@option_parser.add(plugin_options)
|
||||
end
|
||||
else
|
||||
raise "The argument must be an instance of Plugin, #{plugin.class} supplied"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'common/cache_file_store'
|
||||
|
||||
class TyphoeusCache < CacheFileStore
|
||||
|
||||
def get(request)
|
||||
read_entry(request.hash.to_s)
|
||||
end
|
||||
|
||||
def set(request, response)
|
||||
write_entry(request.hash.to_s, response, request.cache_ttl)
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,62 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class VersionCompare
|
||||
|
||||
# Compares two version strings. Returns true if version1 <= version2
|
||||
# and false otherwise
|
||||
#
|
||||
# @param [ String ] version1
|
||||
# @param [ String ] version2
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def self.lesser_or_equal?(version1, version2)
|
||||
# Prepend a '0' if the version starts with a '.'
|
||||
version1 = prepend_zero(version1)
|
||||
version2 = prepend_zero(version2)
|
||||
|
||||
return true if (version1 == version2)
|
||||
# Both versions must be set
|
||||
return false unless (version1 and version2)
|
||||
return false if (version1.empty? or version2.empty?)
|
||||
begin
|
||||
return true if (Gem::Version.new(version1) < Gem::Version.new(version2))
|
||||
rescue ArgumentError => e
|
||||
# Example: ArgumentError: Malformed version number string a
|
||||
return false if e.message =~ /Malformed version number string/
|
||||
raise
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
# Compares two version strings. Returns true if version1 < version2
|
||||
# and false otherwise
|
||||
#
|
||||
# @param [ String ] version1
|
||||
# @param [ String ] version2
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def self.lesser?(version1, version2)
|
||||
# Prepend a '0' if the version starts with a '.'
|
||||
version1 = prepend_zero(version1)
|
||||
version2 = prepend_zero(version2)
|
||||
|
||||
return false if (version1 == version2)
|
||||
# Both versions must be set
|
||||
return false unless (version1 and version2)
|
||||
return false if (version1.empty? or version2.empty?)
|
||||
begin
|
||||
return true if (Gem::Version.new(version1) < Gem::Version.new(version2))
|
||||
rescue ArgumentError => e
|
||||
# Example: ArgumentError: Malformed version number string a
|
||||
return false if e.message =~ /Malformed version number string/
|
||||
raise
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def self.prepend_zero(version)
|
||||
return nil if version.nil?
|
||||
version[0,1] == '.' ? "0#{version}" : version
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'rubygems'
|
||||
|
||||
version = RUBY_VERSION.dup
|
||||
|
||||
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
|
||||
|
||||
# Fix for issue #245 "invalid byte sequence in US-ASCII"
|
||||
Encoding.default_external = Encoding::UTF_8
|
||||
|
||||
begin
|
||||
# Standard libs
|
||||
require 'readline'
|
||||
require 'bundler/setup' unless kali_linux?
|
||||
require 'getoptlong'
|
||||
require 'optparse' # Will replace getoptlong
|
||||
require 'uri'
|
||||
require 'time'
|
||||
require 'resolv'
|
||||
require 'digest/md5'
|
||||
require 'digest/sha1'
|
||||
require 'base64'
|
||||
require 'rbconfig'
|
||||
require 'pp'
|
||||
require 'shellwords'
|
||||
require 'fileutils'
|
||||
require 'pathname'
|
||||
require 'cgi'
|
||||
# Third party libs
|
||||
require 'typhoeus'
|
||||
require 'yajl/json_gem'
|
||||
require 'nokogiri'
|
||||
require 'terminal-table'
|
||||
require 'ruby-progressbar'
|
||||
require 'addressable/uri'
|
||||
# Custom libs
|
||||
require 'common/browser'
|
||||
require 'common/custom_option_parser'
|
||||
rescue LoadError => e
|
||||
puts "[ERROR] #{e}"
|
||||
|
||||
missing_gem = e.to_s[%r{ -- ([^/]+)/?\z}, 1]
|
||||
if missing_gem
|
||||
if missing_gem =~ /nokogiri/i
|
||||
puts
|
||||
puts 'Nokogiri needs some packets, please run \'sudo apt-get install libxml2 libxml2-dev libxslt1-dev\' to install them. Then run the command below'
|
||||
puts
|
||||
end
|
||||
puts "[TIP] Try to run 'gem install #{missing_gem}' or 'gem install --user-install #{missing_gem}'. If you still get an error, Please see README file or https://github.com/wpscanteam/wpscan"
|
||||
end
|
||||
exit(1)
|
||||
end
|
||||
|
||||
if Typhoeus::VERSION == '0.4.0'
|
||||
puts 'Typhoeus 0.4.0 detected, please update the gem otherwise wpscan will not work correctly'
|
||||
exit(1)
|
||||
end
|
||||
@@ -1,143 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'web_site/humans_txt'
|
||||
require 'web_site/interesting_headers'
|
||||
require 'web_site/robots_txt'
|
||||
require 'web_site/security_txt'
|
||||
require 'web_site/sitemap'
|
||||
require 'web_site/sql_file_export'
|
||||
|
||||
class WebSite
|
||||
include WebSite::HumansTxt
|
||||
include WebSite::InterestingHeaders
|
||||
include WebSite::RobotsTxt
|
||||
include WebSite::SecurityTxt
|
||||
include WebSite::Sitemap
|
||||
include WebSite::SqlFileExport
|
||||
|
||||
attr_reader :uri
|
||||
|
||||
def initialize(site_url)
|
||||
self.url = site_url
|
||||
end
|
||||
|
||||
def url=(url)
|
||||
@uri = URI.parse(add_trailing_slash(add_http_protocol(url)))
|
||||
end
|
||||
|
||||
def url
|
||||
@uri.to_s
|
||||
end
|
||||
|
||||
# Checks if the remote website has ssl errors
|
||||
def ssl_error?
|
||||
return false unless @uri.scheme == 'https'
|
||||
c = get_root_path_return_code
|
||||
# http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code
|
||||
return (
|
||||
c == :ssl_connect_error ||
|
||||
c == :peer_failed_verification ||
|
||||
c == :ssl_certproblem ||
|
||||
c == :ssl_cipher ||
|
||||
c == :ssl_cacert ||
|
||||
c == :ssl_cacert_badfile ||
|
||||
c == :ssl_issuer_error ||
|
||||
c == :ssl_crl_badfile ||
|
||||
c == :ssl_engine_setfailed ||
|
||||
c == :ssl_engine_notfound
|
||||
)
|
||||
end
|
||||
|
||||
def get_root_path_return_code
|
||||
Browser.get(@uri.to_s).return_code
|
||||
end
|
||||
|
||||
# Checks if the remote website is up.
|
||||
def online?
|
||||
Browser.get(@uri.to_s).code != 0
|
||||
end
|
||||
|
||||
def has_basic_auth?
|
||||
Browser.get(@uri.to_s).code == 401
|
||||
end
|
||||
|
||||
def has_xml_rpc?
|
||||
response = Browser.get_and_follow_location(xml_rpc_url)
|
||||
response.body =~ %r{XML-RPC server accepts POST requests only}i
|
||||
end
|
||||
|
||||
# See http://www.hixie.ch/specs/pingback/pingback-1.0#TOC2.3
|
||||
def xml_rpc_url
|
||||
unless @xmlrpc_url
|
||||
@xmlrpc_url = @uri.merge('xmlrpc.php').to_s
|
||||
end
|
||||
|
||||
@xmlrpc_url
|
||||
end
|
||||
|
||||
# See if the remote url returns 30x redirect
|
||||
# This method is recursive
|
||||
# Return a string with the redirection or nil
|
||||
def redirection(url = nil)
|
||||
redirection = nil
|
||||
url ||= @uri.to_s
|
||||
response = Browser.get(url)
|
||||
|
||||
redirected_uri = URI.parse(add_trailing_slash(add_http_protocol(url)))
|
||||
if response.code == 301 || response.code == 302
|
||||
redirection = redirected_uri.merge(response.headers_hash['location']).to_s
|
||||
|
||||
return redirection if url == redirection # prevents infinite loop
|
||||
|
||||
# Let's check if there is a redirection in the redirection
|
||||
if other_redirection = redirection(redirection)
|
||||
redirection = other_redirection
|
||||
end
|
||||
end
|
||||
|
||||
redirection
|
||||
end
|
||||
|
||||
# Compute the MD5 of the page
|
||||
# Comments and scripts are deleted from the page to avoid cache generation details
|
||||
#
|
||||
# @param [ String, Typhoeus::Response ] page The url of the response of the page
|
||||
#
|
||||
# @return [ String ] The MD5 hash of the page
|
||||
def self.page_hash(page)
|
||||
page = Browser.get(page, { followlocation: true, cache_ttl: 0 }) unless page.is_a?(Typhoeus::Response)
|
||||
# remove comments
|
||||
page = page.body.gsub(/<!--.*?-->/m, '')
|
||||
# remove javascript stuff
|
||||
page = page.gsub(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/m, '')
|
||||
Digest::MD5.hexdigest(page)
|
||||
end
|
||||
|
||||
def homepage_hash
|
||||
unless @homepage_hash
|
||||
@homepage_hash = WebSite.page_hash(@uri.to_s)
|
||||
end
|
||||
@homepage_hash
|
||||
end
|
||||
|
||||
# Return the MD5 hash of a 404 page
|
||||
def error_404_hash
|
||||
unless @error_404_hash
|
||||
non_existant_page = Digest::MD5.hexdigest(rand(999_999_999).to_s) + '.html'
|
||||
@error_404_hash = WebSite.page_hash(@uri.merge(non_existant_page).to_s)
|
||||
end
|
||||
@error_404_hash
|
||||
end
|
||||
|
||||
# Only the first 700 bytes are checked to avoid the download
|
||||
# of the whole file which can be very huge (like 2 Go)
|
||||
#
|
||||
# @param [ String ] log_url
|
||||
# @param [ RegEx ] pattern
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def self.has_log?(log_url, pattern)
|
||||
log_body = Browser.get(log_url, headers: {'range' => 'bytes=0-700'}).body
|
||||
log_body[pattern] ? true : false
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module HumansTxt
|
||||
|
||||
# Gets the humans.txt URL
|
||||
# @return [ String ]
|
||||
def humans_url
|
||||
@uri.clone.merge('humans.txt').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module InterestingHeaders
|
||||
|
||||
# Checks for interesting headers
|
||||
# @return [ Array ] Interesting Headers
|
||||
def interesting_headers
|
||||
response = Browser.head(@uri.to_s)
|
||||
headers = response.headers
|
||||
# Header Names are case insensitve so convert them to upcase
|
||||
headers_uppercase = headers.inject({}) do |hash, keys|
|
||||
hash[keys[0].upcase] = keys[1]
|
||||
hash
|
||||
end
|
||||
InterestingHeaders.known_headers.each do |h|
|
||||
headers_uppercase.delete(h.upcase)
|
||||
end
|
||||
headers_uppercase.to_a.compact.sort
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# @return [ Array ]
|
||||
def self.known_headers
|
||||
%w{
|
||||
Location
|
||||
Date
|
||||
Content-Type
|
||||
Content-Length
|
||||
Connection
|
||||
Etag
|
||||
Expires
|
||||
Last-Modified
|
||||
Pragma
|
||||
Vary
|
||||
Cache-Control
|
||||
X-Pingback
|
||||
Accept-Ranges
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module RobotsTxt
|
||||
|
||||
# Checks if a robots.txt file exists
|
||||
# @return [ Boolean ]
|
||||
def has_robots?
|
||||
Browser.get(robots_url).code == 200
|
||||
end
|
||||
|
||||
# Gets a robots.txt URL
|
||||
# @return [ String ]
|
||||
def robots_url
|
||||
@uri.clone.merge('robots.txt').to_s
|
||||
end
|
||||
|
||||
# Parse robots.txt
|
||||
# @return [ Array ] URLs generated from robots.txt
|
||||
def parse_robots_txt
|
||||
return_object = []
|
||||
|
||||
# Make request
|
||||
response = Browser.get(robots_url.to_s)
|
||||
body = response.body
|
||||
|
||||
# Get all allow and disallow urls
|
||||
entries = body.scan(/^(?:dis)?allow:\s*(.*)$/i)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
|
||||
# Sort
|
||||
entries.sort!
|
||||
|
||||
# Wordpress URL
|
||||
wordpress_path = @uri.path
|
||||
|
||||
# Each "boring" value as defined below, remove
|
||||
RobotsTxt.known_dirs.each do |d|
|
||||
entries.delete(d)
|
||||
# Also delete when wordpress is installed in subdir
|
||||
dir_with_subdir = "#{wordpress_path}/#{d}".gsub(/\/+/, '/')
|
||||
entries.delete(dir_with_subdir)
|
||||
end
|
||||
|
||||
# Convert to full URIs
|
||||
return_object = full_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Useful ~ "function do_robots()" -> https://github.com/WordPress/WordPress/blob/master/wp-includes/functions.php
|
||||
#
|
||||
# @return [ Array ]
|
||||
def self.known_dirs
|
||||
%w{
|
||||
/
|
||||
/wp-admin/
|
||||
/wp-admin/admin-ajax.php
|
||||
/wp-includes/
|
||||
/wp-content/
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module SecurityTxt
|
||||
|
||||
# Gets the security.txt URL
|
||||
# @return [ String ]
|
||||
def security_url
|
||||
@uri.clone.merge('.well-known/security.txt').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module Sitemap
|
||||
|
||||
# Checks if a sitemap.txt file exists
|
||||
# @return [ Boolean ]
|
||||
def has_sitemap?
|
||||
# Make the request
|
||||
response = Browser.get(sitemap_url)
|
||||
|
||||
# Make sure its HTTP 200
|
||||
return false unless response.code == 200
|
||||
|
||||
# Is there a sitemap value?
|
||||
result = response.body.scan(/^sitemap\s*:\s*(.*)$/i)
|
||||
return true if result[0]
|
||||
return false
|
||||
end
|
||||
|
||||
# Get the robots.txt URL
|
||||
# @return [ String ]
|
||||
def sitemap_url
|
||||
@uri.clone.merge('robots.txt').to_s
|
||||
end
|
||||
|
||||
# Parse robots.txt
|
||||
# @return [ Array ] URLs generated from robots.txt
|
||||
def parse_sitemap
|
||||
return_object = []
|
||||
|
||||
# Make request
|
||||
response = Browser.get(sitemap_url.to_s)
|
||||
|
||||
# Get all allow and disallow urls
|
||||
entries = response.body.scan(/^sitemap\s*:\s*(.*)$/i)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
|
||||
# Sort
|
||||
entries.sort!
|
||||
|
||||
# Convert to full URIs
|
||||
return_object = full_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,35 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WebSite
|
||||
module SqlFileExport
|
||||
|
||||
# Checks if a .sql file exists
|
||||
# @return [ Array ]
|
||||
def sql_file_export
|
||||
export_files = []
|
||||
|
||||
self.sql_file_export_urls.each do |url|
|
||||
response = Browser.get(url)
|
||||
export_files << url if response.code == 200 && response.body =~ /INSERT INTO/
|
||||
end
|
||||
|
||||
export_files
|
||||
end
|
||||
|
||||
# Gets a .sql export file URL
|
||||
# @return [ Array ]
|
||||
def sql_file_export_urls
|
||||
urls = []
|
||||
host = @uri.host[/(^[\w|-]+)/,1]
|
||||
|
||||
files = ["#{host}.sql", "#{host}.sql.gz", "#{host}.zip", 'db.sql', 'site.sql', 'database.sql',
|
||||
'data.sql', 'backup.sql', 'dump.sql', 'db_backup.sql', 'dbdump.sql', 'wordpress.sql', 'mysql.sql']
|
||||
|
||||
files.each do |file|
|
||||
urls << @uri.clone.merge(file).to_s
|
||||
end
|
||||
|
||||
urls
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,184 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require 'web_site'
|
||||
require 'wp_target/wp_api'
|
||||
require 'wp_target/wp_config_backup'
|
||||
require 'wp_target/wp_custom_directories'
|
||||
require 'wp_target/wp_full_path_disclosure'
|
||||
require 'wp_target/wp_login_protection'
|
||||
require 'wp_target/wp_must_use_plugins'
|
||||
require 'wp_target/wp_readme'
|
||||
require 'wp_target/wp_registrable'
|
||||
require 'wp_target/wp_rss'
|
||||
|
||||
class WpTarget < WebSite
|
||||
include WpTarget::WpAPI
|
||||
include WpTarget::WpConfigBackup
|
||||
include WpTarget::WpCustomDirectories
|
||||
include WpTarget::WpFullPathDisclosure
|
||||
include WpTarget::WpLoginProtection
|
||||
include WpTarget::WpMustUsePlugins
|
||||
include WpTarget::WpReadme
|
||||
include WpTarget::WpRegistrable
|
||||
include WpTarget::WpRSS
|
||||
|
||||
attr_reader :verbose
|
||||
|
||||
def initialize(target_url, options = {})
|
||||
raise Exception.new('target_url can not be nil or empty') if target_url.nil? || target_url == ''
|
||||
super(target_url)
|
||||
|
||||
@verbose = options[:verbose]
|
||||
@wp_content_dir = options[:wp_content_dir]
|
||||
@wp_plugins_dir = options[:wp_plugins_dir]
|
||||
@multisite = nil
|
||||
@vhost = options[:vhost]
|
||||
|
||||
Browser.instance.referer = url
|
||||
if @vhost
|
||||
Browser.instance.vhost = @vhost
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# check if the target website is
|
||||
# actually running wordpress.
|
||||
def wordpress?
|
||||
wordpress = false
|
||||
|
||||
response = Browser.get_and_follow_location(@uri.to_s)
|
||||
|
||||
# Note: in the future major WPScan version, change the user-agent to see
|
||||
# if the response is a 200 ?
|
||||
fail "The target is responding with a 403, this might be due to a WAF or a plugin.\n" \
|
||||
'You should try to supply a valid user-agent via the --user-agent option or use the --random-agent option' if response.code == 403
|
||||
|
||||
dir = wp_content_dir ? wp_content_dir : 'wp-content'
|
||||
|
||||
if response.body =~ /["'][^"']*\/#{Regexp.escape(dir)}\/[^"']*["']/i
|
||||
wordpress = true
|
||||
else
|
||||
|
||||
if has_xml_rpc?
|
||||
wordpress = true
|
||||
else
|
||||
response = Browser.get_and_follow_location(login_url)
|
||||
|
||||
if response.code == 200 && response.body =~ %r{WordPress}i
|
||||
wordpress = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
wordpress
|
||||
end
|
||||
|
||||
def wordpress_hosted?
|
||||
@uri.to_s =~ /\.wordpress\.com/i
|
||||
end
|
||||
|
||||
def login_url
|
||||
url = @uri.merge('wp-login.php').to_s
|
||||
|
||||
# Let's check if the login url is redirected (to https url for example)
|
||||
redirection = redirection(url)
|
||||
url = redirection if redirection
|
||||
|
||||
url
|
||||
end
|
||||
|
||||
# Valid HTTP return codes
|
||||
def self.valid_response_codes
|
||||
[200, 301, 302, 401, 403, 500, 400]
|
||||
end
|
||||
|
||||
# @return [ WpTheme ]
|
||||
# :nocov:
|
||||
def theme
|
||||
WpTheme.find(@uri)
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# @param [ String ] versions_xml
|
||||
#
|
||||
# @return [ WpVersion ]
|
||||
# :nocov:
|
||||
def version(versions_xml)
|
||||
WpVersion.find(@uri, wp_content_dir, wp_plugins_dir, versions_xml)
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
# The version is not yet considered
|
||||
#
|
||||
# @param [ String ] name
|
||||
# @param [ String ] version
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_plugin?(name, version = nil)
|
||||
WpPlugin.new(
|
||||
@uri,
|
||||
name: name,
|
||||
version: version,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir
|
||||
).exists?
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def has_debug_log?
|
||||
WebSite.has_log?(debug_log_url, %r{\[[^\]]+\] PHP (?:Warning|Error|Notice):})
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def debug_log_url
|
||||
@uri.merge("#{wp_content_dir}/debug.log").to_s
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def upload_dir_url
|
||||
@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/
|
||||
#
|
||||
# @return [ String ]
|
||||
def search_replace_db_2_url
|
||||
@uri.merge('searchreplacedb2.php').to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def search_replace_db_2_exists?
|
||||
resp = Browser.get(search_replace_db_2_url)
|
||||
resp.code == 200 && resp.body[%r{by interconnect}i]
|
||||
end
|
||||
|
||||
# Script used to recover locked out admin users
|
||||
# http://yoast.com/emergency-wordpress-access/
|
||||
# https://codex.wordpress.org/User:MichaelH/Orphaned_Plugins_needing_Adoption/Emergency
|
||||
#
|
||||
# @return [ String ]
|
||||
def emergency_url
|
||||
@uri.merge('emergency.php').to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def emergency_exists?
|
||||
resp = Browser.get(emergency_url)
|
||||
resp.code == 200 && resp.body[%r{password}i]
|
||||
end
|
||||
|
||||
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
|
||||
@@ -1,86 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpAPI
|
||||
|
||||
# Checks to see if the REST API is enabled
|
||||
#
|
||||
# This by default in a WordPress installation since 4.5+
|
||||
# @return [ Boolean ]
|
||||
def has_api?(url)
|
||||
# Make the request
|
||||
response = Browser.get(url)
|
||||
|
||||
# Able to view the output?
|
||||
if valid_json?(response.body) && response.body != ''
|
||||
# Read in JSON
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
# If there is nothing there, return false
|
||||
if data.empty?
|
||||
return false
|
||||
# WAF/API disabled response
|
||||
elsif data.include?('message') and data['message'] =~ /Only authenticated users can access the REST API/
|
||||
return false
|
||||
# Success!
|
||||
elsif response.code == 200
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# Something went wrong
|
||||
return false
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL
|
||||
def json_url
|
||||
@uri.merge('/wp-json/').to_s
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL to show users
|
||||
def json_users_url
|
||||
@uri.merge('/wp-json/wp/v2/users').to_s
|
||||
end
|
||||
|
||||
# @return [ String ] The API/JSON URL to show users
|
||||
def json_get_users(url)
|
||||
# Variables
|
||||
users = []
|
||||
|
||||
# Make the request
|
||||
response = Browser.get(url)
|
||||
|
||||
# If not HTTP 200, return false
|
||||
return false unless response.code == 200
|
||||
|
||||
# Able to view the output?
|
||||
return false unless valid_json?(response.body)
|
||||
|
||||
# Read in JSON
|
||||
data = JSON.parse(response.body)
|
||||
|
||||
# If there is nothing there, return false
|
||||
return false if data.empty?
|
||||
|
||||
# Add to array
|
||||
data.each do |child|
|
||||
row = [ child['id'], child['name'], child['link'] ]
|
||||
users << row
|
||||
end
|
||||
|
||||
# Sort and uniq
|
||||
users = users.sort.uniq
|
||||
|
||||
if users and users.size >= 1
|
||||
# Feedback
|
||||
grammar = grammar_s(users.size)
|
||||
puts warning("#{users.size} user#{grammar} exposed via API: #{json_users_url}")
|
||||
|
||||
# Print results
|
||||
table = Terminal::Table.new(headings: ['ID', 'Name', 'URL'],
|
||||
rows: users)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpConfigBackup
|
||||
|
||||
# Checks to see if wp-config.php has a backup
|
||||
# See http://www.feross.org/cmsploit/
|
||||
# @return [ Array ] Backup config files
|
||||
def config_backup
|
||||
found = []
|
||||
backups = WpConfigBackup.config_backup_files
|
||||
browser = Browser.instance
|
||||
hydra = browser.hydra
|
||||
queue_count = 0
|
||||
|
||||
backups.each do |file|
|
||||
file_url = @uri.merge(url_encode(file)).to_s
|
||||
request = browser.forge_request(file_url)
|
||||
|
||||
request.on_complete do |response|
|
||||
if response.body[%r{define}i] and not response.body[%r{<\s?html}i]
|
||||
found << file_url
|
||||
end
|
||||
end
|
||||
|
||||
hydra.queue(request)
|
||||
queue_count += 1
|
||||
|
||||
if queue_count == browser.max_threads
|
||||
hydra.run
|
||||
queue_count = 0
|
||||
end
|
||||
end
|
||||
|
||||
hydra.run
|
||||
|
||||
found
|
||||
end
|
||||
|
||||
# @return [ Array ]
|
||||
def self.config_backup_files
|
||||
%w{
|
||||
wp-config.php~ #wp-config.php# wp-config.php.save .wp-config.php.swp wp-config.php.swp wp-config.php.swo
|
||||
wp-config.php_bak wp-config.bak wp-config.php.bak wp-config.save wp-config.old wp-config.php.old
|
||||
wp-config.php.orig wp-config.orig wp-config.php.original wp-config.original wp-config.txt
|
||||
} # thanks to Feross.org for these
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpCustomDirectories
|
||||
|
||||
# @return [ String ] The wp-content directory
|
||||
def wp_content_dir
|
||||
unless @wp_content_dir
|
||||
index_body = Browser.get(@uri.to_s).body
|
||||
uri_path = @uri.path # Only use the path because domain can be text or an IP
|
||||
|
||||
if index_body[/\/wp-content\/(?:themes|plugins)\//i] || default_wp_content_dir_exists?
|
||||
@wp_content_dir = 'wp-content'
|
||||
else
|
||||
domains_excluded = '(?:www\.)?(facebook|twitter)\.com'
|
||||
@wp_content_dir = index_body[/(?:href|src)\s*=\s*(?:"|').+#{Regexp.escape(uri_path)}((?!#{domains_excluded})[^"']+)\/(?:themes|plugins)\/.*(?:"|')/i, 1]
|
||||
end
|
||||
end
|
||||
|
||||
@wp_content_dir
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def default_wp_content_dir_exists?
|
||||
response = Browser.get(@uri.merge('wp-content').to_s)
|
||||
|
||||
if WpTarget.valid_response_codes.include?(response.code)
|
||||
hash = WebSite.page_hash(response)
|
||||
return true if hash != error_404_hash and hash != homepage_hash
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ] The wp-plugins directory
|
||||
def wp_plugins_dir
|
||||
unless @wp_plugins_dir
|
||||
@wp_plugins_dir = "#{wp_content_dir}/plugins"
|
||||
end
|
||||
@wp_plugins_dir
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def wp_plugins_dir_exists?
|
||||
Browser.get(@uri.merge(wp_plugins_dir).to_s).code != 404
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpFullPathDisclosure
|
||||
# Check for Full Path Disclosure (FPD)
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_full_path_disclosure?
|
||||
Browser.get(full_path_disclosure_url).body[%r/Fatal error/i] ? true : false
|
||||
end
|
||||
|
||||
def full_path_disclosure_data
|
||||
return nil unless has_full_path_disclosure?
|
||||
Browser.get(full_path_disclosure_url).body[/Fatal error:.+? in (.+?) on/i, 1]
|
||||
end
|
||||
|
||||
# @return [ String ]
|
||||
def full_path_disclosure_url
|
||||
@uri.merge('wp-includes/rss-functions.php').to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,110 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpLoginProtection
|
||||
|
||||
LOGIN_PROTECTION_METHOD_PATTERN = /^has_(.*)_protection\?/i
|
||||
# Used as cache
|
||||
@login_protection_plugin = nil
|
||||
|
||||
def has_login_protection?
|
||||
!login_protection_plugin.nil?
|
||||
end
|
||||
|
||||
# Checks if a login protection plugin is enabled
|
||||
# return a WpPlugin object or nil if no one is found
|
||||
def login_protection_plugin
|
||||
unless @login_protection_plugin
|
||||
protected_methods.grep(LOGIN_PROTECTION_METHOD_PATTERN).each do |symbol_to_call|
|
||||
|
||||
if send(symbol_to_call)
|
||||
plugin_name = symbol_to_call[LOGIN_PROTECTION_METHOD_PATTERN, 1].gsub('_', '-')
|
||||
|
||||
return @login_protection_plugin = WpPlugin.new(
|
||||
@uri,
|
||||
name: plugin_name,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir
|
||||
)
|
||||
end
|
||||
end
|
||||
@login_protection_plugin = nil
|
||||
end
|
||||
@login_protection_plugin
|
||||
end
|
||||
|
||||
protected
|
||||
# Thanks to Alip Aswalid for providing this method.
|
||||
# http://wordpress.org/extend/plugins/login-lockdown/
|
||||
def has_login_lockdown_protection?
|
||||
Browser.get(login_url).body =~ %r{Login LockDown}i ? true : false
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/login-lock/
|
||||
def has_login_lock_protection?
|
||||
Browser.get(login_url).body =~ %r{LOGIN LOCK} ? true : false
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/better-wp-security/
|
||||
def has_better_wp_security_protection?
|
||||
Browser.get(better_wp_security_url).code != 404
|
||||
end
|
||||
|
||||
def plugin_url(plugin_name)
|
||||
WpPlugin.new(
|
||||
@uri,
|
||||
name: plugin_name,
|
||||
wp_content_dir: wp_content_dir,
|
||||
wp_plugins_dir: wp_plugins_dir
|
||||
).url
|
||||
end
|
||||
|
||||
def better_wp_security_url
|
||||
plugin_url('better-wp-security/')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/simple-login-lockdown/
|
||||
def has_simple_login_lockdown_protection?
|
||||
Browser.get(simple_login_lockdown_url).code != 404
|
||||
end
|
||||
|
||||
def simple_login_lockdown_url
|
||||
plugin_url('simple-login-lockdown/')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/login-security-solution/
|
||||
def has_login_security_solution_protection?
|
||||
Browser.get(login_security_solution_url).code != 404
|
||||
end
|
||||
|
||||
def login_security_solution_url
|
||||
plugin_url('login-security-solution')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/limit-login-attempts/
|
||||
def has_limit_login_attempts_protection?
|
||||
Browser.get(limit_login_attempts_url).code != 404
|
||||
end
|
||||
|
||||
def limit_login_attempts_url
|
||||
plugin_url('limit-login-attempts')
|
||||
end
|
||||
|
||||
# http://wordpress.org/extend/plugins/bluetrait-event-viewer/
|
||||
def has_bluetrait_event_viewer_protection?
|
||||
Browser.get(bluetrait_event_viewer_url).code != 404
|
||||
end
|
||||
|
||||
def bluetrait_event_viewer_url
|
||||
plugin_url('bluetrait-event-viewer')
|
||||
end
|
||||
|
||||
# https://wordpress.org/plugins/security-protection/
|
||||
def has_security_protection_protection?
|
||||
Nokogiri::HTML(Browser.get(login_url).body).css('script').each do |node|
|
||||
return true if node['src'] =~ /security-protection.js/i
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpMustUsePlugins
|
||||
# Checks to see if the must use plugin folder exists
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_must_use_plugins?
|
||||
response = Browser.get(must_use_url)
|
||||
|
||||
if response && [200, 401, 403].include?(response.code)
|
||||
hash = WebSite.page_hash(response)
|
||||
return true if hash != error_404_hash && hash != homepage_hash
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ] The must use plugins directory URL
|
||||
def must_use_url
|
||||
@uri.merge("#{wp_content_dir}/mu-plugins/").to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,27 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpReadme
|
||||
|
||||
# Checks to see if the readme.html file exists
|
||||
#
|
||||
# This file comes by default in a wordpress installation,
|
||||
# and if deleted is reinstated with an upgrade.
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def has_readme?
|
||||
response = Browser.get(readme_url)
|
||||
|
||||
unless response.code == 404
|
||||
return response.body =~ %r{wordpress}i ? true : false
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
# @return [ String ] The readme URL
|
||||
def readme_url
|
||||
@uri.merge('readme.html').to_s
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpRegistrable
|
||||
|
||||
# Should check wp-login.php if registration is enabled or not
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def registration_enabled?
|
||||
resp = Browser.get(registration_url)
|
||||
# redirect only on non multi sites
|
||||
if resp.code == 302 and resp.headers_hash['location'] =~ /wp-login\.php\?registration=disabled/i
|
||||
enabled = false
|
||||
# multi site registration form
|
||||
elsif resp.code == 200 and resp.body =~ /<form id="setupform" method="post" action="[^"]*wp-signup\.php[^"]*">/i
|
||||
enabled = true
|
||||
# normal registration form
|
||||
elsif resp.code == 200 and resp.body =~ /<form name="registerform" id="registerform" action="[^"]*wp-login\.php[^"]*"/i
|
||||
enabled = true
|
||||
# registration disabled
|
||||
else
|
||||
enabled = false
|
||||
end
|
||||
enabled
|
||||
end
|
||||
|
||||
# @return [ String ] The registration URL
|
||||
def registration_url
|
||||
multisite? ? @uri.merge('wp-signup.php').to_s : @uri.merge('wp-login.php?action=register').to_s
|
||||
end
|
||||
|
||||
# @return [ Boolean ]
|
||||
def multisite?
|
||||
unless @multisite
|
||||
# when multi site, there is no redirection or a redirect to the site itself
|
||||
# otherwise redirect to wp-login.php
|
||||
resp = Browser.get(@uri.merge('wp-signup.php').to_s)
|
||||
|
||||
if resp.code == 302 and resp.headers_hash['location'] =~ /wp-login\.php\?action=register/
|
||||
@multisite = false
|
||||
elsif resp.code == 302 and resp.headers_hash['location'] =~ /wp-signup\.php/
|
||||
@multisite = true
|
||||
elsif resp.code == 200
|
||||
@multisite = true
|
||||
else
|
||||
@multisite = false
|
||||
end
|
||||
end
|
||||
@multisite
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,73 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpTarget < WebSite
|
||||
module WpRSS
|
||||
|
||||
# Checks to see if there is an rss feed
|
||||
# Will try to find the rss url in the homepage
|
||||
# Only the first one found is returned
|
||||
#
|
||||
# This file comes by default in a WordPress installation
|
||||
#
|
||||
# @return [ Boolean ]
|
||||
def rss_url
|
||||
homepage_body = Browser.get(@uri.to_s).body
|
||||
# Format: <link rel="alternate" type="application/rss+xml" title=".*" href=".*" />
|
||||
homepage_body[%r{<link\s*.*\s*type=['|"]application\/rss\+xml['|"]\s*.*\stitle=".*" href=['|"]([^"]+)['|"]\s*\/?>}i, 1]
|
||||
end
|
||||
|
||||
|
||||
# Gets all the authors from the RSS feed
|
||||
#
|
||||
# @return [ string ]
|
||||
def rss_authors(url)
|
||||
# Variables
|
||||
users = []
|
||||
|
||||
# Make the request
|
||||
response = Browser.get(url, followlocation: true)
|
||||
|
||||
# Valid repose to view? HTTP 200?
|
||||
return false unless response.code == 200
|
||||
|
||||
# Get output
|
||||
data = response.body
|
||||
|
||||
# If there is nothing there, return false
|
||||
return false if data.empty?
|
||||
|
||||
begin
|
||||
# Read in RSS/XML
|
||||
xml = Nokogiri::XML(data)
|
||||
rescue
|
||||
puts critical("Missformed XML")
|
||||
return false
|
||||
end
|
||||
|
||||
begin
|
||||
# Look for <dc:creator> item
|
||||
xml.xpath('//item/dc:creator').each do |node|
|
||||
#Format: <dc:creator><![CDATA[.*]]></dc:creator>
|
||||
users << [%r{.*}i.match(node).to_s]
|
||||
end
|
||||
rescue
|
||||
puts critical("Missing Author field. Maybe non-standard WordPress RSS feed?")
|
||||
return false
|
||||
end
|
||||
|
||||
# Sort and uniq
|
||||
users = users.sort_by { |user| user.to_s.downcase }.uniq
|
||||
|
||||
if users and users.size >= 1
|
||||
# Feedback
|
||||
grammar = grammar_s(users.size)
|
||||
puts warning("Detected #{users.size} user#{grammar} from RSS feed:")
|
||||
|
||||
# Print results
|
||||
table = Terminal::Table.new(headings: ['Name'],
|
||||
rows: users)
|
||||
puts table
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,193 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
require File.expand_path(File.join(__dir__, '..', 'common', 'common_helper'))
|
||||
|
||||
require_files_from_directory(WPSCAN_LIB_DIR, '**/*.rb')
|
||||
|
||||
# wpscan usage
|
||||
def usage
|
||||
script_name = $0
|
||||
puts
|
||||
puts 'Examples :'
|
||||
puts
|
||||
puts '-Further help ...'
|
||||
puts "ruby #{script_name} --help"
|
||||
puts
|
||||
puts "-Do 'non-intrusive' checks ..."
|
||||
puts "ruby #{script_name} --url www.example.com"
|
||||
puts
|
||||
puts '-Do wordlist password brute force on enumerated users using 50 threads ...'
|
||||
puts "ruby #{script_name} --url www.example.com --wordlist darkc0de.lst --threads 50"
|
||||
puts
|
||||
puts "-Do wordlist password brute force on the 'admin' username only ..."
|
||||
puts "ruby #{script_name} --url www.example.com --wordlist darkc0de.lst --username admin"
|
||||
puts
|
||||
puts '-Enumerate installed plugins ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate p"
|
||||
puts
|
||||
puts '-Enumerate installed themes ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate t"
|
||||
puts
|
||||
puts '-Enumerate users (from 1 - 10)...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate u"
|
||||
puts
|
||||
puts '-Enumerate users (from 1 - 20)...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate u[1-20]"
|
||||
puts
|
||||
puts '-Enumerate installed timthumbs ...'
|
||||
puts "ruby #{script_name} --url www.example.com --enumerate tt"
|
||||
puts
|
||||
puts '-Use a HTTP proxy ...'
|
||||
puts "ruby #{script_name} --url www.example.com --proxy 127.0.0.1:8118"
|
||||
puts
|
||||
puts '-Use a SOCKS5 proxy ... (cURL >= v7.21.7 needed)'
|
||||
puts "ruby #{script_name} --url www.example.com --proxy socks5://127.0.0.1:9000"
|
||||
puts
|
||||
puts '-Use custom content directory ...'
|
||||
puts "ruby #{script_name} -u www.example.com --wp-content-dir custom-content"
|
||||
puts
|
||||
puts '-Use custom plugins directory ...'
|
||||
puts "ruby #{script_name} -u www.example.com --wp-plugins-dir wp-content/custom-plugins"
|
||||
puts
|
||||
puts '-Update the Database ...'
|
||||
puts "ruby #{script_name} --update"
|
||||
puts
|
||||
puts '-Debug output ...'
|
||||
puts "ruby #{script_name} --url www.example.com --debug-output 2>debug.log"
|
||||
puts
|
||||
puts 'See README for further information.'
|
||||
puts
|
||||
end
|
||||
|
||||
# command help
|
||||
def help
|
||||
puts 'Help :'
|
||||
puts
|
||||
puts 'Some values are settable in a config file, see the example.conf.json'
|
||||
puts
|
||||
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.'
|
||||
puts ' option :'
|
||||
puts ' u usernames from id 1 to 10'
|
||||
puts ' u[10-20] usernames from id 10 to 20 (you must write [] chars)'
|
||||
puts ' p plugins'
|
||||
puts ' vp only vulnerable plugins'
|
||||
puts ' ap all plugins (can take a long time)'
|
||||
puts ' tt timthumbs'
|
||||
puts ' t themes'
|
||||
puts ' vt only vulnerable themes'
|
||||
puts ' at all themes (can take a long time)'
|
||||
puts ' Multiple values are allowed : "-e tt,p" will enumerate timthumbs and plugins'
|
||||
puts ' If no option is supplied, the default is "vt,tt,u,vp"'
|
||||
puts
|
||||
puts '--exclude-content-based "<regexp or string>"'
|
||||
puts ' Used with the enumeration option, will exclude all occurrences based on the regexp or string supplied.'
|
||||
puts ' You do not need to provide the regexp delimiters, but you must write the quotes (simple or double).'
|
||||
puts '--config-file | -c <config file> Use the specified config file, see the example.conf.json.'
|
||||
puts '--user-agent | -a <User-Agent> Use the specified User-Agent.'
|
||||
puts '--cookie <string> String to read cookies from.'
|
||||
puts '--random-agent | -r Use a random User-Agent.'
|
||||
puts '--follow-redirection If the target url has a redirection, it will be followed without asking if you wanted to do so or not'
|
||||
puts '--batch Never ask for user input, use the default behaviour.'
|
||||
puts '--no-color Do not use colors in the output.'
|
||||
puts '--log [filename] Creates a log.txt file with WPScan\'s output if no filename is supplied. Otherwise the filename is used for logging.'
|
||||
puts '--no-banner Prevents the WPScan banner from being displayed.'
|
||||
puts '--disable-accept-header Prevents WPScan sending the Accept HTTP header.'
|
||||
puts '--disable-referer Prevents setting the Referer header.'
|
||||
puts '--disable-tls-checks Disables SSL/TLS certificate verification.'
|
||||
puts '--wp-content-dir <wp content dir> WPScan try to find the content directory (ie wp-content) by scanning the index page, however you can specify it.'
|
||||
puts ' Subdirectories are allowed.'
|
||||
puts '--wp-plugins-dir <wp plugins dir> Same thing than --wp-content-dir but for the plugins directory.'
|
||||
puts ' If not supplied, WPScan will use wp-content-dir/plugins. Subdirectories are allowed'
|
||||
puts '--proxy <[protocol://]host:port> Supply a proxy. HTTP, SOCKS4 SOCKS4A and SOCKS5 are supported.'
|
||||
puts ' If no protocol is given (format host:port), HTTP will be used.'
|
||||
puts '--proxy-auth <username:password> Supply the proxy login credentials.'
|
||||
puts '--basic-auth <username:password> Set the HTTP Basic authentication.'
|
||||
puts '--wordlist | -w <wordlist> Supply a wordlist for the password brute forcer.'
|
||||
puts '--username | -U <username> Only brute force the supplied username.'
|
||||
puts '--usernames <path-to-file> Only brute force the usernames from the file.'
|
||||
puts '--cache-dir <cache-directory> Set the cache directory.'
|
||||
puts '--cache-ttl <cache-ttl> Typhoeus cache TTL.'
|
||||
puts '--request-timeout <request-timeout> Request Timeout.'
|
||||
puts '--connect-timeout <connect-timeout> Connect Timeout.'
|
||||
puts '--threads | -t <number of threads> The number of threads to use when multi-threading requests.'
|
||||
puts '--throttle <milliseconds> Milliseconds to wait before doing another web request. If used, the --threads should be set to 1.'
|
||||
puts '--help | -h This help screen.'
|
||||
puts '--verbose | -v Verbose output.'
|
||||
puts '--version Output the current version and exit.'
|
||||
puts
|
||||
end
|
||||
|
||||
|
||||
def clean_uri(entries)
|
||||
# Extract elements
|
||||
entries.flatten!
|
||||
# Remove any leading/trailing spaces
|
||||
entries.collect{|x| x.strip || x }
|
||||
# End Of Line issues
|
||||
entries.collect{|x| x.chomp! || x }
|
||||
# Remove nil's
|
||||
entries.compact
|
||||
# Unique values only
|
||||
entries.uniq!
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
# Return the full URL
|
||||
def full_uri(entries)
|
||||
return_object = []
|
||||
# Each value now, try and make it a full URL
|
||||
entries.each do |d|
|
||||
begin
|
||||
temp = @uri.clone
|
||||
temp.path = d.strip
|
||||
rescue URI::Error
|
||||
temp = d.strip
|
||||
end
|
||||
return_object << temp.to_s
|
||||
end
|
||||
|
||||
return return_object
|
||||
end
|
||||
|
||||
# Parse humans.txt
|
||||
# @return [ Array ] URLs generated from humans.txt
|
||||
def parse_txt(url)
|
||||
return_object = []
|
||||
response = Browser.get(url.to_s)
|
||||
body = response.body
|
||||
|
||||
# Get all non-comments
|
||||
entries = body.split(/\n/)
|
||||
|
||||
# Did we get something?
|
||||
if entries
|
||||
# Remove any rubbish
|
||||
entries = clean_uri(entries)
|
||||
end
|
||||
return return_object
|
||||
end
|
||||
|
||||
|
||||
# Hook to check if the target if down during the scan
|
||||
# And have the number of requests performed to display at the end of the scan
|
||||
# The target is considered down after 30 requests with status = 0
|
||||
down = 0
|
||||
@total_requests_done = 0
|
||||
|
||||
Typhoeus.on_complete do |response|
|
||||
next if response.cached?
|
||||
|
||||
down += 1 if response.code == 0
|
||||
@total_requests_done += 1
|
||||
|
||||
fail 'The target seems to be down' if down >= 30
|
||||
|
||||
next unless Browser.instance.throttle > 0
|
||||
|
||||
sleep(Browser.instance.throttle)
|
||||
end
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
# encoding: UTF-8
|
||||
|
||||
class WpscanOptions
|
||||
ACCESSOR_OPTIONS = [
|
||||
:batch,
|
||||
:enumerate_plugins,
|
||||
:enumerate_only_vulnerable_plugins,
|
||||
:enumerate_all_plugins,
|
||||
:enumerate_themes,
|
||||
:enumerate_only_vulnerable_themes,
|
||||
:enumerate_all_themes,
|
||||
:enumerate_timthumbs,
|
||||
:enumerate_usernames,
|
||||
:enumerate_usernames_range,
|
||||
:no_color,
|
||||
:log,
|
||||
:proxy,
|
||||
:proxy_auth,
|
||||
:threads,
|
||||
:url,
|
||||
:vhost,
|
||||
:wordlist,
|
||||
:force,
|
||||
:update,
|
||||
:verbose,
|
||||
:username,
|
||||
:usernames,
|
||||
:password,
|
||||
:follow_redirection,
|
||||
:wp_content_dir,
|
||||
:wp_plugins_dir,
|
||||
:help,
|
||||
:config_file,
|
||||
:cookie,
|
||||
:exclude_content_based,
|
||||
:basic_auth,
|
||||
:debug_output,
|
||||
:version,
|
||||
:user_agent,
|
||||
:random_agent,
|
||||
:cache_ttl,
|
||||
:request_timeout,
|
||||
:connect_timeout,
|
||||
:max_threads,
|
||||
:no_banner,
|
||||
:throttle,
|
||||
:disable_accept_header,
|
||||
:disable_referer,
|
||||
:cache_dir,
|
||||
:disable_tls_checks
|
||||
]
|
||||
|
||||
attr_accessor *ACCESSOR_OPTIONS
|
||||
|
||||
def initialize
|
||||
ACCESSOR_OPTIONS.each do |option|
|
||||
instance_variable_set("@#{option}", nil)
|
||||
end
|
||||
end
|
||||
|
||||
def url=(url)
|
||||
raise Exception.new('Empty URL given') if url.nil? || url == ''
|
||||
|
||||
url = Addressable::URI.parse(url).normalize.to_s unless url.ascii_only?
|
||||
|
||||
@url = URI.parse(add_http_protocol(url)).to_s
|
||||
end
|
||||
|
||||
def vhost=(vhost)
|
||||
@vhost = vhost
|
||||
end
|
||||
|
||||
def threads=(threads)
|
||||
@threads = threads.is_a?(Integer) ? threads : threads.to_i
|
||||
end
|
||||
|
||||
def wordlist=(wordlist)
|
||||
if File.exists?(wordlist) || wordlist == '-'
|
||||
@wordlist = wordlist
|
||||
else
|
||||
raise "The file #{wordlist} does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
def usernames=(file)
|
||||
fail "The file #{file} does not exist" unless File.exists?(file)
|
||||
|
||||
@usernames = file
|
||||
end
|
||||
|
||||
def proxy=(proxy)
|
||||
if proxy.index(':') == nil
|
||||
raise 'Invalid proxy format. Should be host:port.'
|
||||
else
|
||||
@proxy = proxy
|
||||
end
|
||||
end
|
||||
|
||||
def proxy_auth=(auth)
|
||||
if auth.index(':') == nil
|
||||
raise 'Invalid proxy auth format, username:password expected'
|
||||
else
|
||||
@proxy_auth = auth
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_plugins=(enumerate_plugins)
|
||||
if enumerate_plugins === true and (@enumerate_all_plugins === true or @enumerate_only_vulnerable_plugins === true)
|
||||
raise 'Please choose only one plugin enumeration option'
|
||||
else
|
||||
@enumerate_plugins = enumerate_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_only_vulnerable_plugins=(enumerate_only_vulnerable_plugins)
|
||||
if enumerate_only_vulnerable_plugins === true and (@enumerate_all_plugins === true or @enumerate_plugins === true)
|
||||
raise 'Please choose only one plugin enumeration option'
|
||||
else
|
||||
@enumerate_only_vulnerable_plugins = enumerate_only_vulnerable_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_all_plugins=(enumerate_all_plugins)
|
||||
if enumerate_all_plugins === true and (@enumerate_plugins === true or @enumerate_only_vulnerable_plugins === true)
|
||||
raise 'Please choose only one plugin enumeration option'
|
||||
else
|
||||
@enumerate_all_plugins = enumerate_all_plugins
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_themes=(enumerate_themes)
|
||||
if enumerate_themes === true and (@enumerate_all_themes === true or @enumerate_only_vulnerable_themes === true)
|
||||
raise 'Please choose only one theme enumeration option'
|
||||
else
|
||||
@enumerate_themes = enumerate_themes
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_only_vulnerable_themes=(enumerate_only_vulnerable_themes)
|
||||
if enumerate_only_vulnerable_themes === true and (@enumerate_all_themes === true or @enumerate_themes === true)
|
||||
raise 'Please choose only one theme enumeration option'
|
||||
else
|
||||
@enumerate_only_vulnerable_themes = enumerate_only_vulnerable_themes
|
||||
end
|
||||
end
|
||||
|
||||
def enumerate_all_themes=(enumerate_all_themes)
|
||||
if enumerate_all_themes === true and (@enumerate_themes === true or @enumerate_only_vulnerable_themes === true)
|
||||
raise 'Please choose only one theme enumeration option'
|
||||
else
|
||||
@enumerate_all_themes = enumerate_all_themes
|
||||
end
|
||||
end
|
||||
|
||||
def debug_output=(debug_output)
|
||||
Typhoeus::Config.verbose = debug_output
|
||||
end
|
||||
|
||||
def has_options?
|
||||
!to_h.empty?
|
||||
end
|
||||
|
||||
def random_agent=(useless)
|
||||
@user_agent = get_random_user_agent
|
||||
end
|
||||
|
||||
# return Hash
|
||||
def to_h
|
||||
options = {}
|
||||
|
||||
ACCESSOR_OPTIONS.each do |option|
|
||||
instance_variable = instance_variable_get("@#{option}")
|
||||
|
||||
unless instance_variable.nil?
|
||||
options[:"#{option}"] = instance_variable
|
||||
end
|
||||
end
|
||||
options
|
||||
end
|
||||
|
||||
# Will load the options from ARGV
|
||||
# return WpscanOptions
|
||||
def self.load_from_arguments
|
||||
wpscan_options = WpscanOptions.new
|
||||
|
||||
if ARGV.length > 0
|
||||
WpscanOptions.get_opt_long.each do |opt, arg|
|
||||
wpscan_options.set_option_from_cli(opt, arg)
|
||||
end
|
||||
end
|
||||
|
||||
wpscan_options
|
||||
end
|
||||
|
||||
# string cli_option : --url, -u, --proxy etc
|
||||
# string cli_value : the option value
|
||||
def set_option_from_cli(cli_option, cli_value)
|
||||
|
||||
if WpscanOptions.is_long_option?(cli_option)
|
||||
self.send(
|
||||
WpscanOptions.option_to_instance_variable_setter(cli_option),
|
||||
cli_value
|
||||
)
|
||||
elsif cli_option === '--enumerate' # Special cases
|
||||
# Default value if no argument is given
|
||||
cli_value = 'vt,tt,u,vp' if cli_value.length == 0
|
||||
|
||||
enumerate_options_from_string(cli_value)
|
||||
else
|
||||
text = "Unknown option : #{cli_option}"
|
||||
text << " with value #{cli_value}" if (cli_value && !cli_value.empty?)
|
||||
raise text
|
||||
end
|
||||
end
|
||||
|
||||
# Will set enumerate_* from the string value
|
||||
# IE : if value = vp => :enumerate_only_vulnerable_plugins will be set to true
|
||||
# multiple enumeration are possible : 'u,p' => :enumerate_usernames and :enumerate_plugins
|
||||
# Special case for usernames, a range is possible : u[1-10] will enumerate usernames from 1 to 10
|
||||
def enumerate_options_from_string(value)
|
||||
# Usage of self is mandatory because there are overridden setters
|
||||
|
||||
value = value.split(',').map { |c| c.downcase }
|
||||
|
||||
self.enumerate_only_vulnerable_plugins = true if value.include?('vp')
|
||||
|
||||
self.enumerate_plugins = true if value.include?('p')
|
||||
|
||||
self.enumerate_all_plugins = true if value.include?('ap')
|
||||
|
||||
@enumerate_timthumbs = true if value.include?('tt')
|
||||
|
||||
self.enumerate_only_vulnerable_themes = true if value.include?('vt')
|
||||
|
||||
self.enumerate_themes = true if value.include?('t')
|
||||
|
||||
self.enumerate_all_themes = true if value.include?('at')
|
||||
|
||||
value.grep(/^u/) do |username_enum_value|
|
||||
@enumerate_usernames = true
|
||||
# Check for usernames range
|
||||
matches = %r{\[([\d]+)-([\d]+)\]}.match(username_enum_value)
|
||||
if matches
|
||||
@enumerate_usernames_range = (matches[1].to_i..matches[2].to_i)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
protected
|
||||
# Even if a short option is given (IE : -u), the long one will be returned (IE : --url)
|
||||
def self.get_opt_long
|
||||
GetoptLong.new(
|
||||
['--url', '-u', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--vhost',GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--enumerate', '-e', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--username', '-U', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--usernames', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--wordlist', '-w', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--threads', '-t', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--force', '-f', GetoptLong::NO_ARGUMENT],
|
||||
['--user-agent', '-a', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--random-agent', '-r', GetoptLong::NO_ARGUMENT],
|
||||
['--help', '-h', GetoptLong::NO_ARGUMENT],
|
||||
['--verbose', '-v', GetoptLong::NO_ARGUMENT],
|
||||
['--proxy', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--proxy-auth', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--update', GetoptLong::NO_ARGUMENT],
|
||||
['--follow-redirection', GetoptLong::NO_ARGUMENT],
|
||||
['--wp-content-dir', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--wp-plugins-dir', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--config-file', '-c', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--exclude-content-based', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--basic-auth', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--debug-output', GetoptLong::NO_ARGUMENT],
|
||||
['--version', GetoptLong::NO_ARGUMENT],
|
||||
['--cache-ttl', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--request-timeout', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--connect-timeout', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--batch', GetoptLong::NO_ARGUMENT],
|
||||
['--no-color', GetoptLong::NO_ARGUMENT],
|
||||
['--cookie', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--log', GetoptLong::OPTIONAL_ARGUMENT],
|
||||
['--no-banner', GetoptLong::NO_ARGUMENT],
|
||||
['--throttle', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--disable-accept-header', GetoptLong::NO_ARGUMENT],
|
||||
['--disable-referer', GetoptLong::NO_ARGUMENT],
|
||||
['--cache-dir', GetoptLong::REQUIRED_ARGUMENT],
|
||||
['--disable-tls-checks', GetoptLong::NO_ARGUMENT],
|
||||
)
|
||||
end
|
||||
|
||||
def self.is_long_option?(option)
|
||||
ACCESSOR_OPTIONS.include?(:"#{WpscanOptions.clean_option(option)}")
|
||||
end
|
||||
|
||||
# Will removed the '-' or '--' chars at the beginning of option
|
||||
# and replace any remaining '-' by '_'
|
||||
#
|
||||
# param string option
|
||||
# return string
|
||||
def self.clean_option(option)
|
||||
cleaned_option = option.gsub(/^--?/, '')
|
||||
cleaned_option.gsub(/-/, '_')
|
||||
end
|
||||
|
||||
def self.option_to_instance_variable_setter(option)
|
||||
cleaned_option = WpscanOptions.clean_option(option)
|
||||
option_syms = ACCESSOR_OPTIONS.grep(%r{^#{cleaned_option}$})
|
||||
|
||||
option_syms.length == 1 ? :"#{option_syms.at(0)}=" : nil
|
||||
end
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user