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,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