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

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