diff --git a/lib/wpscan/modules/brute_force.rb b/lib/wpscan/modules/brute_force.rb deleted file mode 100644 index 54351c0f..00000000 --- a/lib/wpscan/modules/brute_force.rb +++ /dev/null @@ -1,107 +0,0 @@ -# encoding: UTF-8 - -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 diff --git a/lib/wpscan/modules/malwares.rb b/lib/wpscan/modules/malwares.rb deleted file mode 100644 index e40a92c9..00000000 --- a/lib/wpscan/modules/malwares.rb +++ /dev/null @@ -1,47 +0,0 @@ -# encoding: UTF-8 - -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 diff --git a/lib/wpscan/modules/wp_config_backup.rb b/lib/wpscan/modules/wp_config_backup.rb deleted file mode 100644 index c009f157..00000000 --- a/lib/wpscan/modules/wp_config_backup.rb +++ /dev/null @@ -1,48 +0,0 @@ -# encoding: UTF-8 - -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 diff --git a/lib/wpscan/modules/wp_full_path_disclosure.rb b/lib/wpscan/modules/wp_full_path_disclosure.rb deleted file mode 100644 index 75731455..00000000 --- a/lib/wpscan/modules/wp_full_path_disclosure.rb +++ /dev/null @@ -1,14 +0,0 @@ -# encoding: UTF-8 - -module WpFullPathDisclosure - - # Check for Full Path Disclosure (FPD) - def has_full_path_disclosure? - response = Browser.instance.get(full_path_disclosure_url()) - response.body[%r{Fatal error}i] - end - - def full_path_disclosure_url - @uri.merge('wp-includes/rss-functions.php').to_s - end -end diff --git a/lib/wpscan/modules/wp_login_protection.rb b/lib/wpscan/modules/wp_login_protection.rb deleted file mode 100644 index da09958f..00000000 --- a/lib/wpscan/modules/wp_login_protection.rb +++ /dev/null @@ -1,101 +0,0 @@ -# encoding: UTF-8 - -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 diff --git a/lib/wpscan/modules/wp_readme.rb b/lib/wpscan/modules/wp_readme.rb deleted file mode 100644 index 85a59d97..00000000 --- a/lib/wpscan/modules/wp_readme.rb +++ /dev/null @@ -1,20 +0,0 @@ -# encoding: UTF-8 - -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. - def has_readme? - response = Browser.instance.get(readme_url()) - - unless response.code == 404 - response.body =~ %r{wordpress}i - end - end - - def readme_url - @uri.merge('readme.html').to_s - end -end diff --git a/lib/wpscan/wp_target.rb b/lib/wpscan/wp_target.rb index 31dda6d4..4dd7a33e 100644 --- a/lib/wpscan/wp_target.rb +++ b/lib/wpscan/wp_target.rb @@ -1,20 +1,24 @@ # encoding: UTF-8 require 'web_site' -require 'modules/wp_readme' -require 'modules/wp_full_path_disclosure' -require 'modules/wp_config_backup' -require 'modules/wp_login_protection' -require 'modules/malwares' -require 'modules/brute_force' +require 'wp_target/malwares' +require 'wp_target/wp_readme' +require 'wp_target/brute_force' +require 'wp_target/wp_registrable' +require 'wp_target/wp_config_backup' +require 'wp_target/wp_login_protection' +require 'wp_target/wp_custom_directories' +require 'wp_target/wp_full_path_disclosure' class WpTarget < WebSite + include Malwares include WpReadme - include WpFullPathDisclosure + include BruteForce + include WpRegistrable include WpConfigBackup include WpLoginProtection - include Malwares - include BruteForce + include WpCustomDirectories + include WpFullPathDisclosure attr_reader :verbose @@ -72,17 +76,21 @@ class WpTarget < WebSite [200, 301, 302, 401, 403, 500, 400] end - # return WpTheme + # @return [ WpTheme ] + # :nocov: def theme WpTheme.find(@uri) end + # :nocov: # @param [ String ] versions_xml # # @return [ WpVersion ] + # :nocov: def version(versions_xml) WpVersion.find(@uri, wp_content_dir, wp_plugins_dir, versions_xml) end + # :nocov: def has_plugin?(name, version = nil) WpPlugin.new( @@ -94,44 +102,6 @@ class WpTarget < WebSite ).exists? end - 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 - - 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 - - def wp_plugins_dir - unless @wp_plugins_dir - @wp_plugins_dir = "#{wp_content_dir}/plugins" - end - @wp_plugins_dir - end - - def wp_plugins_dir_exists? - Browser.instance.get(@uri.merge(wp_plugins_dir)).code != 404 - end - def has_debug_log? # We only get the first 700 bytes of the file to avoid loading huge file (like 2Go) response_body = Browser.instance.get(debug_log_url(), headers: {'range' => 'bytes=0-700'}).body @@ -153,46 +123,4 @@ class WpTarget < WebSite resp = Browser.instance.get(search_replace_db_2_url) resp.code == 200 && resp.body[%r{by interconnect}i] end - - # Should check wp-login.php if registration is enabled or not - 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 =~ /