New enumeration system

This commit is contained in:
erwanlr
2013-03-19 22:59:20 +01:00
parent 634a6222f7
commit d016d33747
79 changed files with 3798 additions and 6388 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,8 @@
# encoding: UTF-8
require 'common/collections/vulnerabilities/output'
class Vulnerabilities < Array
include Vulnerabilities::Output
end

View File

@@ -0,0 +1,13 @@
# encoding: UTF-8
class Vulnerabilities < Array
module Output
def output
self.each do |v|
v.output
end
end
end
end

View File

@@ -0,0 +1,10 @@
# encoding: UTF-8
require 'common/collections/wp_items/detectable'
require 'common/collections/wp_items/output'
class WpItems < Array
extend WpItems::Detectable
include WpItems::Output
end

View File

@@ -0,0 +1,154 @@
# encoding: UTF-8
class WpItems < Array
module Detectable
# The default request parameters
def request_params; { cache_ttl: 0, followlocation: true } end
# options:
# option name - default - description
# show_progress - false - Output a progress bar
# only_vulnerable - nil - Only check for vulnerable items
# exclude_content - nil -
def aggressive_detection(wp_target, options = {})
queue_count = 0
request_count = 0
browser = Browser.instance
hydra = browser.hydra
targets = targets_items(wp_target, options)
targets_size = targets.size
show_progression = options[:show_progression] || false
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
}
# If we only want the vulnerable ones, the passive detection is ignored
# Otherwise, a passive detection is performed, and results will be merged
results = options[:only_vulnerable] ? new : passive_detection(wp_target, options)
targets.each do |target_item|
request = browser.forge_request(target_item.url, request_params)
request_count += 1
request.on_complete do |response|
print "\rChecking for #{targets_size} total ... #{(request_count * 100) / targets_size}% complete." if show_progression
if target_item.exists?(exist_options, response)
if !results.include?(target_item)
results << target_item
end
end
end
hydra.queue(request)
queue_count += 1
if queue_count == browser.max_threads
hydra.run
queue_count = 0
end
end
hydra.run
results.sort!
results # can't just return results.sort because the #sort returns an array, and we want a WpItems
end
def passive_detection(wp_target, options = {})
results = new
item_class = self.item_class
type = self.to_s.gsub(/Wp/, '').downcase
response = Browser.instance.get(wp_target.url)
item_options = {
wp_content_dir: wp_target.wp_content_dir,
wp_plugins_dir: wp_target.wp_plugins_dir,
vulns_file: vulns_file
}
regex1 = %r{(?:[^=:]+)\s?(?:=|:)\s?(?:"|')[^"']+\\?/}
regex2 = %r{\\?/}
regex3 = %r{\\?/([^/\\"']+)\\?(?:/|"|')}
names = response.body.scan(/#{regex1}#{Regexp.escape(wp_target.wp_content_dir)}#{regex2}#{Regexp.escape(type)}#{regex3}/i)
names.flatten.uniq.each do |name|
results << item_class.new(wp_target.uri, item_options.merge(name: name))
end
results.sort!
results
end
protected
def targets_items(wp_target, options = {})
item_class = self.item_class
vulns_file = self.vulns_file
targets = vulnerable_targets_items(wp_target, item_class, vulns_file)
unless options[:only_vulnerable]
unless options[:file]
raise 'A file must be supplied'
end
targets += targets_items_from_file(options[:file], wp_target, item_class, vulns_file)
end
targets.uniq! { |t| t.name }
targets.sort_by { rand }
end
def vulnerable_targets_items(wp_target, item_class, vulns_file)
targets = []
xml = xml(vulns_file)
xml.xpath(item_xpath).each do |node|
targets << create_item(
item_class,
node.attribute('name').text,
wp_target,
vulns_file
)
end
targets
end
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
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

View File

@@ -0,0 +1,11 @@
# encoding: UTF-8
class WpItems < Array
module Output
def output
self.each { |item| item.output }
end
end
end

View File

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

View File

@@ -0,0 +1,18 @@
# encoding: UTF-8
class WpPlugins < WpItems
module Detectable
def vulns_file
unless @vulns_file
@vulns_file = PLUGINS_VULNS_FILE
end
@vulns_file
end
def item_xpath
'//plugin'
end
end
end

View File

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

View File

@@ -0,0 +1,18 @@
# encoding: UTF-8
class WpThemes < WpItems
module Detectable
def vulns_file
unless @vulns_file
@vulns_file = THEMES_VULNS_FILE
end
@vulns_file
end
def item_xpath
'//theme'
end
end
end

View File

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

View File

@@ -0,0 +1,56 @@
# encoding: UTF-8
class WpTimthumbs < WpItems
module Detectable
# No passive detection
# @return [ WpTimthumbs ]
def passive_detection(wp_target, topns = {})
new
end
def targets_items(wp_target, options = {})
unless options[:file]
raise 'A file must be supplied'
end
targets = options[:theme_name] ? theme_timthumbs(options[:theme_name], wp_target) : []
File.open(options[:file], 'r') do |f|
f.readlines.collect do |path|
targets << create_item(wp_target, path.strip)
end
end
targets.uniq { |i| i.url }
end
# @return [ WpTimthumb Array ]
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
}.each do |path|
wp_timthumb.path = "$wp-content$/themes/#{theme_name}/#{path}"
targets << wp_timthumb.dup
end
targets
end
# @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

View File

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

View File

@@ -0,0 +1,27 @@
# encoding: UTF-8
class WpUsers < WpItems
module Detectable
def request_params; {} end
# options:
# :range - default 1..10
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
# No passive detection
# @return [ WpUsers ]
def passive_detection(wp_target, options = {})
new
end
end
end

View File

@@ -0,0 +1,29 @@
# encoding: UTF-8
class WpUsers < WpItems
module Output
# TODO : create a generic method to output tabs
def output(left_margin = '')
max_id_length = self.sort { |a, b| a.id.to_s.length <=> b.id.to_s.length }.last.id.to_s.length
max_login_length = self.sort { |a, b| a.login.length <=> b.login.length }.last.login.length
max_display_name_length = self.sort { |a, b| a.display_name.length <=> b.display_name.length }.last.display_name.length
inner_space = 2
id_length = (max_id_length + inner_space * 2) /2 *2
login_length = max_login_length + inner_space * 2
display_name_length = max_display_name_length + inner_space * 2
puts left_margin + '+' * (id_length + login_length + display_name_length + 4)
puts left_margin + '|' + 'id'.center(id_length) + '|' + 'login'.center(login_length) + '|' + 'display name'.center(display_name_length) + '|'
puts left_margin + '|' + '+' * (id_length + login_length + display_name_length + 2) + '|'
self.each do |u|
puts left_margin + '|' + u.id.to_s.center(id_length) + '|' + u.login.center(login_length) + '|' + u.display_name.center(display_name_length) + '|'
end
puts left_margin + '+' * (id_length + login_length + display_name_length + 4)
end
end
end

View File

@@ -26,7 +26,11 @@ WPSCAN_LIB_DIR = LIB_DIR + '/wpscan'
WPSTOOLS_LIB_DIR = LIB_DIR + '/wpstools'
UPDATER_LIB_DIR = LIB_DIR + '/updater'
COMMON_LIB_DIR = LIB_DIR + '/common'
MODELS_LIB_DIR = COMMON_LIB_DIR + '/models'
COLLECTIONS_LIB_DIR = COMMON_LIB_DIR + '/collections'
LOG_FILE = ROOT_DIR + '/log.txt'
# Plugins directories
COMMON_PLUGINS_DIR = COMMON_LIB_DIR + '/plugins'
WPSCAN_PLUGINS_DIR = WPSCAN_LIB_DIR + '/plugins' # Not used ATM
@@ -49,6 +53,7 @@ LOCAL_FILES_XSD = DATA_DIR + '/local_vulnerable_files.xsd'
WPSCAN_VERSION = '2.1'
$LOAD_PATH.unshift(LIB_DIR)
$LOAD_PATH.unshift(MODELS_LIB_DIR)
require 'environment'
@@ -75,31 +80,6 @@ def add_trailing_slash(url)
url =~ /\/$/ ? url : "#{url}/"
end
# Gets the string all elements in stringarray ends with
def get_equal_string_end(stringarray = [''])
already_found = ''
looping = true
counter = -1
if stringarray.kind_of? Array and stringarray.length > 1
base = stringarray[0]
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
# loading the updater
require_files_from_directory(UPDATER_LIB_DIR)
@updater = UpdaterFactory.get_updater(ROOT_DIR)
@@ -138,12 +118,6 @@ def green(text)
colorize(text, 32)
end
def get_metasploit_url(module_path)
# remove leading slash
module_path = module_path.sub(/^\//, '')
"http://www.metasploit.com/modules/#{module_path}"
end
def xml(file)
Nokogiri::XML(File.open(file)) do |config|
config.noblanks

View File

@@ -0,0 +1,26 @@
# encoding: UTF-8
require 'vulnerability/output'
class Vulnerability
include Vulnerability::Output
attr_accessor :title, :references, :type, :metasploit_modules
def initialize(title, type, references, metasploit_modules = [])
@title = title
@type = type
@references = references
@metasploit_modules = metasploit_modules
end
def self.load_from_xml_node(xml_node)
new(
xml_node.search('title').text,
xml_node.search('type').text,
xml_node.search('reference').map(&:text),
xml_node.search('metasploit').map(&:text)
)
end
end

View File

@@ -0,0 +1,25 @@
# encoding: UTF-8
class Vulnerability
module Output
# output the vulnerability
def output
puts ' |'
puts ' | ' + red("* Title: #{title}")
references.each do |r|
puts ' | ' + red("* Reference: #{r}")
end
metasploit_modules.each do |m|
puts ' | ' + red("* Metasploit module: #{metasploit_module_url(m)}")
end
end
def self.metasploit_module_url(module_path)
# remove leading slash
module_path = module_path.sub(/^\//, '')
"http://www.metasploit.com/modules/#{module_path}"
end
end
end

80
lib/common/models/wp_item.rb Executable file
View File

@@ -0,0 +1,80 @@
# 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
def allowed_options
[:name, :wp_content_dir, :wp_plugins_dir, :path, :version, :vulns_file]
end
# options :
# See allowed_options
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 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
def forge_uri(target_base_uri)
@uri = target_base_uri
end
def uri
return path ? @uri.merge(path) : @uri
end
def url; uri.to_s end
def path=(path)
@path = URI.encode(
path.gsub(/\$wp-plugins\$/i, wp_plugins_dir).gsub(/\$wp-content\$/i, wp_content_dir)
)
end
def <=>(other)
name <=> other.name
end
def ==(other)
name === other.name
end
def ===(other)
self == other && version === other.version
end
end

View File

@@ -0,0 +1,55 @@
# encoding: UTF-8
# HACK
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 = Digest::MD5.hexdigest(self.body)
body_hash != error_404_hash && body_hash != homepage_hash
end
end
end
class WpItem
module Existable
def exists?(options = {}, response = nil)
unless response
response = Browser.instance.get(url)
end
exists_from_response?(response, options)
end
protected
# options:
# :error_404_hash
# :homepage_hash
# :exclude_content REGEXP
#
# @return [ Boolean ]
def exists_from_response?(response, options = {})
# FIXME : The response is supposed to follow locations, so we should not have 301 or 302.
# However, due to an issue with Typhoeus or Webmock, the location is not followed in specs
if [200, 301, 302, 401, 403].include?(response.code)
if response.has_valid_hash?(options[:error_404_hash], options[:homepage_hash])
if options[:exclude_content]
unless response.body.match(options[:exclude_content])
return true
end
else
return true
end
end
end
false
end
end
end

View File

@@ -0,0 +1,15 @@
# encoding: UTF-8
class WpItem
attr_reader :found_from
#def allowed_options; super << :found_from end
def found_from=(method)
@found_from = method[%r{find_from_(.*)}, 1].gsub('_', ' ')
end
module Findable
end
end

View File

@@ -0,0 +1,58 @@
# encoding: UTF-8
class WpItem
module Infos
# @return [ Boolean ]
def has_readme?
Browser.instance.get(readme_url).code == 200 ? true : false
end
# @return [ String ]
def readme_url
@uri.merge('readme.txt').to_s
end
# @return [ String ]
def wordpress_url
end
def wordpress_org_item?
end
# @return [ Boolean ]
def has_changelog?
Browser.instance.get(changelog_url).code == 200 ? true : false
end
# @return [ String ]
def changelog_url
@uri.merge('changelog.txt').to_s
end
# @return [ Boolean ]
def has_directory_listing?
Browser.instance.get(@uri.to_s).body[%r{<title>Index of}] ? true : false
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?
response_body = Browser.instance.get(error_log_url, headers: {'range' => 'bytes=0-700'}).body
response_body[%r{PHP Fatal error}i] ? true : false
end
# @return [ String ]
def error_log_url
@uri.merge('error_log').to_s
end
end
end

View File

@@ -0,0 +1,24 @@
# encoding: UTF-8
class WpItem
module Output
# @return [ Void ]
def output
puts
puts " | Name: #{self}" #this will also output the version number if detected
puts " | Location: #{url}"
#puts " | WordPress: #{wordpress_url}" if wordpress_org_item?
puts ' | Directory listing enabled: Yes' if has_directory_listing?
puts " | Readme: #{readme_url}" if has_readme?
puts " | Changelog: #{changelog_url}" if has_changelog?
vulnerabilities.output
if has_error_log?
puts ' | ' + red('[!]') + " An error_log file has been found : #{error_log_url}"
end
end
end
end

View File

@@ -0,0 +1,25 @@
# encoding: UTF-8
class WpItem
attr_writer :version
#def allowed_options; super << :version end
module Versionable
# Get the version from the readme.txt
def version
unless @version
response = Browser.instance.get(readme_url)
@version = response.body[%r{stable tag: #{WpVersion.version_pattern}}i, 1]
end
@version
end
def to_s
item_version = self.version
"#@name#{' v' + item_version.strip if item_version}"
end
end
end

View File

@@ -0,0 +1,26 @@
# encoding: UTF-8
class WpItem
# moved this into the module ?
def vulns_file=(file)
if File.exists?(file)
@vulns_file = file
else
raise "The file #{file} does not exist"
end
end
module Vulnerable
# @return [ Vulnerabilities ]
def vulnerabilities
xml = xml(vulns_file)
vulnerabilities = Vulnerabilities.new
xml.xpath(vulns_xpath).each do |node|
vulnerabilities << Vulnerability.load_from_xml_node(node)
end
vulnerabilities
end
end
end

10
lib/common/models/wp_plugin.rb Executable file
View File

@@ -0,0 +1,10 @@
# encoding: UTF-8
class WpPlugin < WpItem
include WpPlugin::Vulnerable
def forge_uri(target_base_uri)
@uri = target_base_uri.merge(URI.encode(wp_plugins_dir) + '/' + URI.encode(name) + '/')
end
end

View File

@@ -0,0 +1,20 @@
# encoding: UTF-8
class WpPlugin < WpItem
def vulns_file
unless @vulns_file
@vulns_file = PLUGINS_VULNS_FILE
end
@vulns_file
end
def vulns_xpath
"//plugin[@name='#{@name}']/vulnerability"
end
module Vulnerable
end
end

26
lib/common/models/wp_theme.rb Executable file
View File

@@ -0,0 +1,26 @@
# encoding: UTF-8
require 'wp_theme/findable'
require 'wp_theme/versionable'
class WpTheme < WpItem
extend WpTheme::Findable
include WpTheme::Versionable
include WpTheme::Vulnerable
attr_writer :style_url
def allowed_options; super << :style_url end
def forge_uri(target_base_uri)
@uri = target_base_uri.merge(URI.encode(wp_content_dir + '/themes/' + name + '/')) # make suer that this last / is present (spec)
end
def style_url
unless @style_url
@style_url = uri.merge('style.css').to_s
end
@style_url
end
end

View File

@@ -0,0 +1,60 @@
# encoding: UTF-8
class WpTheme < WpItem
module Findable
# Find the main theme of the blog
# returns a WpTheme object or nil
def find(target_uri)
methods.grep(/find_from_/).each do |method|
if wp_theme = self.send(method, target_uri)
wp_theme.found_from = method
return wp_theme
end
end
end
protected
# Discover the wordpress theme name by parsing the css link rel
def find_from_css_link(target_uri)
response = Browser.instance.get_and_follow_location(target_uri.to_s)
# https + domain is optional because of relative links
matches = %r{(?:https?://[^"']+)?/([^/]+)/themes/([^"']+)/style.css}i.match(response.body)
if matches
return new(
target_uri,
{
name: matches[2],
style_url: matches[0],
wp_content_dir: matches[1]
}
)
end
end
# http://code.google.com/p/wpscan/issues/detail?id=141
def find_from_wooframework(target_uri)
body = Browser.instance.get(target_uri.to_s).body
regexp = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?" />\s+<meta name="generator" content="WooFramework\s?([^"]+)?" />}
matches = regexp.match(body)
if matches
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
#path: woo_theme_name
}
)
end
end
end
end

View File

@@ -0,0 +1,19 @@
# encoding: UTF-8
class WpTheme < WpItem
module Versionable
def version
unless @version
@version = Browser.instance.get(style_url).body[%r{Version:\s([^\s]+)}i, 1]
# Get Version from readme.txt
unless @version
@version = super
end
end
@version
end
end
end

View File

@@ -0,0 +1,20 @@
# encoding: UTF-8
class WpTheme < WpItem
def vulns_file
unless @vulns_file
@vulns_file = THEMES_VULNS_FILE
end
@vulns_file
end
def vulns_xpath
"//theme[@name='#{@name}']/vulnerability"
end
module Vulnerable
end
end

View File

@@ -0,0 +1,12 @@
# encoding: UTF-8
require 'wp_timthumb/versionable'
require 'wp_timthumb/existable'
require 'wp_timthumb/output'
class WpTimthumb < WpItem
include WpTimthumb::Versionable
include WpTimthumb::Existable
include WpTimthumb::Output
end

View File

@@ -0,0 +1,11 @@
# encoding: UTF-8
class WpTimthumb < WpItem
module Existable
def exists_from_response?(response, options = {})
response.code == 400 && response.body =~ /no image specified/i ? true : false
end
end
end

View File

@@ -0,0 +1,11 @@
# encoding: UTF-8
class WpTimthumb < WpItem
module Output
def output
puts ' | ' + red('[!]') + " #{url}"
end
end
end

View File

@@ -0,0 +1,13 @@
# 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
def version
response = Browser.instance.get(url)
response.body[%r{TimThumb version\s*: ([^<]+)} , 1]
end
end
end

33
lib/common/models/wp_user.rb Executable file
View File

@@ -0,0 +1,33 @@
# encoding: UTF-8
require 'wp_user/existable'
class WpUser < WpItem
include WpUser::Existable
attr_accessor :id, :login, :display_name, :password
def allowed_options; [:id, :login, :display_name, :password] end
def uri
if id
return @uri.merge("?author=#{id}")
else
raise 'The id is nil'
end
end
def <=>(other)
id <=> other.id
end
def ==(other)
self === (other)
end
def ===(other)
id === other.id && login === other.login
end
end

View File

@@ -0,0 +1,51 @@
# encoding: UTF-8
class WpUser < WpItem
module Existable
def exists_from_response?(response, options = {})
load_login_from_response(response)
@login ? true : false
end
def load_login_from_response(response)
if response.code == 301 # login in location?
location = response.headers_hash['Location']
@login = WpUser::Existable.login_from_author_pattern(location)
@display_name = WpUser::Existable.display_name_from_body(
Browser.instance.get(location).body
)
elsif response.code == 200 # login in body?
@login = WpUser::Existable.login_from_body(response.body)
@display_name = WpUser::Existable.display_name_from_body(response.body)
end
end
def self.login_from_author_pattern(text)
text[%r{/author/([^/\b]+)/?}i, 1]
end
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]+) author-(\d+)}i, 1]
end
login
end
def self.display_name_from_body(body)
if title_tag = body[%r{<title>([^<]+)</title>}i, 1]
title_tag.sub!('&#124;', '|')
return title_tag[%r{([^|]+) }, 1]
end
end
end
end

32
lib/common/models/wp_version.rb Executable file
View File

@@ -0,0 +1,32 @@
# encoding: UTF-8
require 'wp_version/findable'
require 'wp_version/vulnerable'
require 'wp_version/output'
class WpVersion < WpItem
extend WpVersion::Findable
include WpVersion::Vulnerable
include WpVersion::Output
@@version_xml =
# The version number
attr_accessor :number
def allowed_options; super << :number << :found_from end
def self.version_xml
@@version_xml
end
def self.version_xml=(xml)
if File.exists?(xml)
@@version_xml = xml
else
raise "The file #{xml} does not exist"
end
end
end

View File

@@ -0,0 +1,162 @@
# encoding: UTF-8
class WpVersion < WpItem
module Findable
# Find the version of the wp_target blog
# returns a WpVersion object or nil
def find(target_uri, wp_content_dir, wp_plugins_dir)
methods.grep(/find_from_/).each do |method|
if version = send(method, target_uri, wp_content_dir, wp_plugins_dir)
return new(target_uri, number: version, found_from: method)
end
end
end
# Returns the first match of <pattern> in the body of the url
def scan_url(target_uri, pattern, path = nil)
url = path ? target_uri.merge(path).to_s : target_uri.to_s
response = Browser.instance.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.
def find_from_meta_generator(target_uri, wp_content_dir, wp_plugins_dir)
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.
def find_from_rss_generator(target_uri, wp_content_dir, wp_plugins_dir)
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.
def find_from_rdf_generator(target_uri, wp_content_dir, wp_plugins_dir)
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 RSS2 feed source.
#
# Have not been able to find an example of this - Ryan
#def find_from_rss2_generator(target_uri, wp_content_dir, wp_plugins_dir)
# scan_url(
# target_uri,
# %r{<generator>http://wordpress.org/?v=(#{WpVersion.version_pattern})</generator>}i,
# 'feed/rss/'
# )
#end
# Attempts to find the WordPress version from,
# the generator tag in the Atom source.
def find_from_atom_generator(target_uri, wp_content_dir, wp_plugins_dir)
scan_url(
target_uri,
%r{<generator uri="http://wordpress.org/" version="#{version_pattern}">WordPress</generator>}i,
'feed/atom/'
)
end
# Attempts to find the WordPress version from,
# the generator tag in the comment rss source.
#
# Have not been able to find an example of this - Ryan
#def find_from_comments_rss_generator(target_uri, wp_content_dir, wp_plugins_dir)
# scan_url(
# target_uri,
# %r{<!-- generator="WordPress/#{WpVersion.version_pattern}" -->}i,
# 'comments/feed/'
# )
#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)
#
def find_from_advanced_fingerprinting(target_uri, wp_content_dir, wp_plugins_dir)
xml = xml(version_xml)
# This wp_item will take care of encoding the path
# and replace variables like $wp-content$ and $wp-plugins$
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.instance.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.
def find_from_readme(target_uri, wp_content_dir, wp_plugins_dir)
scan_url(
target_uri,
%r{<br />\sversion #{version_pattern}}i,
'readme.html'
)
end
# Attempts to find the WordPress version from the sitemap.xml file.
#
# See: http://code.google.com/p/wpscan/issues/detail?id=109
def find_from_sitemap_generator(target_uri, wp_content_dir, wp_plugins_dir)
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.
def find_from_links_opml(target_uri, wp_content_dir, wp_plugins_dir)
scan_url(
target_uri,
%r{generator="wordpress/#{version_pattern}"}i,
'wp-links-opml.php'
)
end
# Used to check if the version is correct: must contain at least one dot.
def version_pattern
'([^\r\n"\']+\.[^\r\n"\']+)'
end
end
end

View File

@@ -0,0 +1,20 @@
# encoding: UTF-8
class WpVersion < WpItem
module Output
def output
puts green('[+]') + " WordPress version #{self.number} identified from #{self.found_from}"
vulnerabilities = self.vulnerabilities
unless vulnerabilities.empty?
puts
puts red('[!]') + " We have identified #{vulnerabilities.size} vulnerabilities from the version number :"
vulnerabilities.output
end
end
end
end

View File

@@ -0,0 +1,19 @@
# encoding: UTF-8
class WpVersion < WpItem
def vulns_file
unless @vulns_file
@vulns_file = WP_VULNS_FILE
end
@vulns_file
end
def vulns_xpath
"//wordpress[@version='#{@number}']/vulnerability"
end
module Vulnerable
end
end

View File

@@ -27,7 +27,6 @@ end
begin
# Standard libs
require 'rubygems'
require 'bundler/setup'
require 'getoptlong'
require 'optparse' # Will replace getoptlong

View File

@@ -19,18 +19,18 @@
module BruteForce
# param array of string logins
# param array of WpUsers wp_users
# param string wordlist_path
# param hash options
# boolean :show_progression If true, will output the details (Sucess, error etc)
def brute_force(logins, wordlist_path, options = {})
def brute_force(wp_users, wordlist_path, options = {})
hydra = Browser.instance.hydra
number_of_passwords = BruteForce.lines_in_file(wordlist_path)
login_url = login_url()
found = []
show_progression = options[:show_progression] || false
logins.each do |login|
wp_users.each do |wp_user|
queue_count = 0
request_count = 0
password_found = false
@@ -46,14 +46,14 @@ module BruteForce
queue_count += 1
# create local vars for on_complete call back, Issue 51.
username = login.name != 'empty' ? login.name : login.nickname # Issue #66
login = wp_user.login
password = password
# the request object
request = Browser.instance.forge_request(login_url,
{
method: :post,
body: { log: URI::encode(username), pwd: URI::encode(password) },
body: { log: URI::encode(login), pwd: URI::encode(password) },
cache_ttl: 0
}
)
@@ -61,13 +61,13 @@ module BruteForce
# tell hydra what to do when the request completes
request.on_complete do |response|
puts "\n Trying Username : #{username} Password : #{password}" if @verbose
puts "\n Trying Username : #{login} Password : #{password}" if @verbose
if response.body =~ /login_error/i
puts "\nIncorrect username and/or password." if @verbose
puts "\nIncorrect login and/or password." if @verbose
elsif response.code == 302
puts "\n " + green('[SUCCESS]') + " Username : #{username} Password : #{password}\n" if show_progression
found << { name: username, password: password }
puts "\n " + green('[SUCCESS]') + " Login : #{login} Password : #{password}\n" if show_progression
found << { name: login, password: password }
password_found = true
elsif response.timed_out?
puts red('ERROR:') + ' Request timed out.' if show_progression
@@ -86,14 +86,14 @@ module BruteForce
end
end
# move onto the next username if we have found a valid password
# move onto the next login if we have found a valid password
break if password_found
# queue the request to be sent later
hydra.queue(request)
# progress indicator
print "\r Brute forcing user '#{username}' with #{number_of_passwords} passwords... #{(request_count * 100) / number_of_passwords}% complete." if show_progression
print "\r Brute forcing user '#{login}' with #{number_of_passwords} passwords... #{(request_count * 100) / number_of_passwords}% complete." if show_progression
# it can take a long time to queue 2 million requests,
# for that reason, we queue @threads, send @threads, queue @threads and so on.

View File

@@ -38,10 +38,10 @@ module WpLoginProtection
plugin_name = symbol_to_call[LOGIN_PROTECTION_METHOD_PATTERN, 1].gsub('_', '-')
return @login_protection_plugin = WpPlugin.new(
@uri,
name: plugin_name,
base_url: @uri,
path: "/plugins/#{plugin_name}/",
wp_content_dir: @wp_content_dir
wp_content_dir: wp_content_dir,
wp_plugins_dir: wp_plugins_dir
)
end
end
@@ -54,38 +54,39 @@ module WpLoginProtection
# Thanks to Alip Aswalid for providing this method.
# http://wordpress.org/extend/plugins/login-lockdown/
def has_login_lockdown_protection?
Browser.instance.get(login_url()).body =~ %r{Login LockDown}i ? true : false
Browser.instance.get(login_url).body =~ %r{Login LockDown}i ? true : false
end
# http://wordpress.org/extend/plugins/login-lock/
def has_login_lock_protection?
Browser.instance.get(login_url()).body =~ %r{LOGIN LOCK} ? true : false
Browser.instance.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.instance.get(better_wp_security_url()).code != 404
Browser.instance.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
WpPlugin.new(wp_content_dir: @wp_content_dir,
base_url: @uri,
path: '/plugins/better-wp-security/',
name: 'better-wp-security'
).get_url_without_filename
plugin_url('better-wp-security/')
end
# http://wordpress.org/extend/plugins/simple-login-lockdown/
def has_simple_login_lockdown_protection?
Browser.instance.get(simple_login_lockdown_url()).code != 404
Browser.instance.get(simple_login_lockdown_url).code != 404
end
def simple_login_lockdown_url
WpPlugin.new(wp_content_dir: @wp_content_dir,
base_url: @uri,
path: '/plugins/simple-login-lockdown/',
name: 'simple-login-lockdown'
).get_url_without_filename
plugin_url('simple-login-lockdown/')
end
# http://wordpress.org/extend/plugins/login-security-solution/
@@ -94,36 +95,24 @@ module WpLoginProtection
end
def login_security_solution_url
WpPlugin.new(wp_content_dir: @wp_content_dir,
base_url: @uri,
path: '/plugins/login-security-solution/',
name: 'login-security-solution'
).get_url_without_filename
plugin_url('login-security-solution')
end
# http://wordpress.org/extend/plugins/limit-login-attempts/
def has_limit_login_attempts_protection?
Browser.instance.get(limit_login_attempts_url()).code != 404
Browser.instance.get(limit_login_attempts_url).code != 404
end
def limit_login_attempts_url
WpPlugin.new(wp_content_dir: @wp_content_dir,
base_url: @uri,
path: '/plugins/limit-login-attempts/',
name: 'limit-login-attempts'
).get_url_without_filename
plugin_url('limit-login-attempts')
end
# http://wordpress.org/extend/plugins/bluetrait-event-viewer/
def has_bluetrait_event_viewer_protection?
Browser.instance.get(bluetrait_event_viewer_url()).code != 404
Browser.instance.get(bluetrait_event_viewer_url).code != 404
end
def bluetrait_event_viewer_url
WpPlugin.new(wp_content_dir: @wp_content_dir,
base_url: @uri,
path: '/plugins/bluetrait-event-viewer/',
name: 'bluetrait-event-viewer'
).get_url_without_filename
plugin_url('bluetrait-event-viewer')
end
end

View File

@@ -1,72 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
module WpPlugins
# Enumerate installed plugins.
#
# return array of WpPlugin
def plugins_from_aggressive_detection(options)
if options[:vulns_file].nil? or options[:vulns_file] == ''
options[:vulns_file] = PLUGINS_VULNS_FILE
end
options[:file] = options[:file] || (options[:full] ? PLUGINS_FULL_FILE : PLUGINS_FILE)
options[:vulns_xpath] = "//plugin[@name='#{@name}']/vulnerability"
options[:vulns_xpath_2] = '//plugin'
options[:type] = 'plugins'
result = WpDetector.aggressive_detection(options)
plugins = []
result.each do |r|
plugins << WpPlugin.new(
base_url: r.base_url,
path: r.path,
wp_content_dir: r.wp_content_dir,
name: r.name,
type: 'plugins',
wp_plugins_dir: r.wp_plugins_dir
)
end
plugins.sort_by { |p| p.name }
end
# http://code.google.com/p/wpscan/issues/detail?id=42
# plugins can be found in the source code :
# <script src='http://example.com/wp-content/plugins/s2member/...' />
# <link rel='stylesheet' href='http://example.com/wp-content/plugins/wp-minify/..' type='text/css' media='screen'/>
# ...
# return array of WpPlugin
def plugins_from_passive_detection(options)
plugins = []
temp = WpDetector.passive_detection(options[:base_url], 'plugins', options[:wp_content_dir])
temp.each do |item|
plugins << WpPlugin.new(
base_url: item.base_url,
name: item.name,
path: item.path,
wp_content_dir: options[:wp_content_dir],
type: 'plugins',
wp_plugins_dir: options[:wp_plugins_dir]
)
end
plugins.sort_by { |p| p.name }
end
end

View File

@@ -1,59 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
module WpThemes
def themes_from_aggressive_detection(options)
if options[:vulns_file].nil? or options[:vulns_file] == ''
options[:vulns_file] = THEMES_VULNS_FILE
end
options[:file] = options[:file] || (options[:full] ? THEMES_FULL_FILE : THEMES_FILE)
options[:vulns_xpath] = "//theme[@name='#{@name}']/vulnerability"
options[:vulns_xpath_2] = '//theme'
options[:type] = 'themes'
result = WpDetector.aggressive_detection(options)
themes = []
result.each do |r|
themes << WpTheme.new(
base_url: r.base_url,
path: r.path,
wp_content_dir: r.wp_content_dir,
name: r.name
)
end
themes.sort_by { |t| t.name }
end
def themes_from_passive_detection(options)
themes = []
temp = WpDetector.passive_detection(options[:base_url], 'themes', options[:wp_content_dir])
temp.each do |item|
themes << WpTheme.new(
base_url: item.base_url,
name: item.name,
path: item.path,
wp_content_dir: options[:wp_content_dir]
)
end
themes.sort_by { |t| t.name }
end
end

View File

@@ -1,74 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
module WpTimthumbs
# Used as cache :
# nil => timthumbs not checked,
# [] => no timthumbs,
# otherwise array of timthumbs url found
@wp_timthumbs = nil
def has_timthumbs?(theme_name, options = {})
!timthumbs(theme_name, options).empty?
end
def timthumbs(theme_name = nil, options = {})
if @wp_timthumbs.nil?
options[:type] = 'timthumbs'
options[:only_vulnerable_ones] = false
options[:file] = options[:file] || DATA_DIR + '/timthumbs.txt'
options[:vulns_file] = 'xxx'
options[:vulns_xpath] = 'xxx'
options[:vulns_xpath_2] = 'xxx'
WpOptions.check_options(options)
if theme_name == nil
custom_items = nil
else
custom_items = targets_url_from_theme(theme_name, options)
end
@wp_timthumbs = WpEnumerator.enumerate(options, custom_items)
end
@wp_timthumbs
end
protected
def targets_url_from_theme(theme_name, options)
targets = []
theme_name = URI.escape(theme_name)
%w{
timthumb.php lib/timthumb.php inc/timthumb.php includes/timthumb.php
scripts/timthumb.php tools/timthumb.php functions/timthumb.php
}.each do |file|
targets << WpItem.new(
base_url: options[:base_url],
path: "themes/#{theme_name}/#{file}",
wp_content_dir: options[:wp_content_dir],
name: theme_name,
vulns_file: 'XX',
type: 'timthumbs',
wp_plugins_dir: options[:wp_plugins_dir]
)
end
targets
end
end

View File

@@ -1,117 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
module WpUsernames
# Enumerate wordpress usernames by using Veronica Valeros's technique:
# http://seclists.org/fulldisclosure/2011/May/493
#
# Available options :
# :range - default : 1..10
#
# returns an array of WpUser (can be empty)
def usernames(options = {})
range = options[:range] || (1..10)
browser = Browser.instance
usernames = []
range.each do |author_id|
url = author_url(author_id)
response = browser.get(url)
username = nil
nickname = nil
if response.code == 301 # username in location?
username = response.headers_hash['location'][%r{/author/([^/\b]+)/?}i, 1]
# Get the real name from the redirect site
nickname = get_nickname_from_url(url)
elsif response.code == 200 # username in body?
# get the username from the author feed URL
username = get_username_from_response(response)
nickname = get_nickname_from_response(response)
end
unless username == nil and nickname == nil
usernames << WpUser.new(username, author_id, nickname)
end
end
usernames = remove_junk_from_nickname(usernames)
# clean the array, remove nils and possible duplicates
usernames.flatten!
usernames.compact!
usernames.uniq
end
def get_nickname_from_url(url)
resp = Browser.instance.get_and_follow_location(url)
nickname = nil
if resp.code == 200
nickname = extract_nickname_from_body(resp.body)
end
nickname
end
def get_nickname_from_response(resp)
nickname = nil
if resp.code == 200
nickname = extract_nickname_from_body(resp.body)
end
nickname
end
def get_username_from_response(resp)
# Feed URL with Permalinks
username = resp.body[%r{/author/([^/\b]+)/?}i, 1]
if username.nil?
# No Permalinks
username = resp.body[%r{<body class="archive author author-([^\s]+) author-(\d+)}i, 1]
end
username
end
def extract_nickname_from_body(body)
body[%r{<title>([^<]*)</title>}i, 1]
end
def remove_junk_from_nickname(usernames)
unless usernames.kind_of? Array
raise('Need an array as input')
end
nicknames = []
usernames.each do |u|
unless u.kind_of? WpUser
raise('Items must be of type WpUser')
end
nickname = u.nickname
unless nickname == 'empty'
nicknames << nickname
end
end
junk = get_equal_string_end(nicknames)
usernames.each do |u|
u.nickname = u.nickname.sub(/#{Regexp.escape(junk)}$/, '')
end
usernames
end
def author_url(author_id)
@uri.merge("?author=#{author_id}").to_s
end
end

View File

@@ -1,40 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
class Vulnerable
attr_reader :vulns_file, :vulns_xpath
# @return an array of WpVulnerability (can be empty)
def vulnerabilities
xml = xml(@vulns_file)
vulnerabilities = []
xml.xpath(@vulns_xpath).each do |node|
vulnerabilities << WpVulnerability.new(
node.search('title').text,
node.search('reference').map(&:text),
node.search('type').text,
node.search('metasploit').map(&:text)
)
end
vulnerabilities
end
end

View File

@@ -1,78 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
class WpDetector
def self.aggressive_detection(options, items = [])
WpOptions.check_options(options)
result = items
if items == nil or items.length == 0
unless options[:only_vulnerable_ones]
result = passive_detection(options[:base_url], options[:type], options[:wp_content_dir])
end
end
enum_results = WpEnumerator.enumerate(options)
enum_results.each do |enum_result|
already_present = false
result.each do |r|
# Already found via passive detection
if r.name == enum_result.name
already_present = true
break
end
end
unless already_present
result << enum_result
end
end
result
end
# plugins and themes can be found in the source code :
# <script src='http://example.com/wp-content/plugins/s2member/...' />
# <link rel='stylesheet' href='http://example.com/wp-content/plugins/wp-minify/..' type='text/css' media='screen'/>
# ...
def self.passive_detection(url, type, wp_content_dir)
items = []
response = Browser.instance.get(url)
regex1 = %r{(?:[^=:]+)\s?(?:=|:)\s?(?:"|')[^"']+\\?/}
regex2 = %r{\\?/}
regex3 = %r{\\?/([^/\\"']+)\\?(?:/|"|')}
# Custom wp-content dir is now used in this regex
names = response.body.scan(/#{regex1}#{Regexp.escape(wp_content_dir)}#{regex2}#{Regexp.escape(type)}#{regex3}/i)
names.flatten!
names.uniq!
names.each do |item|
items << WpItem.new(
base_url: url,
name: item,
type: type,
path: "#{item}/",
wp_content_dir: wp_content_dir,
vulns_file: ''
)
end
items
end
end

View File

@@ -1,146 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
# Enumerate over a given set of items and check if they exist
class WpEnumerator
# Enumerate the given Targets
#
# ==== Attributes
#
# * +targets+ - targets to enumerate
# * * +:base_url+ - Base URL
# * * +:wp_content+ - wp-content directory
# * * +:path+ - Path to plugin
# * +type+ - "plugins" or "themes", item to enumerate
# * +filename+ - filename in the data directory with paths
# * +show_progression+ - Show a progress bar during enumeration
def self.enumerate(options = {}, items = nil)
WpOptions.check_options(options)
targets = self.generate_items(options)
unless items == nil
items.each do |i|
targets << i
end
end
found = []
queue_count = 0
request_count = 0
enum_browser = Browser.instance
enum_hydra = enum_browser.hydra
enumerate_size = targets.size
exclude_regexp = options[:exclude_content_based] ? %r{#{options[:exclude_content_based]}} : nil
show_progression = options[:show_progression] || false
targets.each do |target|
url = target.get_full_url
request = enum_browser.forge_request(url, cache_ttl: 0, followlocation: true)
request_count += 1
request.on_complete do |response|
page_hash = Digest::MD5.hexdigest(response.body)
print "\rChecking for #{enumerate_size} total #{options[:type]}... #{(request_count * 100) / enumerate_size}% complete." if show_progression
if WpTarget.valid_response_codes.include?(response.code)
if page_hash != options[:error_404_hash] and page_hash != options[:homepage_hash]
if options[:exclude_content_based]
unless response.body[exclude_regexp]
found << target
end
else
found << target
end
end
end
end
enum_hydra.queue(request)
queue_count += 1
if queue_count == enum_browser.max_threads
enum_hydra.run
queue_count = 0
end
end
enum_hydra.run
found
end
protected
def self.generate_items(options = {})
only_vulnerable = options[:only_vulnerable_ones]
file = options[:file]
vulns_file = options[:vulns_file]
wp_content_dir = options[:wp_content_dir]
url = options[:base_url]
type = options[:type]
plugins_dir = options[:wp_plugins_dir]
targets_url = []
unless only_vulnerable
# Open and parse the 'most popular' plugin list...
File.open(file, 'r') do |f|
f.readlines.collect do |line|
l = line.strip
targets_url << WpItem.new(
base_url: url,
path: l,
wp_content_dir: wp_content_dir,
name: l =~ /.+\/.+/ ? File.dirname(l) : l.sub(/\/$/, ''),
vulns_file: vulns_file,
type: type,
wp_plugins_dir: plugins_dir
)
end
end
end
# Timthumbs have no XML file
unless type =~ /timthumbs/i
xml = xml(vulns_file)
# We check if the plugin name from the plugin_vulns_file is already in targets, otherwise we add it
xml.xpath(options[:vulns_xpath_2]).each do |node|
name = node.attribute('name').text
targets_url << WpItem.new(
base_url: url,
path: name,
wp_content_dir: wp_content_dir,
name: name,
vulns_file: vulns_file,
type: type,
wp_plugins_dir: plugins_dir
)
end
end
targets_url.flatten! { |t| t.name }
targets_url.uniq! { |t| t.name }
# randomize the plugins array to *maybe* help in some crappy IDS/IPS/WAF detection
targets_url.sort_by! { rand }
end
end

View File

@@ -1,196 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require "wpscan/vulnerable"
class WpItem < Vulnerable
attr_reader :base_url, :path, :wp_content_dir, :name, :vulns_file, :vulns_xpath, :wp_plugins_dir, :type
@version = nil
def initialize(options)
@type = options[:type]
@wp_content_dir = options[:wp_content_dir] ? options[:wp_content_dir].sub(/^\//, '').sub(/\/$/, '') : 'wp-content'
@wp_plugins_dir = options[:wp_plugins_dir] || "#@wp_content_dir/plugins"
@base_url = options[:base_url]
@path = options[:path]
@name = options[:name] || extract_name_from_url
@vulns_file = options[:vulns_file]
@vulns_xpath = options[:vulns_xpath].sub(/\$name\$/, @name) unless options[:vulns_xpath] == nil
raise('base_url not set') unless @base_url
raise('path not set') unless @path
raise('wp_content_dir not set') unless @wp_content_dir
raise('name not set') unless @name
raise('vulns_file not set') unless @vulns_file
raise('type not set') unless @type
end
# The wordpress.org plugins directory URL
# See: https://github.com/wpscanteam/wpscan/issues/100
def wp_org_url
case @type
when 'themes'
return URI('http://wordpress.org/extend/themes/').merge("#@name/")
when 'plugins'
return URI('http://wordpress.org/extend/plugins/').merge("#@name/")
else
raise("No Wordpress URL for #@type")
end
end
# returns true if this theme or plugin is hosted on wordpress.org
def wp_org_item?
case @type
when 'themes'
file = THEMES_FULL_FILE
when 'plugins'
file = PLUGINS_FULL_FILE
else
raise("Unknown type #@type")
end
f = File.readlines(file, encoding: 'UTF-8').grep(/^#{Regexp.escape(@name)}$/i)
f.empty? ? false : true
end
def get_sub_folder
case @type
when 'themes'
folder = 'themes'
when 'timthumbs'
# not needed
folder = nil
else
raise("unknown type #@type")
end
folder
end
# Get the full url for this item
def get_full_url
url = @base_url.to_s.end_with?('/') ? @base_url.to_s : "#@base_url/"
# remove first and last /
wp_content_dir = @wp_content_dir.sub(/^\//, "").sub(/\/$/, '')
# remove first /
path = @path.sub(/^\//, '')
if type == 'plugins'
# plugins can be outside of wp-content. wp_content_dir included in wp_plugins_dir
ret = URI.parse(URI.encode("#{url}#@wp_plugins_dir/#{path}"))
elsif type == 'timthumbs'
# timthumbs have folder in path variable
ret = URI.parse(URI.encode("#{url}#{wp_content_dir}/#{path}"))
else
ret = URI.parse(URI.encode("#{url}#{wp_content_dir}/#{get_sub_folder}/#{path}"))
end
ret
end
# Gets the full url for this item without filenames
def get_url_without_filename
location_url = get_full_url.to_s
valid_location_url = location_url[%r{^(https?://.*/)[^.]+\.[^/]+$}, 1]
unless valid_location_url
valid_location_url = add_trailing_slash(location_url)
end
URI.parse(URI.encode(valid_location_url))
end
# Returns version number from readme.txt if it exists
def version
unless @version
response = Browser.instance.get(readme_url.to_s)
@version = response.body[%r{stable tag: #{WpVersion.version_pattern}}i, 1]
end
@version
end
# Is directory listing enabled?
def directory_listing?
# Need to remove to file part from the url
Browser.instance.get(get_url_without_filename).body[%r{<title>Index of}] ? true : false
end
# Extract item name from a url
def extract_name_from_url
get_full_url.to_s[%r{^(https?://.*/([^/]+)/)}i, 2]
end
# To string. Adds a version number if detected
def to_s
item_version = version
"#@name#{' v' + item_version.strip if item_version}"
end
# Compare
def ==(other)
other.name == self.name
end
# Compare
def ===(other)
other.name == self.name
end
# Compare
def <=>(other)
other.name <=> self.name
end
# Url for readme.txt
def readme_url
get_url_without_filename.merge('readme.txt')
end
# Url for changelog.txt
def changelog_url
get_url_without_filename.merge('changelog.txt')
end
def error_log_url
get_url_without_filename.merge('error_log')
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/
def error_log?
response_body = Browser.instance.get(error_log_url, headers: {'range' => 'bytes=0-700'}).body
response_body[%r{PHP Fatal error}i] ? true : false
end
# readme.txt present?
def has_readme?
unless @readme
status = Browser.instance.get(readme_url).code
@readme = status == 200 ? true : false
end
@readme
end
# changelog.txt present?
def has_changelog?
unless @changelog
status = Browser.instance.get(changelog_url).code
@changelog = status == 200 ? true : false
end
@changelog
end
end

View File

@@ -1,52 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
# Options Hash
#
# ==== Options
#
# * +url+ - The base URL of the WordPress site
# * +only_vulnerable_ones+ - Only detect vulnerable items
# * +file+ - Filename with items to detect
# * +vulns_file+ - XML file with vulnerabilities
# * +vulns_xpath+ - XPath for vulnerability XML file
# * +vulns_xpath_2+ - XPath for vulnerability XML file
# * +wp_content_dir+ - Name of the wp-content directory
# * +show_progression+ - Show a progress bar during enumeration
# * +error_404_hash+ - MD5 hash of a 404 page
# * +type+ - Type: plugins, themes
class WpOptions
def self.check_options(options)
raise('base_url must be set') unless options[:base_url] != nil and options[:base_url].to_s.length > 0
raise('only_vulnerable_ones must be set') unless options[:only_vulnerable_ones] != nil
raise('file must be set') unless options[:file] != nil and options[:file].length > 0
raise('vulns_file must be set') unless options[:vulns_file] != nil and options[:vulns_file].length > 0
raise('vulns_xpath must be set') unless options[:vulns_xpath] != nil and options[:vulns_xpath].length > 0
raise('vulns_xpath_2 must be set') unless options[:vulns_xpath_2] != nil and options[:vulns_xpath_2].length > 0
raise('wp_content_dir must be set') unless options[:wp_content_dir] != nil and options[:wp_content_dir].length > 0
raise('show_progression must be set') unless options[:show_progression] != nil
raise('error_404_hash must be set') unless options[:error_404_hash] != nil and options[:error_404_hash].length > 0
raise('type must be set') unless options[:type] != nil and options[:type].length > 0
unless options[:type] =~ /plugins/i or options[:type] =~ /themes/i or options[:type] =~ /timthumbs/i
raise("Unknown type #{options[:type]}")
end
end
end

View File

@@ -1,32 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
class WpPlugin < WpItem
def initialize(options = {})
if options[:vulns_file].nil? or options[:vulns_file] == ''
options[:vulns_file] = PLUGINS_VULNS_FILE
end
options[:vulns_xpath] = "//plugin[@name='$name$']/vulnerability"
options[:vulns_xpath_2] = '//plugin'
options[:type] = 'plugins'
super(options)
end
end

View File

@@ -23,10 +23,6 @@ class WpTarget < WebSite
include WpConfigBackup
include WpLoginProtection
include Malwares
include WpUsernames
include WpTimthumbs
include WpPlugins
include WpThemes
include BruteForce
attr_reader :verbose
@@ -92,7 +88,17 @@ class WpTarget < WebSite
# return WpVersion
def version
WpVersion.find(@uri, wp_content_dir)
WpVersion.find(@uri, wp_content_dir, wp_plugins_dir)
end
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
def wp_content_dir

View File

@@ -1,115 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require "wpscan/vulnerable"
class WpTheme < WpItem
attr_reader :style_url, :version
def initialize(options = {})
if options[:vulns_file].nil? or options[:vulns_file] == ''
options[:vulns_file] = THEMES_VULNS_FILE
end
options[:vulns_xpath] = "//theme[@name='$name$']/vulnerability"
options[:type] = 'themes'
@version = options[:version]
@style_url = options[:style_url]
super(options)
end
def version
unless @version
if @style_url
url = @style_url
else
url = default_style_url
end
@version = Browser.instance.get(url).body[%r{Version:\s([^\s]+)}i, 1]
# Get Version from readme.txt
if @version.nil?
@version = super
end
end
@version
end
def default_style_url
get_url_without_filename.merge('style.css')
end
def self.find(target_uri)
self.methods.grep(/find_from_/).each do |method_to_call|
theme = self.send(method_to_call, target_uri)
return theme if theme
end
nil
end
def ===(wp_theme)
wp_theme.name === @name and wp_theme.version === @version
end
protected
# Discover the wordpress theme name by parsing the css link rel
def self.find_from_css_link(target_uri)
response = Browser.instance.get_and_follow_location(target_uri.to_s)
# https + domain is optional because of relative links
matches = %r{(?:https?://[^"']+)?/([^/]+)/themes/([^"']+)/style.css}i.match(response.body)
if matches
style_url = matches[0]
wp_content_dir = matches[1]
theme_name = matches[2]
return new(
name: theme_name,
style_url: style_url,
base_url: target_uri,
path: theme_name,
wp_content_dir: wp_content_dir
)
end
end
# http://code.google.com/p/wpscan/issues/detail?id=141
def self.find_from_wooframework(target_uri)
body = Browser.instance.get(target_uri.to_s).body
regexp = %r{<meta name="generator" content="([^\s"]+)\s?([^"]+)?" />\s+<meta name="generator" content="WooFramework\s?([^"]+)?" />}
matches = regexp.match(body)
if matches
woo_theme_name = matches[1]
woo_theme_version = matches[2]
woo_framework_version = matches[3] # Not used at this time
return new(
name: woo_theme_name,
version: woo_theme_version,
base_url: target_uri.to_s,
path: woo_theme_name
)
end
end
end

View File

@@ -1,76 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
class WpUser
def name
if @name.nil? or @name.to_s.strip.empty?
return 'empty'
end
@name
end
def name=(new_name)
@name = new_name
end
def id
if @id.nil? or @id.to_s.strip.empty?
return 'empty'
end
@id
end
def id=(new_id)
@id = new_id
end
def nickname
if @nickname.nil? or @nickname.to_s.strip.empty?
return 'empty'
end
@nickname
end
def nickname=(new_nickname)
@nickname = new_nickname
end
def initialize(name, id, nickname)
self.name = name
self.id = id
self.nickname = nickname
end
def <=>(other)
other.name <=> self.name
end
def ==(other)
self === other
end
def ===(other)
other.name === self.name and other.id === self.id and other.nickname === self.nickname
end
def eql?(other)
self === other
end
end

View File

@@ -1,202 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require "wpscan/vulnerable"
class WpVersion < Vulnerable
attr_reader :number, :discovery_method
def initialize(number, options = {})
@number = number
@discovery_method = options[:discovery_method]
@vulns_file = options[:vulns_file] || WP_VULNS_FILE
@vulns_xpath = "//wordpress[@version='#{@number}']/vulnerability"
end
# Will use all method self.find_from_* to try to detect the version
# Once the version is found, it will return a WpVersion object
# The method_name will be without 'find_from_' and '_' will be replace by ' ' (IE 'meta generator', 'rss generator' etc)
# If the version is not found, nil is returned
#
# The order in which the find_from_* methods are is important, they will be called in the same order
# (find_from_meta_generator, find_from_rss_generator etc)
def self.find(target_uri, wp_content_dir)
options = {
base_uri: target_uri,
wp_content_dir: wp_content_dir
}
self.methods.grep(/find_from_/).each do |method_to_call|
version = self.send(method_to_call, options)
if version
return new(version, discovery_method: method_to_call[%r{find_from_(.*)}, 1].gsub('_', ' '))
end
end
nil
end
protected
# Returns the first match of <pattern> in the body of the url
def self.scan_url(base_uri, pattern, path = nil)
url = path ? base_uri.merge(path).to_s : base_uri.to_s
response = Browser.instance.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.
def self.find_from_meta_generator(options)
WpVersion.scan_url(
options[:base_uri],
%r{name="generator" content="wordpress #{WpVersion.version_pattern}"}i
)
end
# Attempts to find the WordPress version from,
# the generator tag in the RSS feed source.
def self.find_from_rss_generator(options)
WpVersion.scan_url(
options[:base_uri],
%r{<generator>http://wordpress.org/\?v=#{WpVersion.version_pattern}</generator>}i,
'feed/'
)
end
# Attempts to find WordPress version from,
# the generator tag in the RDF feed source.
def self.find_from_rdf_generator(options)
WpVersion.scan_url(
options[:base_uri],
%r{<admin:generatorAgent rdf:resource="http://wordpress.org/\?v=#{WpVersion.version_pattern}" />}i,
'feed/rdf/'
)
end
# Attempts to find the WordPress version from,
# the generator tag in the RSS2 feed source.
#
# Have not been able to find an example of this - Ryan
#def self.find_from_rss2_generator(options)
# WpVersion.scan_url(
# options[:base_uri],
# %r{<generator>http://wordpress.org/?v=(#{WpVersion.version_pattern})</generator>}i,
# 'feed/rss/'
# )
#end
# Attempts to find the WordPress version from,
# the generator tag in the Atom source.
def self.find_from_atom_generator(options)
WpVersion.scan_url(
options[:base_uri],
%r{<generator uri="http://wordpress.org/" version="#{WpVersion.version_pattern}">WordPress</generator>}i,
'feed/atom/'
)
end
# Attempts to find the WordPress version from,
# the generator tag in the comment rss source.
#
# Have not been able to find an example of this - Ryan
#def self.find_from_comments_rss_generator(options)
# WpVersion.scan_url(
# options[:base_uri],
# %r{<!-- generator="WordPress/#{WpVersion.version_pattern}" -->}i,
# 'comments/feed/'
# )
#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)
#
def self.find_from_advanced_fingerprinting(options)
target_uri = options[:base_uri]
version_xml = options[:version_xml] || WP_VERSIONS_FILE # needed for rpsec
wp_content = options[:wp_content_dir]
wp_plugins = "#{wp_content}/plugins"
xml = xml(version_xml)
xml.xpath('//file').each do |node|
file_src = node.attribute('src').text
file_url = target_uri.merge(file_src).to_s.
gsub(/\$wp-plugins\$/i, wp_plugins).
gsub(/\$wp-content\$/i, wp_content)
response = Browser.instance.get(file_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.
def self.find_from_readme(options)
WpVersion.scan_url(
options[:base_uri],
%r{<br />\sversion #{WpVersion.version_pattern}}i,
'readme.html'
)
end
# Attempts to find the WordPress version from the sitemap.xml file.
#
# See: http://code.google.com/p/wpscan/issues/detail?id=109
def self.find_from_sitemap_generator(options)
WpVersion.scan_url(
options[:base_uri],
%r{generator="wordpress/#{WpVersion.version_pattern}"}i,
'sitemap.xml'
)
end
# Attempts to find the WordPress version from the p-links-opml.php file.
def self.find_from_links_opml(options)
WpVersion.scan_url(
options[:base_uri],
%r{generator="wordpress/#{WpVersion.version_pattern}"}i,
'wp-links-opml.php'
)
end
# Used to check if the version is correct: must contain at least one dot.
def self.version_pattern
'([^\r\n"\']+\.[^\r\n"\']+)'
end
end

View File

@@ -1,29 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
class WpVulnerability
attr_accessor :title, :references, :type, :metasploit_modules
def initialize(title, references, type, metasploit_modules)
@title = title
@references = references
@type = type
@metasploit_modules = metasploit_modules
end
end

View File

@@ -17,9 +17,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require 'wpscan/wp_enumerator'
require 'wpscan/wp_item'
class StatsPlugin < Plugin
def initialize
@@ -60,23 +57,16 @@ class StatsPlugin < Plugin
xml(file).xpath("count(//vulnerability)").to_i
end
def total_plugins(file=PLUGINS_FULL_FILE, xml=PLUGINS_VULNS_FILE)
total('plugins', file, xml)
def total_plugins(file=PLUGINS_FULL_FILE)
lines_in_file(file)
end
def total_themes(file=THEMES_FULL_FILE, xml=THEMES_VULNS_FILE)
total('themes', file, xml)
def total_themes(file=THEMES_FULL_FILE)
lines_in_file(file)
end
def total(type, file, xml)
options = {
type: type,
file: file,
vulns_file: xml,
base_url: 'http://localhost',
only_vulnerable_ones: false
}
WpEnumerator.generate_items(options).count
def lines_in_file(file)
IO.readlines(file).size
end
end

170
main.rb
View File

@@ -19,35 +19,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
def output_vulnerabilities(vulns)
vulns.each do |vulnerability|
puts
puts ' | ' + red("* Title: #{vulnerability.title}")
vulnerability.references.each do |r|
puts ' | ' + red("* Reference: #{r}")
end
vulnerability.metasploit_modules.each do |m|
puts ' | ' + red("* Metasploit module: #{get_metasploit_url(m)}")
end
end
end
def output_item_details(item)
puts
puts " | Name: #{item}" #this will also output the version number if detected
puts " | Location: #{item.get_url_without_filename}"
puts " | WordPress: #{item.wp_org_url}" if item.wp_org_item?
puts ' | Directory listing enabled: Yes' if item.directory_listing?
puts " | Readme: #{item.readme_url}" if item.has_readme?
puts " | Changelog: #{item.changelog_url}" if item.has_changelog?
output_vulnerabilities(item.vulnerabilities)
if item.error_log?
puts ' | ' + red('[!]') + " A WordPress error_log file has been found : #{item.error_log_url}"
end
end
def main
# delete old logfile, check if it is a symlink first.
File.delete(LOG_FILE) if File.exist?(LOG_FILE) and !File.symlink?(LOG_FILE)
@@ -193,38 +164,31 @@ def main
puts
end
wp_version = wp_target.version
if wp_version
puts green('[+]') + " WordPress version #{wp_version.number} identified from #{wp_version.discovery_method}"
enum_options = {
show_progression: true,
exclude_content: wpscan_options.exclude_content_based
}
version_vulnerabilities = wp_version.vulnerabilities
unless version_vulnerabilities.empty?
puts
puts red('[!]') + " We have identified #{version_vulnerabilities.size} vulnerabilities from the version number :"
output_vulnerabilities(version_vulnerabilities)
end
if wp_version = wp_target.version
wp_version.output
end
wp_theme = wp_target.theme
if wp_theme
if wp_theme = wp_target.theme
puts
# Theme version is handled in wp_item.to_s
# Theme version is handled in #to_s
puts green('[+]') + " The WordPress theme in use is #{wp_theme}"
output_item_details(wp_theme)
wp_theme.output
end
if wpscan_options.enumerate_plugins == nil and wpscan_options.enumerate_only_vulnerable_plugins == nil
puts
puts green('[+]') + ' Enumerating plugins from passive detection ... '
plugins = wp_target.plugins_from_passive_detection(base_url: wp_target.uri, wp_content_dir: wp_target.wp_content_dir)
if !plugins.empty?
puts "#{plugins.size} plugins found :"
wp_plugins = WpPlugins.passive_detection(wp_target)
if !wp_plugins.empty?
puts "#{wp_plugins.size} plugins found :"
plugins.each do |plugin|
output_item_details(plugin)
end
wp_plugins.output
else
puts 'No plugins found :('
end
@@ -236,27 +200,18 @@ def main
puts green('[+]') + " Enumerating installed plugins #{'(only vulnerable ones)' if wpscan_options.enumerate_only_vulnerable_plugins} ..."
puts
options = {
base_url: wp_target.uri,
only_vulnerable_ones: wpscan_options.enumerate_only_vulnerable_plugins || false,
show_progression: true,
wp_content_dir: wp_target.wp_content_dir,
error_404_hash: wp_target.error_404_hash,
homepage_hash: wp_target.homepage_hash,
wp_plugins_dir: wp_target.wp_plugins_dir,
full: wpscan_options.enumerate_all_plugins,
exclude_content_based: wpscan_options.exclude_content_based
}
plugins = wp_target.plugins_from_aggressive_detection(options)
if !plugins.empty?
wp_plugins = WpPlugins.aggressive_detection(wp_target,
enum_options.merge(
file: wpscan_options.enumerate_all_plugins ? PLUGINS_FULL_FILE : PLUGINS_FILE,
only_vulnerable: wpscan_options.enumerate_only_vulnerable_plugins || false
)
)
if !wp_plugins.empty?
puts
puts
puts green('[+]') + " We found #{plugins.size.to_s} plugins:"
puts green('[+]') + " We found #{wp_plugins.size} plugins:"
plugins.each do |plugin|
output_item_details(plugin)
end
wp_plugins.output
else
puts
puts 'No plugins found :('
@@ -269,26 +224,19 @@ def main
puts green('[+]') + " Enumerating installed themes #{'(only vulnerable ones)' if wpscan_options.enumerate_only_vulnerable_themes} ..."
puts
options = {
base_url: wp_target.uri,
only_vulnerable_ones: wpscan_options.enumerate_only_vulnerable_themes || false,
show_progression: true,
wp_content_dir: wp_target.wp_content_dir,
error_404_hash: wp_target.error_404_hash,
homepage_hash: wp_target.homepage_hash,
full: wpscan_options.enumerate_all_themes,
exclude_content_based: wpscan_options.exclude_content_based
}
wp_themes = WpThemes.aggressive_detection(wp_target,
enum_options.merge(
file: wpscan_options.enumerate_all_themes ? THEMES_FULL_FILE : THEMES_FILE,
only_vulnerable: wpscan_options.enumerate_only_vulnerable_themes || false
)
)
themes = wp_target.themes_from_aggressive_detection(options)
if !themes.empty?
if !wp_themes.empty?
puts
puts
puts green('[+]') + " We found #{themes.size.to_s} themes:"
puts green('[+]') + " We found #{wp_themes.size} themes:"
themes.each do |theme|
output_item_details(theme)
end
wp_themes.output
else
puts
puts 'No themes found :('
@@ -300,26 +248,19 @@ def main
puts green('[+]') + ' Enumerating timthumb files ...'
puts
options = {
base_url: wp_target.uri,
show_progression: true,
wp_content_dir: wp_target.wp_content_dir,
error_404_hash: wp_target.error_404_hash,
homepage_hash: wp_target.homepage_hash,
exclude_content_based: wpscan_options.exclude_content_based
}
theme_name = wp_theme ? wp_theme.name : nil
if wp_target.has_timthumbs?(theme_name, options)
timthumbs = wp_target.timthumbs
wp_timthumbs = WpTimthumbs.aggressive_detection(wp_target,
enum_options.merge(
file: DATA_DIR + '/timthumbs.txt',
theme_name: wp_theme ? wp_theme.name : nil
)
)
if !wp_timthumbs.empty?
puts
puts green('[+]') + " We found #{timthumbs.size.to_s} timthumb file/s :"
puts green('[+]') + " We found #{timthumbs.size} timthumb file/s :"
puts
timthumbs.each do |t|
puts ' | ' + red('[!]') + " #{t.get_full_url.to_s}"
end
wp_timthumbs.output
puts
puts red(' * Reference: http://www.exploit-db.com/exploits/17602/')
else
@@ -333,9 +274,14 @@ def main
puts
puts green('[+]') + ' Enumerating usernames ...'
usernames = wp_target.usernames(range: wpscan_options.enumerate_usernames_range)
wp_users = WpUsers.aggressive_detection(wp_target,
enum_options.merge(
range: wpscan_options.enumerate_usernames_range,
show_progression: false
)
)
if usernames.empty?
if wp_users.empty?
puts
puts 'We did not enumerate any usernames :('
puts 'Try supplying your own username with the --username option'
@@ -343,24 +289,14 @@ def main
exit(1)
else
puts
puts green('[+]') + " We found the following #{usernames.length.to_s} username/s :"
puts
puts green('[+]') + " We found the following #{wp_users.size} user/s :"
max_id_length = usernames.sort { |a, b| a.id.to_s.length <=> b.id.to_s.length }.last.id.to_s.length
max_name_length = usernames.sort { |a, b| a.name.length <=> b.name.length }.last.name.length
max_nickname_length = usernames.sort { |a, b| a.nickname.length <=> b.nickname.length }.last.nickname.length
space = 1
usernames.each do |u|
id_string = "id: #{u.id.to_s.ljust(max_id_length + space)}"
name_string = "name: #{u.name.ljust(max_name_length + space)}"
nickname_string = "nickname: #{u.nickname.ljust(max_nickname_length + space)}"
puts " | #{id_string}| #{name_string}| #{nickname_string}"
end
wp_users.output(' ' * 4)
end
else
usernames = [WpUser.new(wpscan_options.username, -1, 'empty')]
# FIXME : Change the .username to .login (and also the --username in the CLI)
wp_users = WpUsers.new << WpUser.new(wp_target, login: wpscan_options.username)
end
# Start the brute forcer
@@ -381,7 +317,7 @@ def main
puts
puts green('[+]') + ' Starting the password brute forcer'
puts
wp_target.brute_force(usernames, wpscan_options.wordlist, {show_progression: true})
wp_target.brute_force(wp_users, wpscan_options.wordlist, { show_progression: true })
else
puts
puts 'Brute forcing aborted'

View File

@@ -1,76 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '../../../lib/wpscan/wpscan_helper')
describe 'common_helper' do
describe '#get_equal_string' do
after :each do
output = get_equal_string_end(@input)
output.should == @expected
end
it 'sould return an empty string' do
@input = ['']
@expected = ''
end
it 'sould return an empty string' do
@input = []
@expected = ''
end
it 'sould return asdf' do
@input = ['kjh asdf', 'oijr asdf']
@expected = ' asdf'
end
it 'sould return &laquo; BlogName' do
@input = ['user1 &laquo; BlogName',
'user2 &laquo; BlogName',
'user3 &laquo; BlogName',
'user4 &laquo; BlogName']
@expected = ' &laquo; BlogName'
end
it 'sould return an empty string' do
@input = %w{user1 user2 user3 user4}
@expected = ''
end
it 'sould return an empty string' do
@input = ['user1 &laquo; BlogName',
'user2 &laquo; BlogName',
'user3 &laquo; BlogName',
'user4 &laquo; BlogNamea']
@expected = ''
end
it 'sould return an empty string' do
@input = %w{ user1 }
@expected = ''
end
it 'sould return | test' do
@input = ['admin | test', 'test | test']
@expected = ' | test'
end
end
end

View File

@@ -59,7 +59,7 @@ shared_examples_for 'BruteForce' do
)
end
user = WpUser.new('admin', 1, nil)
user = WpUser.new(@module.uri, login: 'admin')
result = @module.brute_force([user], @wordlist)
result.length.should == 1
@@ -69,7 +69,7 @@ shared_examples_for 'BruteForce' do
it 'should cover the timeout branch and return an empty array' do
stub_request(:post, @module.login_url).to_timeout
user = WpUser.new('admin', 1, nil)
user = WpUser.new(@module.uri, login: 'admin')
result = @module.brute_force([user], @wordlist)
result.should == []
end

View File

@@ -22,6 +22,7 @@ shared_examples_for 'WpLoginProtection' do
before :each do
@module = WpScanModuleSpec.new('http://example.localhost')
@module.extend(WpLoginProtection)
@module.stub(:wp_plugins_dir).and_return('wp-content/plugins')
@fixtures_dir = SPEC_FIXTURES_WPSCAN_MODULES_DIR + '/wp_login_protection'
end
@@ -70,10 +71,10 @@ shared_examples_for 'WpLoginProtection' do
expected = plugin_name_from_fixture === plugin_name_from_symbol ? true : false
it "#{symbol_to_call} with #{fixture} should return #{expected}" do
@plugin_name = plugin_name_from_fixture
@fixture = @fixtures_dir + '/' + fixture
@plugin_name = plugin_name_from_fixture
@fixture = @fixtures_dir + '/' + fixture
@symbol_to_call = symbol_to_call
@expected = expected
@expected = expected
end
end
end
@@ -89,33 +90,25 @@ shared_examples_for 'WpLoginProtection' do
stub_request(:get, @module.send(:limit_login_attempts_url).to_s).to_return(status: 404)
stub_request(:get, @module.send(:bluetrait_event_viewer_url).to_s).to_return(status: 404)
@module.login_protection_plugin().should === @plugin_expected
@module.login_protection_plugin().should == @plugin_expected
@module.has_login_protection?.should === @has_protection_expected
end
it 'should return nil if no protection is present' do
@fixture = @fixtures_dir + '/wp-login-clean.php'
@plugin_expected = nil
@fixture = @fixtures_dir + '/wp-login-clean.php'
@plugin_expected = nil
@has_protection_expected = false
end
it 'should return a login-lockdown WpPlugin object' do
@fixture = @fixtures_dir + '/wp-login-login_lockdown.php'
@plugin_expected = WpPlugin.new(
base_url: @module.url,
path: '/plugins/login-lockdown/',
name: 'login-lockdown'
)
@fixture = @fixtures_dir + '/wp-login-login_lockdown.php'
@plugin_expected = WpPlugin.new(@module.uri, name: 'login-lockdown')
@has_protection_expected = true
end
it 'should return a login-lock WpPlugin object' do
@fixture = @fixtures_dir + '/wp-login-login_lock.php'
@plugin_expected = WpPlugin.new(
base_url: @module.url,
path: '/plugins/login-lock/',
name: 'login-lock'
)
@fixture = @fixtures_dir + '/wp-login-login_lock.php'
@plugin_expected = WpPlugin.new(@module.uri, name: 'login-lock')
@has_protection_expected = true
end
end

View File

@@ -1,197 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
shared_examples_for 'WpPlugins' do
before :all do
@fixtures_dir = SPEC_FIXTURES_WPSCAN_MODULES_DIR + '/wp_plugins'
@plugins_file = @fixtures_dir + '/plugins.txt'
@plugin_vulns_file = @fixtures_dir + '/plugin_vulns.xml'
@wp_url = 'http://example.localhost/'
end
before :each do
@module = WpScanModuleSpec.new(@wp_url)
@module.error_404_hash = Digest::MD5.hexdigest('Error 404!')
@module.homepage_hash = Digest::MD5.hexdigest('Homepage!')
@module.extend(WpPlugins)
@options = {
base_url: @wp_url,
only_vulnerable_ones: false,
show_progression: false,
error_404_hash: @module.error_404_hash,
homepage_hash: @module.homepage_hash,
vulns_file: @plugin_vulns_file,
file: @plugins_file,
type: 'plugins',
wp_content_dir: 'wp-content',
vulns_xpath_2: '//plugin'
}
File.exist?(@plugin_vulns_file).should == true
File.exist?(@plugins_file).should == true
# These targets are listed in @fixtures_dir + '/plugins.txt'
# TODO : load them directly from the fixture file
@targets = [
WpPlugin.new(
{
base_url: 'http://example.localhost/',
path: 'exclude-pages/exclude_pages.php',
wp_content_dir: 'wp-content',
name: 'exclude-pages'
}),
WpPlugin.new(
{
base_url: 'http://example.localhost/',
path: 'display-widgets/display-widgets.php',
wp_content_dir: 'wp-content',
name: 'display-widgets'
}),
WpPlugin.new(
{
base_url: 'http://example.localhost/',
path: 'media-library',
wp_content_dir: 'wp-content',
name: 'media-library'
}),
WpPlugin.new(
{
base_url: 'http://example.localhost/',
path: 'deans',
wp_content_dir: 'wp-content',
name: 'deans'
}),
WpPlugin.new(
{
base_url: 'http://example.localhost/',
path: 'formidable/formidable.php',
wp_content_dir: 'wp-content',
name: 'formidable'
}),
WpPlugin.new(
{
base_url: 'http://example.localhost/',
path: 'regenerate-thumbnails/readme.txt',
wp_content_dir: 'wp-content',
name: 'regenerate-thumbnails'
})
]
end
describe '#plugins_from_passive_detection' do
let(:passive_detection_fixtures) { @fixtures_dir + '/passive_detection' }
it 'should return an empty array' do
stub_request_to_fixture(url: @module.url, fixture: File.new(passive_detection_fixtures + '/no_plugins.htm'))
plugins = @module.plugins_from_passive_detection(base_url: @module.url, wp_content_dir: 'wp-content')
plugins.should be_empty
end
it 'should return the expected plugins' do
stub_request_to_fixture(url: @module.url, fixture: File.new(passive_detection_fixtures + '/various_plugins.htm'))
expected_plugin_names = %w{
wp-minify
comment-info-tip
tweet-blender
optinpop
s2member
wp-polls
commentluv
}
expected_plugins = []
expected_plugin_names.each do |plugin_name|
expected_plugins << WpPlugin.new(
base_url: @module.url,
path: "/plugins/#{plugin_name}/",
name: plugin_name
)
end
plugins = @module.plugins_from_passive_detection(base_url: @module.url, wp_content_dir: 'wp-content')
plugins.should_not be_empty
plugins.length.should == expected_plugins.length
plugins.sort.should == expected_plugins.sort
end
end
describe '#plugins_from_aggressive_detection' do
before :each do
stub_request(:get, @module.uri.to_s).to_return(status: 200)
# Point all targets to a 404
@targets.each do |target|
stub_request(:get, target.get_full_url.to_s).to_return(status: 404)
# to_s calls readme_url
stub_request(:get, target.readme_url.to_s).to_return(status: 404)
end
end
after :each do
@passive_detection_fixture = SPEC_FIXTURES_DIR + '/empty-file' unless @passive_detection_fixture
stub_request_to_fixture(url: "#{@module.uri}/".sub(/\/\/$/, '/'), fixture: @passive_detection_fixture)
detected = @module.plugins_from_aggressive_detection(@options)
detected.length.should == @expected_plugins.length
detected.sort.should == @expected_plugins.sort
end
it 'should return an empty array' do
@expected_plugins = []
end
it 'should return an array with 3 WpPlugin (1 detected from passive method)' do
@passive_detection_fixture = @fixtures_dir + '/passive_detection/one_plugin.htm'
@expected_plugins = @targets.sample(2)
@expected_plugins.each do |p|
stub_request(:get, p.get_full_url.to_s).to_return(status: 200)
end
new_plugin = WpPlugin.new(
base_url: 'http://example.localhost/',
path: '/plugins/comment-info-tip/',
name: 'comment-info-tip'
)
stub_request(:get, new_plugin.readme_url.to_s).to_return(status: 200)
@expected_plugins << new_plugin
end
# testing response codes
WpTarget.valid_response_codes.each do |valid_response_code|
it "should detect the plugin if the reponse.code is #{valid_response_code}" do
@expected_plugins = []
plugin_url = [@targets.sample(1)[0]]
plugin_url.should_not be_nil
plugin_url.length.should == 1
@expected_plugins = plugin_url
stub_request(:get, plugin_url[0].get_full_url.to_s).to_return(status: valid_response_code)
end
end
it 'should not detect the plugin if there is a redirection to the homepage' do
# Let's pick up 2 plugins (The first one will redirect to the homepage)
plugins = @targets.sample(2)
stub_request(:get, plugins[0].get_full_url.to_s).to_return(status: 200, body: 'Homepage!')
stub_request(:get, plugins[1].get_full_url.to_s).to_return(status: 200)
@expected_plugins = [plugins[1]]
end
end
end

View File

@@ -1,211 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
shared_examples_for 'WpThemes' do
before :all do
@fixtures_dir = SPEC_FIXTURES_WPSCAN_MODULES_DIR + '/wp_themes'
@themes_file = @fixtures_dir + '/themes.txt'
@theme_vulns_file = @fixtures_dir + '/theme_vulns.xml'
@wp_url = 'http://example.localhost/'
end
before :each do
@module = WpScanModuleSpec.new(@wp_url)
@module.error_404_hash = Digest::MD5.hexdigest('Error 404!')
@module.extend(WpThemes)
@options = {
base_url: @wp_url,
only_vulnerable_ones: false,
show_progression: false,
error_404_hash: Digest::MD5.hexdigest('Error 404!'),
vulns_file: @theme_vulns_file,
file: @themes_file,
type: 'themes',
wp_content_dir: 'wp-content',
vulns_xpath_2: '//theme'
}
File.exist?(@theme_vulns_file).should == true
File.exist?(@themes_file).should == true
@targets = [
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zenpro/404.php',
wp_content_dir: 'wp-content',
name: 'zenpro'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zeta-zip/404.php',
wp_content_dir: 'wp-content',
name: 'zeta-zip'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zfirst/404.php',
wp_content_dir: 'wp-content',
name: 'zfirst'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zgrey/404.php',
wp_content_dir: 'wp-content',
name: 'zgrey'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zindi-ii/404.php',
wp_content_dir: 'wp-content',
name: 'zindi-ii'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zindi/404.php',
wp_content_dir: 'wp-content',
name: 'zindi'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zombie-apocalypse/404.php',
wp_content_dir: 'wp-content',
name: 'zombie-apocalypse'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zsofa/404.php',
wp_content_dir: 'wp-content',
name: 'zsofa'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'zwei-seiten/404.php',
wp_content_dir: 'wp-content',
name: 'zwei-seiten'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'twentyten/404.php',
wp_content_dir: 'wp-content',
name: 'twentyten'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'shopperpress',
wp_content_dir: 'wp-content',
name: 'shopperpress'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'wise',
wp_content_dir: 'wp-content',
name: 'wise'
}),
WpTheme.new({
base_url: 'http://example.localhost/',
path: 'webfolio',
wp_content_dir: 'wp-content',
name: 'webfolio'
})
]
end
describe '#themes_from_passive_detection' do
let(:passive_detection_fixtures) { @fixtures_dir + '/passive_detection' }
it 'should return an empty array' do
stub_request_to_fixture(url: @module.url, fixture: File.new(passive_detection_fixtures + '/no_theme.htm'))
themes = @module.themes_from_passive_detection(base_url: @module.url, wp_content_dir: 'wp-content')
themes.should be_empty
end
it 'should return the expected themes' do
stub_request_to_fixture(url: @module.url, fixture: File.new(passive_detection_fixtures + '/various_themes.htm'))
expected_theme_names = %w{ theme1 theme2 theme3 }
expected_themes = []
expected_theme_names.each do |theme_name|
expected_themes << WpTheme.new(
base_url: @module.url,
path: "/themes/#{theme_name}/",
name: theme_name
)
end
themes = @module.themes_from_passive_detection(base_url: @module.url, wp_content_dir: 'wp-content')
themes.should_not be_empty
themes.length.should == expected_themes.length
themes.sort.should == expected_themes.sort
end
end
describe '#themes_from_aggressive_detection' do
before :each do
stub_request(:get, @module.uri.to_s).to_return(status: 200)
# Point all targets to a 404
@targets.each do |target|
stub_request(:get, target.get_full_url.to_s).to_return(status: 404)
# to_s calls readme_url
stub_request(:get, target.readme_url.to_s).to_return(status: 404)
end
end
after :each do
@passive_detection_fixture = SPEC_FIXTURES_DIR + '/empty-file' unless @passive_detection_fixture
stub_request_to_fixture(url: "#{@module.uri}/".sub(/\/\/$/, '/'), fixture: @passive_detection_fixture)
detected = @module.themes_from_aggressive_detection(@options)
detected.length.should == @expected_themes.length
detected.sort.should == @expected_themes.sort
end
it 'should return an empty array' do
@expected_themes = []
end
it 'should return an array with 3 WpTheme (1 detected from passive method)' do
@passive_detection_fixture = @fixtures_dir + '/passive_detection/one_theme.htm'
@expected_themes = @targets.sample(2)
@expected_themes.each do |p|
stub_request(:get, p.get_full_url.to_s).to_return(status: 200)
end
new_theme = WpTheme.new(
base_url: 'http://example.localhost/',
path: '/themes/custom-twentyten/',
name: 'custom-twentyten'
)
stub_request(:get, new_theme.readme_url.to_s).to_return(status: 200)
@expected_themes << new_theme
end
# testing response codes
WpTarget.valid_response_codes.each do |valid_response_code|
it "should detect the theme if the reponse.code is #{valid_response_code}" do
@expected_themes = []
theme_url = [@targets.sample(1)[0]]
theme_url.should_not be_nil
theme_url.length.should == 1
@expected_themes = theme_url
stub_request(:get, theme_url[0].get_full_url.to_s).to_return(status: valid_response_code)
end
end
end
end

View File

@@ -1,115 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
shared_examples_for 'WpTimthumbs' do
before :each do
@options = {}
@url = 'http://example.localhost/'
@theme_name = 'bueno'
@options[:base_url] = @url
@options[:wp_content_dir] = 'wp-content'
@options[:name] = @theme_name
@options[:error_404_hash] = 'xx'
@options[:show_progression] = false
@options[:only_vulnerable_ones] = false
@options[:vulns_file] = 'xx'
@options[:type] = 'timthumbs'
@module = WpScanModuleSpec.new(@url)
@fixtures_dir = SPEC_FIXTURES_WPSCAN_MODULES_DIR + '/wp_timthumbs'
@timthumbs_file = @fixtures_dir + '/timthumbs.txt'
@targets_from_file = %w{
http://example.localhost/wp-content/plugins/fotoslide/timthumb.php
http://example.localhost/wp-content/plugins/feature-slideshow/timthumb.php
}
@targets_from_theme =
[
'http://example.localhost/wp-content/themes/' + @theme_name + '/timthumb.php',
'http://example.localhost/wp-content/themes/' + @theme_name + '/lib/timthumb.php',
'http://example.localhost/wp-content/themes/' + @theme_name + '/inc/timthumb.php',
'http://example.localhost/wp-content/themes/' + @theme_name + '/includes/timthumb.php',
'http://example.localhost/wp-content/themes/' + @theme_name + '/scripts/timthumb.php',
'http://example.localhost/wp-content/themes/' + @theme_name + '/tools/timthumb.php',
'http://example.localhost/wp-content/themes/' + @theme_name + '/functions/timthumb.php'
]
@module.extend(WpTimthumbs)
end
describe '#targets_url_from_theme' do
it 'should return the targets for the theme' do
targets = @module.send(:targets_url_from_theme, @theme_name, @options)
targets.should_not be_empty
targets.length.should > 0
temp = []
targets.each do |t|
temp << t.get_full_url.to_s
end
temp.sort.should === @targets_from_theme.sort
end
end
describe '#timthumbs and #has_timthumbs?' do
before :each do
@options[:file] = @timthumbs_file
@targets_from_file.each do |url|
stub_request(:get, url).to_return(status: 404)
end
end
it 'should return an empty array' do
timthumbs = @module.timthumbs(nil, @options)
timthumbs.should be_empty
@module.has_timthumbs?(nil, @options).should be_false
end
it 'should return an array with 7 elements (from passive detection)' do
stub_request(:get, %r{http://example\.localhost/wp-content/themes/my-theme/.*}).to_return(status: 200)
timthumbs = @module.timthumbs('my-theme', @options)
timthumbs.length.should == 7
end
it 'should return an array with 2 timthumbs url' do
expected = []
urls = []
urls_hash = WpEnumerator.generate_items(@options)
urls_hash.each do |u|
url = u.get_full_url.to_s
urls << url
stub_request(:get, url).to_return(status: 404)
end
urls.sample(2).each do |target_url|
expected << target_url
stub_request(:get, target_url).
to_return(status: 200, body: File.new(@fixtures_dir + '/timthumb.php'))
end
timthumbs = @module.timthumbs(nil, @options)
timthumbs.should_not be_empty
temp = []
timthumbs.each do |t|
temp << t.get_full_url.to_s
end
temp.sort.should === expected.sort
@module.has_timthumbs?(nil).should be_true
end
end
end

View File

@@ -1,279 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
shared_examples_for 'WpUsernames' do
before :each do
@target_url = 'http://example.localhost/'
@module = WpScanModuleSpec.new(@target_url)
@fixtures_dir = SPEC_FIXTURES_WPSCAN_MODULES_DIR + '/wp_usernames'
@module.extend(WpUsernames)
end
describe '#author_url' do
it 'should return the auhor url according to his id' do
@module.author_url(1).should === "#@target_url?author=1"
end
end
describe '#usernames' do
before :each do
(1..10).each do |index|
stub_request(:get, @module.author_url(index)).to_return(status: 404)
end
end
it 'should return an empty array' do
@module.usernames.should be_empty
end
it 'should return an array with 1 username (from header location)' do
stub_request(:get, @module.author_url(3)).
to_return(status: 301, headers: {'location' => '/author/Youhou'})
usernames = @module.usernames
usernames.should_not be_empty
usernames.length.should == 1
usernames[0].id.should == 3
usernames[0].name.should == 'Youhou'
usernames[0].nickname.should == 'empty'
end
it 'should return an array with 1 username (from in the body response)' do
stub_request(:get, @module.author_url(2)).
to_return(status: 200, body: File.new(@fixtures_dir + '/test.html'))
usernames = @module.usernames(range: (1..2))
usernames.should_not be_empty
usernames.should === ([WpUser.new('test', 2, 'first last | user&#039;s Blog!')])
end
it 'should return an array with 2 usernames (one is a duplicate and should not be present twice)' do
stub_request(:get, @module.author_url(4)).
to_return(status: 301, headers: {'location' => '/author/Youhou/'})
stub_request(:get, @module.author_url(2)).
to_return(status: 200, body: File.new(@fixtures_dir + '/test.html'))
usernames = @module.usernames(range: (1..5))
usernames.should_not be_empty
expected = [
WpUser.new('test', 2, 'first last | user&#039;s Blog!'),
WpUser.new('Youhou', 4, 'empty')
]
usernames.sort_by { |u| u.name }.should === expected.sort_by { |u| u.name }
end
end
describe '#get_nickname_from_url' do
after :each do
url = 'http://example.localhost/'
stub_request(:get, url).to_return(status: @status, body: @content)
username = @module.get_nickname_from_url(url)
username.should === @expected
end
it 'should return nil' do
@status = 200
@content = ''
@expected = nil
end
it 'should return nil' do
@status = 400
@content = ''
@expected = nil
end
it 'should return admin' do
@status = 200
@content = '<title>admin</title>'
@expected = 'admin'
end
it 'should return nil' do
@status = 201
@content = '<title>admin</title>'
@expected = nil
end
end
describe '#get_nickname_from_response' do
after :each do
url = 'http://example.localhost/'
stub_request(:get, url).to_return(status: @status, body: @content)
resp = Browser.instance.get(url)
nickname = @module.get_nickname_from_response(resp)
nickname.should === @expected
end
it 'should return nil' do
@status = 200
@content = ''
@expected = nil
end
it 'should return nil' do
@status = 400
@content = ''
@expected = nil
end
it 'should return admin' do
@status = 200
@content = '<title>admin</title>'
@expected = 'admin'
end
it 'should return nil' do
@status = 201
@content = '<title>admin</title>'
@expected = nil
end
end
describe '#get_username_from_response' do
after :each do
url = @module.url
stub_request_to_fixture(url: url, fixture: File.new(@fixtures_dir + @file))
resp = Browser.instance.get(url)
username = @module.get_username_from_response(resp)
username.should === @expected
end
# No Permalinks
it 'should return admin' do
@file = '/admin.html'
@expected = 'admin'
end
# With Permalinks
it 'should return test' do
@file = '/test.html'
@expected = 'test'
end
end
describe '#extract_nickname_from_body' do
after :each do
result = @module.extract_nickname_from_body(@body)
result.should === @expected
end
it 'should return admin' do
@body = '<title>admin</title>'
@expected = 'admin'
end
it 'should return nil' do
@body = '<title>adm<in</title>'
@expected = nil
end
it 'should return nil' do
@body = '<titler>admin</titler>'
@expected = nil
end
it 'should return admin | ' do
@body = '<title>admin | </title>'
@expected = 'admin | '
end
it 'should return an empty string' do
@body = '<title></title>'
@expected = ''
end
end
describe '#remove_junk_from_nickname' do
it 'should throw an exception' do
@input = nil
expect { @module.remove_junk_from_nickname(@input) }.to raise_error(RuntimeError, 'Need an array as input')
end
it 'should not throw an exception' do
@input = []
expect { @module.remove_junk_from_nickname(@input) }.to_not raise_error
end
it 'should throw an exception' do
@input = [WpOptions.new]
expect { @module.remove_junk_from_nickname(@input) }.to raise_error(RuntimeError, 'Items must be of type WpUser')
end
end
describe '#remove_junk_from_nickname' do
after :each do
result = @module.remove_junk_from_nickname(@input)
result.should === @expected
end
it 'should return an empty array' do
@input = []
@expected = @input
end
it 'should return input object' do
@input = [WpUser.new(nil, nil, nil)]
@expected = @input
end
it 'should return input object' do
@input = [WpUser.new('', '', '')]
@expected = @input
end
it 'should remove asdf' do
@input = [WpUser.new(nil, nil, 'lkjh asdf'), WpUser.new(nil, nil, 'ijrjd asdf')]
@expected = [WpUser.new(nil, nil, 'lkjh'), WpUser.new(nil, nil, 'ijrjd')]
end
it 'should return unmodified input object' do
@input = [WpUser.new(nil, nil, 'lkjh asdfa'), WpUser.new(nil, nil, 'ijrjd asdf')]
@expected = @input
end
it 'should return input object' do
@input = [WpUser.new(nil, nil, 'lkjh asdf')]
@expected = @input
end
it 'should return lkhj asdf' do
@input = [WpUser.new(nil, nil, 'lkhj asdf'), WpUser.new(nil, nil, 'lkhj asdf')]
@expected = [WpUser.new(nil, nil, ''), WpUser.new(nil, nil, '')]
end
end
# Issue 66
describe '#remove_junk_from_nickname' do
it 'should contain the string empty' do
input = [WpUser.new('admin', 1, 'admin | Wordpress 3.4.2'), WpUser.new('', 2, 'Wordpress 3.4.2')]
result = @module.remove_junk_from_nickname(input)
result[0].nickname.should === 'admin | '
result[0].name.should === 'admin'
result[0].id.should === 1
result[1].nickname.should === 'empty'
result[1].name.should === 'empty'
result[1].id.should === 2
end
end
end

View File

@@ -1,24 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpDetector do
# TODO
end

View File

@@ -1,24 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpEnumerator do
# TODO
end

View File

@@ -1,587 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpPlugin do
describe '#initialize' do
it 'should create a correct instance' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
instance.wp_content_dir.should == 'wp-content'
instance.base_url.should == 'http://sub.example.com/path/to/wordpress/'
instance.path.should == 'test/asdf.php'
end
end
describe '#get_full_url' do
after :each do
arguments = {
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins',
wp_content_dir: @wp_content_dir
}
instance = WpItem.new(arguments)
instance.get_full_url.to_s.should === @expected
end
it 'should return the correct url' do
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/plugins/test/asdf.php'
end
it 'should return the correct url (custom wp_content_dir)' do
@wp_content_dir = 'custom'
@expected = 'http://sub.example.com/path/to/wordpress/custom/plugins/test/asdf.php'
end
it 'should trim / and add missing / before concatenating url' do
@wp_content_dir = '/custom/'
@expected = 'http://sub.example.com/path/to/wordpress/custom/plugins/test/asdf.php'
end
end
describe '#get_url_without_filename' do
after :each do
arguments = {
base_url: @base_url || 'http://sub.example.com/path/to/wordpress/',
path: @path || 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins',
wp_content_dir: @wp_content_dir
}
instance = WpItem.new(arguments)
instance.get_url_without_filename.to_s.should === @expected
end
it 'should return the correct url' do
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/plugins/test/'
end
it 'should return the correct url (custom wp_content_dir)' do
@wp_content_dir = 'custom'
@expected = 'http://sub.example.com/path/to/wordpress/custom/plugins/test/'
end
it 'should trim / and add missing / before concatenating url' do
@wp_content_dir = '/custom/'
@expected = 'http://sub.example.com/path/to/wordpress/custom/plugins/test/'
end
it 'should not remove the last foldername' do
@path = 'test/'
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/plugins/test/'
end
it 'should return the correct url (https)' do
@base_url = 'https://sub.example.com/path/to/wordpress/'
@expected = 'https://sub.example.com/path/to/wordpress/wp-content/plugins/test/'
end
it "should add the last slash if it's not present" do
@path = 'test-one'
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/plugins/test-one/'
end
end
describe '#version' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_PLUGIN_DIR + '/version' }
before :each do
@instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
end
it 'should return a version number' do
stub_request(:get, @instance.readme_url.to_s).to_return(status: 200, body: 'Stable tag: 1.2.4.3.2.1')
@instance.version.should == '1.2.4.3.2.1'
end
it 'should not return a version number' do
stub_request(:get, @instance.readme_url.to_s).to_return(status: 200, body: 'Stable tag: trunk')
@instance.version.should be nil
end
it 'should return nil if the version is invalid (IE : trunk etc)' do
stub_request_to_fixture(url: @instance.readme_url.to_s, fixture: fixtures_dir + '/trunk-version.txt')
@instance.version.should be_nil
end
it 'should return the version 0.4' do
stub_request_to_fixture(url: @instance.readme_url.to_s, fixture: fixtures_dir + '/simple-login-lockdown-0.4.txt')
@instance.version.should === '0.4'
end
end
describe '#directory_listing?' do
before :each do
@instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
end
it 'should return true' do
stub_request(:get, @instance.get_url_without_filename.to_s)
.to_return(status: 200, body: '<html><head><title>Index of asdf</title></head></html>')
@instance.directory_listing?.should == true
end
it 'should return false' do
stub_request(:get, @instance.get_url_without_filename.to_s)
.to_return(status: 200, body: '<html><head><title>My Wordpress Site</title></head></html>')
@instance.directory_listing?.should == false
end
it 'should return false on a 404' do
stub_request(:get, @instance.get_url_without_filename.to_s.to_s).to_return(status: 404)
@instance.directory_listing?.should be_false
end
end
describe '#extract_name_from_url' do
after :each do
arguments = {
base_url: 'http://sub.example.com/path/to/wordpress/',
path: @path || 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: @type || 'plugins',
wp_content_dir: @wp_content_dir
}
instance = WpItem.new(arguments)
instance.extract_name_from_url.should === @expected
end
it 'should extract the correct name' do
@expected = 'test'
end
it 'should extract the correct name (custom wp_content_dir)' do
@wp_content_dir = 'custom'
@expected = 'test'
end
it 'should extract the correct name' do
@path = 'test2/asdf.php'
@wp_content_dir = '/custom/'
@expected = 'test2'
end
it 'should extract the correct plugin name' do
@path = 'testplugin/'
@expected = 'testplugin'
end
it 'should extract the correct theme name' do
@path = 'testtheme/'
@type = 'themes'
@expected = 'testtheme'
end
end
describe '#to_s' do
before :each do
@instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
end
it 'should return the name including a version number' do
stub_request(:get, @instance.readme_url.to_s).to_return(status: 200, body: 'Stable tag: 1.2.4.3.2.1')
@instance.to_s.should == 'test v1.2.4.3.2.1'
end
it 'should not return the name without a version number' do
stub_request(:get, @instance.readme_url.to_s).to_return(status: 200, body: 'Stable tag: trunk')
@instance.to_s.should == 'test'
end
end
describe '#==' do
before :each do
@instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
end
it 'should return false' do
instance2 = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'newname/asdf.php',
type: 'plugins',
vulns_file: 'XXX.xml',
vulns_xpath: 'XX'
)
(@instance == instance2).should == false
end
it 'should return true' do
instance2 = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
type: 'plugins',
vulns_file: 'XXX.xml',
vulns_xpath: 'XX'
)
(@instance == instance2).should == true
end
end
describe '#get_sub_folder' do
after :each do
arguments = {
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
wp_content_dir: 'wp-content',
wp_plugins_dir: 'wp-content/plugins',
name: 'test',
vulns_xpath: 'XX',
type: @type || 'themes'
}
instance = WpItem.new(arguments)
if @raise_error
expect { instance.get_sub_folder }.to @raise_error
else
instance.get_sub_folder.should === @expected
end
end
it 'should return themes' do
@expected = 'themes'
end
it 'should return nil' do
@type = 'timthumbs'
@expected = nil
end
it 'should raise an exception' do
@type = 'type'
@raise_error = raise_error(RuntimeError, 'unknown type type')
end
end
describe '#readme_url' do
after :each do
arguments = {
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: @type || 'plugins',
wp_content_dir: @wp_content_dir
}
instance = WpItem.new(arguments)
instance.readme_url.to_s.should === @expected
end
it 'should return the corrent plugin readme url' do
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/plugins/test/readme.txt'
end
it 'should return the corrent plugin readme url (custom wp_content)' do
@wp_content_dir = 'custom'
@expected = 'http://sub.example.com/path/to/wordpress/custom/plugins/test/readme.txt'
end
it 'should return the corrent theme readme url' do
@type = 'themes'
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/themes/test/readme.txt'
end
it 'should return the corrent theme readme url (custom wp_content)' do
@type = 'themes'
@wp_content_dir = 'custom'
@expected = 'http://sub.example.com/path/to/wordpress/custom/themes/test/readme.txt'
end
end
describe '#changelog_url' do
after :each do
arguments = {
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: @type || 'plugins',
wp_content_dir: @wp_content_dir
}
instance = WpItem.new(arguments)
instance.changelog_url.to_s.should === @expected
end
it 'should return the corrent plugin changelog url' do
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/plugins/test/changelog.txt'
end
it 'should return the corrent plugin changelog url (custom wp_content)' do
@wp_content_dir = 'custom'
@expected = 'http://sub.example.com/path/to/wordpress/custom/plugins/test/changelog.txt'
end
it 'should return the corrent theme changelog url' do
@type = 'themes'
@expected = 'http://sub.example.com/path/to/wordpress/wp-content/themes/test/changelog.txt'
end
it 'should return the corrent theme changelog url (custom wp_content)' do
@type = 'themes'
@wp_content_dir = 'custom'
@expected = 'http://sub.example.com/path/to/wordpress/custom/themes/test/changelog.txt'
end
end
describe '#has_readme?' do
before :each do
@instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
end
it 'should return true' do
stub_request(:get, @instance.readme_url.to_s).to_return(status: 200)
@instance.has_readme?.should == true
end
it 'should return false' do
stub_request(:get, @instance.readme_url.to_s).to_return(status: 403)
@instance.has_readme?.should == false
end
end
describe '#has_changelog?' do
before :each do
@instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
end
it 'should return true' do
stub_request(:get, @instance.changelog_url.to_s).to_return(status: 200)
@instance.has_changelog?.should == true
end
it 'should return false' do
stub_request(:get, @instance.changelog_url.to_s).to_return(status: 403)
@instance.has_changelog?.should == false
end
end
describe '#wp_org_url' do
it 'sould return a themes url' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'themes'
)
instance.wp_org_url.to_s.should == 'http://wordpress.org/extend/themes/test/'
end
it 'sould return a plugins url' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'plugins'
)
instance.wp_org_url.to_s.should == 'http://wordpress.org/extend/plugins/test/'
end
it 'sould raise an exception' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'invalid'
)
expect { instance.wp_org_url }.to raise_error(RuntimeError, 'No Wordpress URL for invalid')
end
end
describe '#wp_org_item?' do
it 'sould return true' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'w3-total-cache',
vulns_xpath: 'XX',
type: 'plugins'
)
instance.wp_org_item?.should be_true
end
it 'sould return true' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'twentyten',
vulns_xpath: 'XX',
type: 'themes'
)
instance.wp_org_item?.should be_true
end
it 'sould return false' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'can_not_be_in_repository',
vulns_xpath: 'XX',
type: 'plugins'
)
instance.wp_org_item?.should be_false
end
it 'sould return false' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'can_not_be_in_repository',
vulns_xpath: 'XX',
type: 'themes'
)
instance.wp_org_item?.should be_false
end
it 'sould raise an exception' do
instance = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'test',
vulns_xpath: 'XX',
type: 'invalid'
)
expect { instance.wp_org_item? }.to raise_error(RuntimeError, 'Unknown type invalid')
end
end
describe '#error_log_url' do
it 'should return a correct url' do
temp = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'name/asdf.php',
vulns_file: 'XXX.xml',
name: 'name',
vulns_xpath: 'XX',
type: 'plugins')
temp.error_log_url.to_s.should == 'http://sub.example.com/path/to/wordpress/wp-content/plugins/name/error_log'
end
end
describe '#error_log?' do
before :each do
@temp = WpItem.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'test/asdf.php',
vulns_file: 'XXX.xml',
name: 'name',
vulns_xpath: 'XX',
type: 'plugins')
end
it 'should return true' do
stub_request(:get, @temp.error_log_url.to_s).to_return(status: 200, body: 'PHP Fatal error')
@temp.error_log?.should be true
end
it 'should return false' do
stub_request(:get, @temp.error_log_url.to_s).to_return(status: 500, body: 'Access denied')
@temp.error_log?.should be false
end
it 'should return true' do
fixtures_dir = SPEC_FIXTURES_WPSCAN_WP_PLUGIN_DIR + '/error_log'
stub_request(:get, @temp.error_log_url.to_s).to_return(
status: 200,
body: File.new(fixtures_dir + '/error_log')
)
@temp.error_log?.should be true
end
end
end

View File

@@ -1,139 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpOptions do
describe '#check_options' do
before :each do
@options = {}
@options[:base_url] = 'url'
@options[:only_vulnerable_ones] = false
@options[:file] = 'file'
@options[:vulns_file] = 'vulns_file'
@options[:vulns_xpath] = 'vulns_xpath'
@options[:vulns_xpath_2] = 'vulns_xpath_2'
@options[:wp_content_dir] = 'wp_content_dir'
@options[:show_progression] = true
@options[:error_404_hash] = 'error_404_hash'
@options[:type] = 'type'
@message = ''
end
after :each do
expect { WpOptions.check_options(@options) }.to raise_error(RuntimeError, @message)
end
it 'should raise an exception (base_url empty)' do
@options[:base_url] = ''
@message = 'base_url must be set'
end
it 'should raise an exception (base_url nil)' do
@options[:base_url] = nil
@message = 'base_url must be set'
end
it 'should raise an exception (only_vulnerable_ones nil)' do
@options[:only_vulnerable_ones] = nil
@message = 'only_vulnerable_ones must be set'
end
it 'should raise an exception (file empty)' do
@options[:file] = ''
@message = 'file must be set'
end
it 'should raise an exception (file nil)' do
@options[:file] = nil
@message = 'file must be set'
end
it 'should raise an exception (vulns_file empty)' do
@options[:vulns_file] = ''
@message = 'vulns_file must be set'
end
it 'should raise an exception (vulns_file nil)' do
@options[:vulns_file] = nil
@message = 'vulns_file must be set'
end
it 'should raise an exception (vulns_xpath empty)' do
@options[:vulns_xpath] = ''
@message = 'vulns_xpath must be set'
end
it 'should raise an exception (vulns_xpath nil)' do
@options[:vulns_xpath] = nil
@message = 'vulns_xpath must be set'
end
it 'should raise an exception (vulns_xpath_2 empty)' do
@options[:vulns_xpath_2] = ''
@message = 'vulns_xpath_2 must be set'
end
it 'should raise an exception (vulns_xpath_2 nil)' do
@options[:vulns_xpath_2] = nil
@message = 'vulns_xpath_2 must be set'
end
it 'should raise an exception (wp_content_dir empty)' do
@options[:wp_content_dir] = ''
@message = 'wp_content_dir must be set'
end
it 'should raise an exception (wp_content_dir nil)' do
@options[:wp_content_dir] = nil
@message = 'wp_content_dir must be set'
end
it 'should raise an exception (show_progression nil)' do
@options[:show_progression] = nil
@message = 'show_progression must be set'
end
it 'should raise an exception (error_404_hash empty)' do
@options[:error_404_hash] = ''
@message = 'error_404_hash must be set'
end
it 'should raise an exception (error_404_hash nil)' do
@options[:error_404_hash] = nil
@message = 'error_404_hash must be set'
end
it 'should raise an exception (type empty)' do
@options[:type] = ''
@message = 'type must be set'
end
it 'should raise an exception (type nil)' do
@options[:type] = nil
@message = 'type must be set'
end
it 'should raise an exception (type unknown)' do
@options[:type] = 'unknown'
@message = 'Unknown type unknown'
end
end
end

View File

@@ -1,44 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpPlugin do
describe '#initialize' do
it 'should not raise an exception' do
expect { WpPlugin.new(base_url: 'url', path: 'path', wp_content_dir: 'dir', name: 'name') }.to_not raise_error
end
it 'should not raise an exception (wp_content_dir not set)' do
expect { WpPlugin.new(base_url: 'url', path: 'path', name: 'name') }.to_not raise_error
end
it 'should raise an exception (base_url not set)' do
expect { WpPlugin.new(path: 'path', wp_content_dir: 'dir', name: 'name') }.to raise_error
end
it 'should raise an exception (path not set)' do
expect { WpPlugin.new(base_url: 'url', wp_content_dir: 'dir', name: 'name') }.to raise_error
end
it 'should raise an exception (name not set)' do
expect { WpPlugin.new(base_url: 'url', path: 'path', wp_content_dir: 'dir') }.to raise_error
end
end
end

View File

@@ -41,10 +41,6 @@ describe WpTarget do
it_should_behave_like 'WpLoginProtection'
it_should_behave_like 'Malwares'
it_should_behave_like 'BruteForce'
it_should_behave_like 'WpUsernames'
it_should_behave_like 'WpTimthumbs'
it_should_behave_like 'WpPlugins'
it_should_behave_like 'WpThemes'
describe '#initialize' do
it 'should raise an error if the target_url is nil or empty' do

View File

@@ -1,289 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpTheme do
before :all do
@target_uri = URI.parse('http://example.localhost/')
Browser.instance(
config_file: SPEC_FIXTURES_CONF_DIR + '/browser/browser.conf.json',
cache_timeout: 0
)
end
describe '#initialize' do
it 'should not raise an exception' do
expect { WpTheme.new(base_url: 'url', path: 'path', wp_content_dir: 'dir', name: 'name') }.to_not raise_error
end
it 'should not raise an exception (wp_content_dir not set)' do
expect { WpTheme.new(base_url: 'url', path: 'path', name: 'name') }.to_not raise_error
end
it 'should raise an exception (base_url not set)' do
expect { WpTheme.new(path: 'path', wp_content_dir: 'dir', name: 'name') }.to raise_error
end
it 'should raise an exception (path not set)' do
expect { WpTheme.new(base_url: 'url', wp_content_dir: 'dir', name: 'name') }.to raise_error
end
it 'should raise an exception (name not set)' do
expect { WpTheme.new(base_url: 'url', path: 'path', wp_content_dir: 'dir') }.to raise_error
end
end
describe '#find_from_css_link' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_THEME_DIR + '/find/css_link' }
after :each do
if @expected_name
stub_request_to_fixture(url: @target_uri.to_s, fixture: @fixture)
wp_theme = WpTheme.find_from_css_link(@target_uri)
wp_theme.should be_a WpTheme
wp_theme.name.should === @expected_name
end
end
it 'should return nil if no theme is present' do
stub_request(:get, @target_uri.to_s).to_return(status: 200, body: '')
WpTheme.find_from_css_link(@target_uri).should be_nil
end
it 'should return a WpTheme object with .name = twentyeleven' do
@fixture = fixtures_dir + '/wordpress-twentyeleven.htm'
@expected_name = 'twentyeleven'
end
# http://code.google.com/p/wpscan/issues/detail?id=131
# Theme name with spaces raises bad URI(is not URI?)
it 'should not raise an error if the theme name has spaces or special chars' do
@fixture = fixtures_dir + '/theme-name-with-spaces.html'
@expected_name = 'Copia di simplefolio'
end
# https://github.com/wpscanteam/wpscan/issues/18
it 'should get the theme if the <link> is inline with some other tags' do
@fixture = fixtures_dir + '/inline_link_tag.html'
@expected_name = 'inline'
end
it 'should get the theme name even if relative URLs are used' do
@fixture = fixtures_dir + '/relative_urls.html'
@expected_name = 'theme_name'
end
end
describe '#find_from_wooframework' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_THEME_DIR + '/find/wooframework' }
after :each do
stub_request_to_fixture(url: @target_uri.to_s, fixture: @fixture)
wp_theme = WpTheme.find_from_wooframework(@target_uri)
stub_request(:get, wp_theme.default_style_url.to_s).to_return(status: 200)
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 200)
wp_theme.should be_a WpTheme unless wp_theme.nil?
wp_theme.should === @expected_theme
end
it "should return a WpTheme object with .name 'Editorial' and .version '1.3.5'" do
@fixture = fixtures_dir + '/editorial-1.3.5.html'
@expected_theme = WpTheme.new(name: 'Editorial', version: '1.3.5', base_url: 'http://example.localhost/', path: 'Editorial')
end
it "should return a WpTheme object with .name 'Merchant'" do
@fixture = fixtures_dir + '/merchant-no-version.html'
@expected_theme = WpTheme.new(name: 'Merchant', base_url: 'http://example.localhost/', path: 'Merchant')
end
end
describe '#find' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_THEME_DIR + '/find' }
after :each do
stub_request_to_fixture(url: @target_uri.to_s, fixture: @fixture)
wp_theme = WpTheme.find(@target_uri)
if @expected_name
wp_theme.should be_a WpTheme
wp_theme.name.should === @expected_name
else
wp_theme.should be_nil
end
end
it 'should return nil if no theme is found' do
@fixture = SPEC_FIXTURES_DIR + '/empty-file'
@expected_name = nil
end
it "should return a WpTheme object with .name 'twentyeleven'" do
@fixture = fixtures_dir + '/css_link/wordpress-twentyeleven.htm'
@expected_name = 'twentyeleven'
end
it "should a WpTheme object with .name 'Merchant'" do
@fixture = fixtures_dir + '/wooframework/merchant-no-version.html'
@expected_name = 'Merchant'
end
end
describe '#version' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_THEME_DIR + '/version' }
let(:theme_style_url) { @target_uri.merge('wp-content/themes/spec-theme/style.css').to_s }
after :each do
if @fixture
stub_request_to_fixture(url: theme_style_url, fixture: @fixture)
wp_theme = WpTheme.new(name: 'spec-theme', style_url: theme_style_url, base_url: 'http://example.localhost/', path: 'spec-theme')
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 200)
wp_theme.version.should === @expected
end
end
it 'should return nil if the version is not found' do
@fixture = fixtures_dir + '/twentyeleven-unknow.css'
@expected = nil
end
it 'should return nil if the style_url is nil' do
wp_theme = WpTheme.new(name: 'hello-world', base_url: 'http://example.localhost/', path: 'hello-world')
stub_request(:get, wp_theme.default_style_url.to_s).to_return(status: 200)
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 200)
wp_theme.version.should be_nil
end
it 'should return 1.3' do
@fixture = fixtures_dir + '/twentyeleven-1.3.css'
@expected = '1.3'
end
it 'should return 1.5.1' do
@fixture = fixtures_dir + '/bueno-1.5.1.css'
@expected = '1.5.1'
end
it 'should get the version from default style.css url' do
wp_theme = WpTheme.new(name: 'hello-world', base_url: 'http://example.localhost/', path: 'hello-world')
stub_request(:get, wp_theme.default_style_url.to_s).to_return(status: 200, body: 'Version: 1.3.4.5')
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 404)
wp_theme.version.should === '1.3.4.5'
end
it 'should get the version from custom style.css url' do
style_url = 'http://example.localhost/custom_style.css'
wp_theme = WpTheme.new(name: 'hello-world', base_url: 'http://example.localhost/', path: 'hello-world', style_url: style_url)
stub_request(:get, style_url).to_return(status: 200, body: 'Version: 1.3.4.5')
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 404)
wp_theme.version.should === '1.3.4.5'
end
it 'should get the version from readme.txt' do
wp_theme = WpTheme.new(name: 'hello-world', base_url: 'http://example.localhost/', path: 'hello-world')
stub_request(:get, wp_theme.default_style_url.to_s).to_return(status: 404)
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 200, body: 'Stable Tag: 1.2.3.4')
wp_theme.version.should === '1.2.3.4'
end
it 'should get the version from readme.txt' do
wp_theme = WpTheme.new(name: 'hello-world', base_url: 'http://example.localhost/', path: 'hello-world')
stub_request(:get, wp_theme.default_style_url.to_s).to_return(status: 200)
stub_request(:get, wp_theme.readme_url.to_s).to_return(status: 200, body: 'Stable Tag: 1.2.3.4')
wp_theme.version.should === '1.2.3.4'
end
end
describe '#===' do
it 'should return false (name not equal)' do
instance = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/name/asdf.php',
vulns_file: 'XXX.xml',
version: '1.0'
)
instance2 = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/newname/asdf.php',
vulns_file: 'XXX.xml',
version: '1.0'
)
(instance === instance2).should == false
end
it 'should return false (version not equal)' do
instance = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/name/asdf.php',
vulns_file: 'XXX.xml',
version: '1.0'
)
instance2 = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/name/asdf.php',
vulns_file: 'XXX.xml',
version: '2.0'
)
(instance === instance2).should == false
end
it 'should return false (version and name not equal)' do
instance = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/name/asdf.php',
vulns_file: 'XXX.xml',
version: '1.0'
)
instance2 = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/newname/asdf.php',
vulns_file: 'XXX.xml',
version: '2.0'
)
(instance === instance2).should == false
end
it 'should return true' do
instance = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/test/asdf.php',
vulns_file: 'XXX.xml',
version: '1.0'
)
instance2 = WpTheme.new(
base_url: 'http://sub.example.com/path/to/wordpress/',
path: 'themes/test/asdf.php',
vulns_file: 'XXX.xml',
version: '1.0'
)
(instance === instance2).should == true
end
end
end

View File

@@ -1,86 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpUser do
describe '#initialize' do
it 'should replace nil with empty' do
user = WpUser.new(nil, nil, nil)
user.name.should == 'empty'
user.id.should == 'empty'
user.nickname == 'empty'
end
it 'should initialize a user object' do
user = WpUser.new('name', 'id', 'nickname')
user.name.should == 'name'
user.id.should == 'id'
user.nickname == 'nickname'
end
end
describe '#<=>' do
it 'should return -1' do
user1 = WpUser.new('b', nil, nil)
user2 = WpUser.new('a', nil, nil)
(user1 <=> user2).should === -1
end
it 'should return 0' do
user1 = WpUser.new('a', nil, nil)
user2 = WpUser.new('a', nil, nil)
(user1 <=> user2).should === 0
end
it 'should return 1' do
user1 = WpUser.new('a', nil, nil)
user2 = WpUser.new('b', nil, nil)
(user1 <=> user2).should === 1
end
end
describe '#===' do
it 'should return true' do
user1 = WpUser.new('a', 'id', 'nick')
user2 = WpUser.new('a', 'id', 'nick')
(user1 === user2).should be_true
end
it 'should return false' do
user1 = WpUser.new('a', 'id', 'nick')
user2 = WpUser.new('b', 'id', 'nick')
(user1 === user2).should be_false
end
end
describe '#eql?' do
it 'should return true' do
user1 = WpUser.new('a', 'id', 'nick')
user2 = WpUser.new('a', 'id', 'nick')
(user1.eql? user2).should be_true
end
it 'should return false' do
user1 = WpUser.new('a', 'id', 'nick')
user2 = WpUser.new('b', 'id', 'nick')
(user1.eql? user2).should be_false
end
end
end

View File

@@ -1,306 +0,0 @@
# encoding: UTF-8
#--
# WPScan - WordPress Security Scanner
# Copyright (C) 2012-2013
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
require File.expand_path(File.dirname(__FILE__) + '/wpscan_helper')
describe WpVersion do
before :all do
@target_uri = URI.parse('http://example.localhost/')
@browser = Browser.instance(config_file: SPEC_FIXTURES_CONF_DIR + '/browser/browser.conf.json')
end
describe '#find_from_meta_generator' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/meta-generator' }
after :each do
stub_request_to_fixture(url: @target_uri.to_s, fixture: @fixture)
WpVersion.find_from_meta_generator(base_uri: @target_uri.to_s).should === @expected
end
it 'should return nil if the meta-generator is not found' do
@fixture = fixtures_dir + '/no-meta-generator.htm'
@expected = nil
end
it 'should return 3.3.2' do
@fixture = fixtures_dir + '/3.3.2.htm'
@expected = '3.3.2'
end
it 'should return 3.4-beta4' do
@fixture = fixtures_dir + '/3.4-beta4.htm'
@expected = '3.4-beta4'
end
it "should return nil if it's not a valid version, must contains at least one '.'" do
@fixture = fixtures_dir + '/invalid_version.htm'
@expected = nil
end
it 'should return 3.5' do
@fixture = fixtures_dir + '/3.5_minified.htm'
@expected = '3.5'
end
end
describe '#find_from_rss_generator' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/rss-generator' }
after :each do
@status_code ||= 200
stub_request_to_fixture(url: @target_uri.merge('feed/').to_s, status: @status_code, fixture: @fixture)
WpVersion.find_from_rss_generator(base_uri: @target_uri).should === @expected
end
it 'should return nil on a 404' do
@status_code = 404
@fixture = SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/404.htm'
@expected = nil
end
it 'should return nil if the rss-generator is not found' do
@fixture = fixtures_dir + '/no-rss-generator.htm'
@expected = nil
end
it 'should return nil if the version is not found (but the rss-generator is present)' do
@fixture = fixtures_dir + '/no-version.htm'
@expected = nil
end
it 'shuld return 3.3.2' do
@fixture = fixtures_dir + '/3.3.2.htm'
@expected = '3.3.2'
end
it 'should return 3.4-beta4' do
@fixture = fixtures_dir + '/3.4-beta4.htm'
@expected = '3.4-beta4'
end
it "should return nil if it's not a valid version, must contains at least one '.'" do
@fixture = fixtures_dir + '/invalid_version.htm'
@expected = nil
end
end
describe '#find_from_rdf_generator' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/rdf-generator' }
after :each do
@status_code ||= 200
stub_request_to_fixture(url: @target_uri.merge('feed/rdf/').to_s, status: @status_code, fixture: @fixture)
WpVersion.find_from_rdf_generator(base_uri: @target_uri).should === @expected
end
it 'should return nil on a 404' do
@status_code = 404
@fixture = SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/404.htm'
@expected = nil
end
it 'should return nil if the rdf-generator is not found' do
@fixture = fixtures_dir + '/no-rdf-generator.htm'
@expected = nil
end
it 'should return nil if the version is not found (but the rdf-generator is present)' do
@fixture = fixtures_dir + '/no-version.htm'
@expected = nil
end
it 'shuld return 3.3.2' do
@fixture = fixtures_dir + '/3.3.2.htm'
@expected = '3.3.2'
end
it 'should return 3.4-beta4' do
@fixture = fixtures_dir + '/3.4-beta4.htm'
@expected = '3.4-beta4'
end
it "should return nil if it's not a valid version, must contains at least one '.'" do
@fixture = fixtures_dir + '/invalid_version.htm'
@expected = nil
end
end
describe '#find_from_atom_generator' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/atom-generator' }
after :each do
@status_code ||= 200
stub_request_to_fixture(url: @target_uri.merge('feed/atom/').to_s, status: @status_code, fixture: @fixture)
WpVersion.find_from_atom_generator(base_uri: @target_uri).should === @expected
end
it 'should return nil on a 404' do
@status_code = 404
@fixture = SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/404.htm'
@expected = nil
end
it 'should return nil if the atom-generator is not found' do
@fixture = fixtures_dir + '/no-atom-generator.htm'
@expected = nil
end
it 'should return nil if the version is not found (but the atom-generator is present)' do
@fixture = fixtures_dir + '/no-version.htm'
@expected = nil
end
it 'shuld return 3.3.2' do
@fixture = fixtures_dir + '/3.3.2.htm'
@expected = '3.3.2'
end
it 'should return 3.4-beta4' do
@fixture = fixtures_dir + '/3.4-beta4.htm'
@expected = '3.4-beta4'
end
it "should return nil if it's not a valid version, must contains at least one '.'" do
@fixture = fixtures_dir + '/invalid_version.htm'
@expected = nil
end
end
describe '#find_from_sitemap_generator' do
after :each do
stub_request(:get, @target_uri.merge('sitemap.xml').to_s).
to_return(status: 200, body: @body)
WpVersion.find_from_sitemap_generator(base_uri: @target_uri).should === @expected
end
it 'should return nil if the generator is not found' do
@body = ''
@expected = nil
end
it 'should return the version : 3.3.2' do
@body = '<!-- generator="wordpress/3.3.2" -->'
@expected = '3.3.2'
end
it "should return nil if it's not a valid version, must contains at least one '.'" do
@body = '<!-- generator="wordpress/5065" -->'
@expected = nil
end
end
describe '#find_from_readme' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/readme' }
after :each do
@status_code ||= 200
stub_request_to_fixture(url: @target_uri.merge('readme.html').to_s, status: @status_code, fixture: @fixture)
WpVersion.find_from_readme(base_uri: @target_uri).should === @expected
end
it 'should return nil on a 404' do
@status_code = 404
@fixture = SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/404.htm'
@expected = nil
end
it 'should return nil if the version number is not present' do
@fixture = fixtures_dir + '/empty-version.html'
@expected = nil
end
it 'should return 3.3.2' do
@fixture = fixtures_dir + '/readme-3.3.2.html'
@expected = '3.3.2'
end
it "should return nil if it's not a valid version, must contains at least one '.'" do
@fixture = fixtures_dir + '/invalid_version.html'
@expected = nil
end
end
describe '#find_from_advanced_fingerprinting' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/advanced' }
it 'should return 3.2.1' do
stub_request_to_fixture(
url: @target_uri.merge('wp-admin/js/wp-fullscreen.js').to_s,
fixture: "#{fixtures_dir}/3.2.1.js"
)
version = WpVersion.find_from_advanced_fingerprinting(
base_uri: @target_uri,
wp_content_dir: 'wp-content',
version_xml: "#{fixtures_dir}/wp_versions.xml"
)
version.should == '3.2.1'
end
end
describe '#find_from_links_opml' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/opml' }
it 'should return 3.4.2' do
stub_request_to_fixture(
url: @target_uri.merge('wp-links-opml.php').to_s,
fixture: "#{fixtures_dir}/wp-links-opml.xml"
)
version = WpVersion.find_from_links_opml(base_uri: @target_uri)
version.should == '3.4.2'
end
it 'should return nil' do
stub_request_to_fixture(
url: @target_uri.merge('wp-links-opml.php').to_s,
fixture: "#{fixtures_dir}/wp-links-opml-nogenerator.xml"
)
version = WpVersion.find_from_links_opml(base_uri: @target_uri)
version.should be_nil
end
end
describe '#initialize' do
it 'should initialize a WpVersion object' do
v = WpVersion.new(1, {discovery_method: 'method', vulns_file: 'asdf.xml'})
v.number.should == 1
v.discovery_method.should == 'method'
end
end
describe '#find' do
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_VERSION_DIR + '/advanced' }
it 'should find all versions' do
# All requests get a HTTP 404
stub_request(:any, /.*/).to_return(status: 404)
# Wordpress Version 3.2.1
stub_request_to_fixture(
url: @target_uri.merge('wp-admin/js/wp-fullscreen.js').to_s,
fixture: "#{fixtures_dir}/3.2.1.js"
)
version = WpVersion.find(@target_uri, 'wp-content')
version.number.should == '3.2.1'
version.discovery_method.should == 'advanced fingerprinting'
end
end
end

