WpTarget modules reworked

This commit is contained in:
erwanlr
2013-04-08 18:22:06 +02:00
parent e07bb73eeb
commit 748b5d3166
53 changed files with 1122 additions and 1360 deletions

View File

@@ -0,0 +1,110 @@
# encoding: UTF-8
class WpTarget < WebSite
module BruteForce
# 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(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
wp_users.each do |wp_user|
queue_count = 0
request_count = 0
password_found = false
File.open(wordlist_path, 'r').each do |password|
# ignore file comments, but will miss passwords if they start with a hash...
next if password[0, 1] == '#'
password.strip!
# keep a count of the amount of requests to be sent
request_count += 1
queue_count += 1
# create local vars for on_complete call back, Issue 51.
login = wp_user.login
password = password
# the request object
request = Browser.instance.forge_request(login_url,
{
method: :post,
body: { log: URI::encode(login), pwd: URI::encode(password) },
cache_ttl: 0
}
)
# tell hydra what to do when the request completes
request.on_complete do |response|
puts "\n Trying Username : #{login} Password : #{password}" if @verbose
if response.body =~ /login_error/i
puts "\nIncorrect login and/or password." if @verbose
elsif response.code == 302
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
elsif response.code == 0
puts red('ERROR:') + ' No response from remote server. WAF/IPS?' if show_progression
# code is a fixnum, needs a string for regex
elsif response.code.to_s =~ /^50/
puts red('ERROR:') + ' Server error, try reducing the number of threads.' if show_progression
else
puts "\n" + red('ERROR:') + " We received an unknown response for #{password}..." if show_progression
# HACK to get the coverage :/ (otherwise some output is present in the rspec)
puts red("Code: #{response.code.to_s}") if @verbose
puts red("Body: #{response.body}") if @verbose
puts if @verbose
end
end
# 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 '#{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.
# hydra.run only returns when it has recieved all of its,
# responses. This means that while we are waiting for @threads,
# responses, we are waiting...
if queue_count >= Browser.instance.max_threads
hydra.run
queue_count = 0
puts "Sent #{Browser.instance.max_threads} requests ..." if @verbose
end
end
# run all of the remaining requests
hydra.run
end
found
end
# Counts the number of lines in the wordlist
# It can take a couple of minutes on large
# wordlists, although bareable.
def self.lines_in_file(file_path)
lines = 0
File.open(file_path, 'r').each { |_| lines += 1 }
lines
end
end
end

View File

@@ -0,0 +1,50 @@
# encoding: UTF-8
class WpTarget < WebSite
module Malwares
# Used as cache :
# nil => malwares not checked,
# [] => no malwares,
# otherwise array of malwares url found
@malwares = nil
def has_malwares?(malwares_file_path = nil)
!malwares(malwares_file_path).empty?
end
# return array of string (url of malwares found)
def malwares(malwares_file_path = nil)
unless @malwares
malwares_found = []
malwares_file = Malwares.malwares_file(malwares_file_path)
index_page_body = Browser.instance.get(@uri.to_s).body
File.open(malwares_file, 'r') do |file|
file.readlines.collect do |url|
chomped_url = url.chomp
if chomped_url.length > 0
malwares_found += index_page_body.scan(Malwares.malware_pattern(chomped_url))
end
end
end
malwares_found.flatten!
malwares_found.uniq!
@malwares = malwares_found
end
@malwares
end
def self.malwares_file(malwares_file_path)
malwares_file_path || DATA_DIR + '/malwares.txt'
end
def self.malware_pattern(url_regex)
# no need to escape regex here, because malware.txt contains regex
%r{<(?:script|iframe).* src=(?:"|')(#{url_regex}[^"']*)(?:"|')[^>]*>}i
end
end
end

View File

@@ -0,0 +1,50 @@
# encoding: UTF-8
class WpTarget < WebSite
module WpConfigBackup
# Checks to see if wp-config.php has a backup
# See http://www.feross.org/cmsploit/
# return an array of backup config files url
def config_backup
found = []
backups = WpConfigBackup.config_backup_files
browser = Browser.instance
hydra = browser.hydra
queue_count = 0
backups.each do |file|
file_url = @uri.merge(URI.escape(file)).to_s
request = browser.forge_request(file_url)
request.on_complete do |response|
if response.body[%r{define}i] and not response.body[%r{<\s?html}i]
found << file_url
end
end
hydra.queue(request)
queue_count += 1
if queue_count == browser.max_threads
hydra.run
queue_count = 0
end
end
hydra.run
found
end
# @return Array
def self.config_backup_files
%w{
wp-config.php~ #wp-config.php# wp-config.php.save wp-config.php.swp wp-config.php.swo wp-config.php_bak
wp-config.bak wp-config.php.bak wp-config.save wp-config.old wp-config.php.old wp-config.php.orig
wp-config.orig wp-config.php.original wp-config.original wp-config.txt
} # thanks to Feross.org for these
end
end
end

View File

