WpTarget modules reworked
This commit is contained in:
110
lib/wpscan/wp_target/brute_force.rb
Normal file
110
lib/wpscan/wp_target/brute_force.rb
Normal 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
|
||||
50
lib/wpscan/wp_target/malwares.rb
Normal file
50
lib/wpscan/wp_target/malwares.rb
Normal 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
|
||||
50
lib/wpscan/wp_target/wp_config_backup.rb
Normal file
50
lib/wpscan/wp_target/wp_config_backup.rb
Normal 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
|
||||
49
lib/wpscan/wp_target/wp_custom_directories.rb
Normal file
49
lib/wpscan/wp_target/wp_custom_directories.rb
Normal 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
|
||||
20
lib/wpscan/wp_target/wp_full_path_disclosure.rb
Normal file
20
lib/wpscan/wp_target/wp_full_path_disclosure.rb
Normal 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
|
||||
104
lib/wpscan/wp_target/wp_login_protection.rb
Normal file
104
lib/wpscan/wp_target/wp_login_protection.rb
Normal 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
|
||||
27
lib/wpscan/wp_target/wp_readme.rb
Normal file
27
lib/wpscan/wp_target/wp_readme.rb
Normal 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
|
||||
54
lib/wpscan/wp_target/wp_registrable.rb
Normal file
54
lib/wpscan/wp_target/wp_registrable.rb
Normal 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
|
||||
Reference in New Issue
Block a user