View File

@@ -1,37 +0,0 @@
# encoding: UTF-8
# TODO
describe '#vulnerabilities' do
let(:location_url) { 'http://example.localhost/' }
let(:fixtures_dir) { SPEC_FIXTURES_WPSCAN_WP_PLUGIN_DIR + '/vulnerabilities' }
let(:vulns_file) { fixtures_dir + '/plugin_vulns.xml' }
let(:wp_plugin) do
WpPlugin.new(
base_url: location_url,
name: 'spec-plugin',
path: 'plugins/spec-plugin/',
vulns_file: vulns_file
)
end
it 'should return an empty array when no vulnerabilities are found' do
WpPlugin.new(
base_url: 'http://example.localhost/',
name: 'no-vulns',
path: 'plugins/no-vulns/',
vulns_file: vulns_file
).vulnerabilities.should be_empty
end
it 'should return an arry with 2 vulnerabilities' do
vulnerabilities = wp_plugin.vulnerabilities
vulnerabilities.should_not be_empty
vulnerabilities.length.should == 2
vulnerabilities.each { |vulnerability| vulnerability.should be_a WpVulnerability }
vulnerabilities[0].title.should === 'WPScan Spec'
vulnerabilities[1].title.should === 'Spec SQL Injection'
end
end

View File

@@ -54,17 +54,15 @@ describe 'StatsPlugin' do
describe '#total_plugins' do
it 'should return the correct numer' do
xml = "#{SPEC_FIXTURES_WPSCAN_WP_PLUGIN_DIR}/vulnerabilities/plugin_vulns.xml"
file = "#{SPEC_FIXTURES_WPSCAN_WP_PLUGIN_DIR}/plugins.txt"
@stats.total_plugins(file, xml).should == 4
@stats.total_plugins(file).should == 4
end
end
describe '#total_themes' do
it 'should return the correct numer' do
xml = "#{SPEC_FIXTURES_WPSCAN_WP_THEME_DIR}/vulnerabilities/theme_vulns.xml"
file = "#{SPEC_FIXTURES_WPSCAN_WP_THEME_DIR}/themes.txt"
@stats.total_themes(file, xml).should == 5
@stats.total_themes(file).should == 5
end
end
end
end