@@ -0,0 +1,49 @@
# encoding: UTF-8
class WpTarget < WebSite
module WpCustomDirectories
# @return [ String ] The wp-content directory
def wp_content_dir
unless @wp_content_dir
index_body = Browser.instance.get(@uri.to_s).body
uri_path = @uri.path # Only use the path because domain can be text or an IP
if index_body[/\/wp-content\/(?:themes|plugins)\//i] || default_wp_content_dir_exists?
@wp_content_dir = 'wp-content'
else
domains_excluded = '(?:www\.)?(facebook|twitter)\.com'
@wp_content_dir = index_body[/(?:href|src)\s*=\s*(?:"|').+#{Regexp.escape(uri_path)}((?!#{domains_excluded})[^"']+)\/(?:themes|plugins)\/.*(?:"|')/i, 1]
end
end
@wp_content_dir
end
# @return [ Boolean ]
def default_wp_content_dir_exists?
response = Browser.instance.get(@uri.merge('wp-content').to_s)
hash = Digest::MD5.hexdigest(response.body)
if WpTarget.valid_response_codes.include?(response.code)
return true if hash != error_404_hash and hash != homepage_hash
end
false
end
# @return [ String ] The wp-plugins directory
def wp_plugins_dir
unless @wp_plugins_dir
@wp_plugins_dir = "#{wp_content_dir}/plugins"
end
@wp_plugins_dir
end
# @return [ Boolean ]
def wp_plugins_dir_exists?
Browser.instance.get(@uri.merge(wp_plugins_dir)).code != 404
end
end
end

View File

@@ -0,0 +1,20 @@
# encoding: UTF-8
class WpTarget < WebSite
module WpFullPathDisclosure
# Check for Full Path Disclosure (FPD)
#
# @return [ Boolean ]
def has_full_path_disclosure?
response = Browser.instance.get(full_path_disclosure_url())
response.body[%r{Fatal error}i] ? true : false
end
# @return [ String ]
def full_path_disclosure_url
@uri.merge('wp-includes/rss-functions.php').to_s
end
end
end

View File

@@ -0,0 +1,104 @@
# encoding: UTF-8
class WpTarget < WebSite
module WpLoginProtection
LOGIN_PROTECTION_METHOD_PATTERN = /^has_(.*)_protection\?/i
# Used as cache
@login_protection_plugin = nil
def has_login_protection?
!login_protection_plugin().nil?
end
# Checks if a login protection plugin is enabled
# http://code.google.com/p/wpscan/issues/detail?id=111
# return a WpPlugin object or nil if no one is found
def login_protection_plugin
unless @login_protection_plugin
protected_methods.grep(LOGIN_PROTECTION_METHOD_PATTERN).each do |symbol_to_call|
if send(symbol_to_call)
plugin_name = symbol_to_call[LOGIN_PROTECTION_METHOD_PATTERN, 1].gsub('_', '-')
return @login_protection_plugin = WpPlugin.new(
@uri,
name: plugin_name,
wp_content_dir: wp_content_dir,
wp_plugins_dir: wp_plugins_dir
)
end
end
@login_protection_plugin = nil
end
@login_protection_plugin
end
protected
# Thanks to Alip Aswalid for providing this method.
# http://wordpress.org/extend/plugins/login-lockdown/
def has_login_lockdown_protection?
Browser.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
end
# http://wordpress.org/extend/plugins/better-wp-security/
def has_better_wp_security_protection?
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
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
end
def simple_login_lockdown_url
plugin_url('simple-login-lockdown/')
end
# http://wordpress.org/extend/plugins/login-security-solution/
def has_login_security_solution_protection?
Browser.instance.get(login_security_solution_url()).code != 404
end
def login_security_solution_url
plugin_url('login-security-solution')
end
# http://wordpress.org/extend/plugins/limit-login-attempts/
def has_limit_login_attempts_protection?
Browser.instance.get(limit_login_attempts_url).code != 404
end
def limit_login_attempts_url
plugin_url('limit-login-attempts')
end
# http://wordpress.org/extend/plugins/bluetrait-event-viewer/
def has_bluetrait_event_viewer_protection?
Browser.instance.get(bluetrait_event_viewer_url).code != 404
end
def bluetrait_event_viewer_url
plugin_url('bluetrait-event-viewer')
end
end
end

View File

@@ -0,0 +1,27 @@
# encoding: UTF-8
class WpTarget < WebSite
module WpReadme
# Checks to see if the readme.html file exists
#
# This file comes by default in a wordpress installation,
# and if deleted is reinstated with an upgrade.
#
# @return [ Boolean ]
def has_readme?
response = Browser.instance.get(readme_url())
unless response.code == 404
return response.body =~ %r{wordpress}i ? true : false
end
false
end
# @return [ String ] The readme URL
def readme_url
@uri.merge('readme.html').to_s
end
end
end

View File

@@ -0,0 +1,54 @@
# encoding: UTF-8
class WpTarget < WebSite
module WpRegistrable
# Should check wp-login.php if registration is enabled or not
#
# @return [ Boolean ]
def registration_enabled?
resp = Browser.instance.get(registration_url)
# redirect only on non multi sites
if resp.code == 302 and resp.headers_hash['location'] =~ /wp-login\.php\?registration=disabled/i
enabled = false
# multi site registration form
elsif resp.code == 200 and resp.body =~ /<form id="setupform" method="post" action="[^"]*wp-signup\.php[^"]*">/i
enabled = true
# normal registration form
elsif resp.code == 200 and resp.body =~ /<form name="registerform" id="registerform" action="[^"]*wp-login\.php[^"]*"/i
enabled = true
# registration disabled
else
enabled = false
end
enabled
end
# @return [ String ] The registration URL
def registration_url
multisite? ? @uri.merge('wp-signup.php').to_s : @uri.merge('wp-login.php?action=register').to_s
end
# @return [ Boolean ]
def multisite?
unless @multisite
# when multi site, there is no redirection or a redirect to the site itself
# otherwise redirect to wp-login.php
url = @uri.merge('wp-signup.php')
resp = Browser.instance.get(url)
if resp.code == 302 and resp.headers_hash['location'] =~ /wp-login\.php\?action=register/
@multisite = false
elsif resp.code == 302 and resp.headers_hash['location'] =~ /wp-signup\.php/
@multisite = true
elsif resp.code == 200
@multisite = true
else
@multisite = false
end
end
@multisite
end
end
end