#!/usr/bin/env ruby # encoding: UTF-8 $: << '.' $exit_code = 0 require File.join(__dir__, 'lib', 'wpscan', 'wpscan_helper') def main begin wpscan_options = WpscanOptions.load_from_arguments $log = wpscan_options.log # some sanity checks if $log if $log.empty? $log = DEFAULT_LOG_FILE end # translate to full path if no starting / detected if $log !~ /^#{File::SEPARATOR}/ $log = File.join(ROOT_DIR, $log) end # check if file exists and has a size greater zero if File.exist?($log) && File.size?($log) puts notice("The supplied log file #{$log} already exists. If you continue the new output will be appended.") print '[?] Do you want to continue? [Y]es [N]o, default: [N]' if Readline.readline !~ /^y/i # unset logging so puts will try to log to the file $log = nil puts notice('Scan aborted') exit(1) end end # check if we can write the file begin File.open($log, 'a') rescue SystemCallError => e # unset logging so puts will try to log to the file temp = $log $log = nil puts critical("Error with logfile #{temp}:") puts critical(e) exit(1) end end banner() unless wpscan_options.no_banner # called after $log set unless wpscan_options.has_options? # first parameter only url? if ARGV.length == 1 wpscan_options.url = ARGV[0] else usage() raise('No argument supplied') end end # Define a global variable $COLORSWITCH = wpscan_options.no_color if wpscan_options.help help() usage() exit(0) end if wpscan_options.version puts "Current version: #{WPSCAN_VERSION}" date = last_update puts "Last DB update: #{date.strftime('%Y-%m-%d')}" unless date.nil? exit(0) end # Initialize the browser to allow the db update # to be done over a proxy if set Browser.instance( wpscan_options.to_h.merge(max_threads: wpscan_options.threads) ) # check if db file needs upgrade and we are not running in batch mode # also no need to check if the user supplied the --update switch if update_required? && !wpscan_options.batch && !wpscan_options.update puts notice('It seems like you have not updated the database for some time.') print '[?] Do you want to update now? [Y]es [N]o [A]bort, default: [N]' if (input = Readline.readline) =~ /^y/i wpscan_options.update = true elsif input =~ /^a/i puts 'Scan aborted' exit(1) else if missing_db_file? puts critical('You can not run a scan without any databases. Extract the data.zip file.') exit(1) end end end if wpscan_options.update puts notice('Updating the Database ...') DbUpdater.new(DATA_DIR).update(wpscan_options.verbose) puts notice('Update completed.') # Exit program if only option --update is used exit(0) unless wpscan_options.url end unless wpscan_options.url raise 'The URL is mandatory, please supply it with --url or -u' end wp_target = WpTarget.new(wpscan_options.url, wpscan_options.to_h) if wp_target.wordpress_hosted? raise 'We do not support scanning *.wordpress.com hosted blogs' end if wp_target.ssl_error? raise "The target site returned an SSL/TLS error. You can try again using the --disable-tls-checks option.\nError: #{wp_target.get_root_path_return_code}\nSee here for a detailed explanation of the error: http://www.rubydoc.info/github/typhoeus/ethon/Ethon/Easy:return_code" end # Remote website up? unless wp_target.online? raise "The WordPress URL supplied '#{wp_target.uri}' seems to be down. Maybe the site is blocking wpscan so you can try the --random-agent parameter." end if wpscan_options.proxy proxy_response = Browser.get(wp_target.url) unless WpTarget::valid_response_codes.include?(proxy_response.code) raise "Proxy Error :\r\nResponse Code: #{proxy_response.code}\r\nResponse Headers: #{proxy_response.headers}" end end # Remote website has a redirection? if (redirection = wp_target.redirection) if redirection =~ /\/wp-admin\/install\.php$/ puts critical('The Website is not fully configured and currently in install mode. Call it to create a new admin user.') else if wpscan_options.follow_redirection puts "Following redirection #{redirection}" else puts notice("The remote host tried to redirect to: #{redirection}") print '[?] Do you want follow the redirection ? [Y]es [N]o [A]bort, default: [N]' end if wpscan_options.follow_redirection || !wpscan_options.batch if wpscan_options.follow_redirection || (input = Readline.readline) =~ /^y/i wpscan_options.url = redirection wp_target = WpTarget.new(redirection, wpscan_options.to_h) else if input =~ /^a/i puts 'Scan aborted' exit(1) end end end end end if wp_target.has_basic_auth? && wpscan_options.basic_auth.nil? raise 'Basic authentication is required, please provide it with --basic-auth ' end # test for valid credentials unless wpscan_options.basic_auth.nil? res = Browser.get_and_follow_location(wp_target.url) raise 'Invalid credentials supplied' if res && res.code == 401 end # Remote website is wordpress? unless wpscan_options.force unless wp_target.wordpress? raise 'The remote website is up, but does not seem to be running WordPress.' end end unless wp_target.wp_content_dir raise 'The wp_content_dir has not been found, please supply it with --wp-content-dir' end unless wp_target.wp_plugins_dir_exists? puts "The plugins directory '#{wp_target.wp_plugins_dir}' does not exist." puts 'You can specify one per command line option (don\'t forget to include the wp-content directory if needed)' puts '[?] Continue? [Y]es [N]o, default: [N]' if wpscan_options.batch || Readline.readline !~ /^y/i exit(1) end end # Output runtime data start_time = Time.now start_memory = get_memory_usage unless windows? puts info("URL: #{wp_target.url}") puts info("Started: #{start_time.asctime}") puts if wp_target.has_robots? puts info("robots.txt available under: '#{wp_target.robots_url}'") wp_target.parse_robots_txt.each do |dir| puts info("Interesting entry from robots.txt: #{dir}") end end if wp_target.has_full_path_disclosure? puts warning("Full Path Disclosure (FPD) in '#{wp_target.full_path_disclosure_url}': #{wp_target.full_path_disclosure_data}") end if wp_target.has_debug_log? puts critical("Debug log file found: #{wp_target.debug_log_url}") end wp_target.config_backup.each do |file_url| puts critical("A wp-config.php backup file has been found in: '#{file_url}'") end if wp_target.search_replace_db_2_exists? puts critical("searchreplacedb2.php has been found in: '#{wp_target.search_replace_db_2_url}'") end if wp_target.emergency_exists? puts critical("emergency.php has been found in: '#{wp_target.emergency_url}'") end wp_target.interesting_headers.each do |header| output = info('Interesting header: ') if header[1].class == Array header[1].each do |value| puts output + "#{header[0]}: #{value}" end else puts output + "#{header[0]}: #{header[1]}" end end if wp_target.multisite? puts info('This site seems to be a multisite (http://codex.wordpress.org/Glossary#Multisite)') end if wp_target.has_must_use_plugins? puts info("This site has 'Must Use Plugins' (http://codex.wordpress.org/Must_Use_Plugins)") end if wp_target.registration_enabled? puts warning("Registration is enabled: #{wp_target.registration_url}") end if wp_target.has_xml_rpc? puts info("XML-RPC Interface available under: #{wp_target.xml_rpc_url}") end if wp_target.upload_directory_listing_enabled? puts warning("Upload directory has directory listing enabled: #{wp_target.upload_dir_url}") end if wp_target.include_directory_listing_enabled? puts warning("Includes directory has directory listing enabled: #{wp_target.includes_dir_url}") end enum_options = { show_progression: true, exclude_content: wpscan_options.exclude_content_based } wp_version = wp_target.version(WP_VERSIONS_FILE) if wp_target.has_readme? && VersionCompare::lesser_or_equal?(wp_version.identifier, '4.8') puts warning("The WordPress '#{wp_target.readme_url}' file exists exposing a version number") end if wp_version wp_version.output(wpscan_options.verbose) else puts puts notice('WordPress version can not be detected') end if wp_theme = wp_target.theme puts # Theme version is handled in #to_s puts info("WordPress theme in use: #{wp_theme}") wp_theme.output(wpscan_options.verbose) # Check for parent Themes parent_theme_count = 0 while wp_theme.is_child_theme? && parent_theme_count <= wp_theme.parent_theme_limit parent_theme_count += 1 parent = wp_theme.get_parent_theme puts puts info("Detected parent theme: #{parent}") parent.output(wpscan_options.verbose) wp_theme = parent end end if wpscan_options.enumerate_plugins == nil and wpscan_options.enumerate_only_vulnerable_plugins == nil puts puts info('Enumerating plugins from passive detection ...') wp_plugins = WpPlugins.passive_detection(wp_target) if !wp_plugins.empty? if wp_plugins.size == 1 puts " | #{wp_plugins.size} plugin found:" else puts " | #{wp_plugins.size} plugins found:" end wp_plugins.output(wpscan_options.verbose) else puts info('No plugins found') end end # Enumerate the installed plugins if wpscan_options.enumerate_plugins or wpscan_options.enumerate_only_vulnerable_plugins or wpscan_options.enumerate_all_plugins puts if wpscan_options.enumerate_only_vulnerable_plugins puts info('Enumerating installed plugins (only ones with known vulnerabilities) ...') plugin_enumeration_type = :vulnerable end if wpscan_options.enumerate_plugins puts info('Enumerating installed plugins (only ones marked as popular) ...') plugin_enumeration_type = :popular end if wpscan_options.enumerate_all_plugins puts info('Enumerating all plugins (may take a while and use a lot of system resources) ...') plugin_enumeration_type = :all end puts wp_plugins = WpPlugins.aggressive_detection(wp_target, enum_options.merge( file: PLUGINS_FILE, type: plugin_enumeration_type ) ) puts if !wp_plugins.empty? puts info("We found #{wp_plugins.size} plugins:") wp_plugins.output(wpscan_options.verbose) else puts info('No plugins found') end end # Enumerate installed themes if wpscan_options.enumerate_themes or wpscan_options.enumerate_only_vulnerable_themes or wpscan_options.enumerate_all_themes puts if wpscan_options.enumerate_only_vulnerable_themes puts info('Enumerating installed themes (only ones with known vulnerabilities) ...') theme_enumeration_type = :vulnerable end if wpscan_options.enumerate_themes puts info('Enumerating installed themes (only ones marked as popular) ...') theme_enumeration_type = :popular end if wpscan_options.enumerate_all_themes puts info('Enumerating all themes (may take a while and use a lot of system resources) ...') theme_enumeration_type = :all end puts wp_themes = WpThemes.aggressive_detection(wp_target, enum_options.merge( file: THEMES_FILE, type: theme_enumeration_type ) ) puts if !wp_themes.empty? puts info("We found #{wp_themes.size} themes:") wp_themes.output(wpscan_options.verbose) else puts info('No themes found') end end if wpscan_options.enumerate_timthumbs puts puts info('Enumerating timthumb files ...') puts wp_timthumbs = WpTimthumbs.aggressive_detection(wp_target, enum_options.merge( file: DATA_DIR + '/timthumbs.txt', theme_name: wp_theme ? wp_theme.name : nil ) ) puts if !wp_timthumbs.empty? puts info("We found #{wp_timthumbs.size} timthumb file/s:") wp_timthumbs.output(wpscan_options.verbose) else puts info('No timthumb files found') end end # If we haven't been supplied a username/usernames list, enumerate them... if !wpscan_options.username && !wpscan_options.usernames && wpscan_options.wordlist || wpscan_options.enumerate_usernames puts puts info('Enumerating usernames ...') if wp_target.has_plugin?('stop-user-enumeration') puts warning("Stop User Enumeration plugin detected, results might be empty. However a bypass exists for v1.2.8 and below, see stop_user_enumeration_bypass.rb in #{__dir__}") end wp_users = WpUsers.aggressive_detection(wp_target, enum_options.merge( range: wpscan_options.enumerate_usernames_range, show_progression: false ) ) if wp_users.empty? puts info('We did not enumerate any usernames') if wpscan_options.wordlist puts 'Try supplying your own username with the --username option' puts exit(1) end else puts info("Identified the following #{wp_users.size} user/s:") wp_users.output(margin_left: ' ' * 4) if wp_users[0].login == "admin" puts warning("Default first WordPress username 'admin' is still used") end end else wp_users = WpUsers.new if wpscan_options.usernames File.open(wpscan_options.usernames).each do |username| wp_users << WpUser.new(wp_target.uri, login: username.chomp) end else wp_users << WpUser.new(wp_target.uri, login: wpscan_options.username) end end # Start the brute forcer bruteforce = true if wpscan_options.wordlist if wp_target.has_login_protection? protection_plugin = wp_target.login_protection_plugin() puts puts warning("The plugin #{protection_plugin.name} has been detected. It might record the IP and timestamp of every failed login and/or prevent brute forcing altogether. Not a good idea for brute forcing!") puts '[?] Do you want to start the brute force anyway ? [Y]es [N]o, default: [N]' bruteforce = false if wpscan_options.batch || Readline.readline !~ /^y/i end if bruteforce puts info('Starting the password brute forcer') begin wp_users.brute_force( wpscan_options.wordlist, show_progression: true, verbose: wpscan_options.verbose ) ensure puts wp_users.output(show_password: true, margin_left: ' ' * 2) end else puts critical('Brute forcing aborted') end end stop_time = Time.now elapsed = stop_time - start_time used_memory = get_memory_usage - start_memory unless windows? puts puts info("Finished: #{stop_time.asctime}") puts info("Requests Done: #{@total_requests_done}") puts info("Memory used: #{used_memory.bytes_to_human}") unless windows? puts info("Elapsed time: #{Time.at(elapsed).utc.strftime('%H:%M:%S')}") # do nothing on interrupt rescue Interrupt exit(1) # Error on Updating rescue ChecksumError => e puts critical(e.message) if e.file puts critical("Current Version: #{WPSCAN_VERSION}") puts critical('Downloaded File Content:') puts e.file[0..500] # print first 500 chars puts '.........' puts e.file[-500..-1] || e.file # print last 500 chars or the whole file if it's < 500 puts end puts critical('Some hints to help you with this issue:') puts critical('-) Try updating again using --verbose') puts critical('-) If you see SSL/TLS related error messages you have to fix your local TLS setup') puts critical('-) Windows is still not supported') exit(1) rescue => e puts puts critical(e.message) if wpscan_options && wpscan_options.verbose puts critical('Trace:') puts critical(e.backtrace.join("\n")) end exit(1) ensure # Ensure a clean abort of Hydra # See https://github.com/wpscanteam/wpscan/issues/461#issuecomment-42735615 Browser.instance.hydra.abort Browser.instance.hydra.run end end main() exit($exit_code)