Class: WPScan::Controller::Core
- Defined in:
- app/controllers/core.rb,
app/controllers/core/cli_options.rb
Overview
CLI Options for the Core Controller
Constant Summary
Constants included from OptParseValidator
Instance Method Summary collapse
- #after_scan ⇒ Object
- #base_cli_options ⇒ Object
- #before_scan ⇒ Object
-
#check_target_availability ⇒ Void
Checks that the target is accessible, raises related errors otherwise.
-
#check_wordpress_state ⇒ Object
Raises errors if the target is hosted on wordpress.com or is not running WordPress.
- #cli_browser_cache_options ⇒ Array<OptParseValidator::OptBase>
- #cli_browser_cookies_options ⇒ Array<OptParseValidator::OptBase>
- #cli_browser_headers_options ⇒ Array<OptParseValidator::OptBase>
- #cli_browser_options ⇒ Array<OptParseValidator::OptBase>
- #cli_browser_proxy_options ⇒ Array<OptParseValidator::OptBase>
- #cli_options ⇒ Array<OptParseValidator::Opt>
-
#handle_follow_redirect(effective_url, effective_uri) ⇒ Object
Handles –follow-redirect option.
-
#handle_redirection(res) ⇒ Object
Checks for redirects; an out-of-scope redirect raises Error::HTTPRedirect.
-
#handle_saml_authentication(effective_uri) ⇒ Void
Drives an interactive SAML login via a headless browser, injects the resulting session cookies into the shared Browser, and clears the target’s cached homepage so the rest of the scan runs against the authenticated session.
-
#handle_scheme_redirect(effective_url, effective_uri) ⇒ Object
Handles scheme-only redirects (http => https or vice versa).
-
#load_server_module ⇒ Symbol
Loads the related server module into the target and includes it on WpItem (needed to check if directory listing is enabled etc.).
- #local_db ⇒ DB::Updater
- #maybe_output_banner_help_and_version ⇒ Object
- #mixed_cli_options ⇒ Object
- #run ⇒ Object
-
#saml_request?(effective_uri, homepage_res = nil) ⇒ Boolean
Checks whether the response or its redirect chain contains a SAMLRequest, indicating that the target requires SAML authentication.
- #setup_cache ⇒ Object
- #update_db ⇒ Object
- #update_db_required? ⇒ Boolean
-
#updating_db? ⇒ Boolean
Whether the DB update is currently in progress.
Methods inherited from Base
#==, #datastore, #formatter, #option_parser, option_parser=, #output, #render, reset, #target, #tmp_directory, #user_interaction?
Instance Method Details
#after_scan ⇒ Object
266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 |
# File 'app/controllers/core.rb', line 266 def after_scan @stop_time = Time.now @elapsed = @stop_time - @start_time @used_memory = GetProcessMem.new.bytes - @start_memory warnings = WPScan. output('finished', cached_requests: WPScan.cached_requests, requests_done: WPScan.total_requests, data_sent: WPScan.total_data_sent, data_received: WPScan.total_data_received, response_status_codes: WPScan.format_status_codes(WPScan.top_status_codes), response_status_codes_warning: warnings.any?, response_status_codes_warnings: warnings) end |
#base_cli_options ⇒ Object
7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
# File 'app/controllers/core/cli_options.rb', line 7 def formats = WPScan::Formatter.availables [ OptURL.new(['-u', '--url URL', 'The URL to scan'], required_unless: %i[help hh version], default_protocol: 'http'), OptBoolean.new(['--force', 'Do not check if target returns a 403']) ] + + [ OptFilePath.new(['-o', '--output FILE', 'Output to FILE'], writable: true, exists: false), OptChoice.new(['-f', '--format FORMAT', 'Output results in the format supplied'], choices: formats), OptBoolean.new(['--[no-]stream', 'Emit enumeration findings (plugins/themes/users) as they are discovered, ' \ 'instead of waiting until each enumeration step completes. ' \ 'Has no effect on the json or sarif output formats, which always batch.'], default: true), OptChoice.new(['--detection-mode MODE'], choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed), OptArray.new(['--scope DOMAINS', 'Comma separated (sub-)domains to consider in scope. ', 'Wildcard(s) allowed in the trd of valid domains, e.g: *.target.tld'], advanced: true), OptArray.new(['--exclude-vulns UUIDs', 'Comma separated list of vulnerability UUIDs to exclude. ', 'Format: UUID (e.g: c099c1da-3750-4e63-8af9-929e773bbe57)'], advanced: true) ] + end |
#before_scan ⇒ Object
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'app/controllers/core.rb', line 34 def before_scan @last_update = local_db.last_update @saml_authenticated = false update_db if update_db_required? setup_cache check_target_availability load_server_module check_wordpress_state rescue Error::NotWordPress => e target. raise e unless target.wordpress?(ParsedCli.detection_mode) end |
#check_target_availability ⇒ Void
Checks that the target is accessible, raises related errors otherwise.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'app/controllers/core.rb', line 62 def check_target_availability res = WPScan::Browser.get(target.url) case res.code when 0 raise Error::TargetDown, res when 401 raise Error::HTTPAuthRequired when 403 raise Error::AccessForbidden, WPScan::ParsedCli.random_user_agent unless WPScan::ParsedCli.force when 407 raise Error::ProxyAuthRequired end handle_redirection(res) end |
#check_wordpress_state ⇒ Object
Raises errors if the target is hosted on wordpress.com or is not running WordPress. Also checks if the homepage_url is still the install URL.
217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'app/controllers/core.rb', line 217 def check_wordpress_state raise Error::WordPressHosted if target.wordpress_hosted? if %r{/wp-admin/install.php$}i.match?(Addressable::URI.parse(target.homepage_url).path) output('not_fully_configured', url: target.homepage_url) exit(WPScan::ExitCode::VULNERABLE) end raise Error::NotWordPress unless target.wordpress?(ParsedCli.detection_mode) || ParsedCli.force end |
#cli_browser_cache_options ⇒ Array<OptParseValidator::OptBase>
136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'app/controllers/core/cli_options.rb', line 136 def [ OptInteger.new(['--cache-ttl TIME_TO_LIVE', 'The cache time to live in seconds'], default: 600, advanced: true), OptBoolean.new(['--clear-cache', 'Clear the cache before the scan'], advanced: true), OptDirectoryPath.new(['--cache-dir PATH'], readable: true, writable: true, create: true, default: tmp_directory.join('cache'), advanced: true) ] end |
#cli_browser_cookies_options ⇒ Array<OptParseValidator::OptBase>
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'app/controllers/core/cli_options.rb', line 116 def [ OptString.new(['--cookie-string COOKIE', 'Cookie string to use in requests, ' \ 'format: cookie1=value1[; cookie2=value2]']), OptFilePath.new(['--cookie-jar FILE-PATH', 'File to read and write cookies'], writable: true, readable: true, create: true, default: tmp_directory.join('cookie_jar.txt')), OptBoolean.new(['--expect-saml', 'Expect SAML authentication to be required. ' \ 'When the target redirects to a SAML IdP, an interactive browser ' \ 'is launched for login and the resulting session cookies are reused ' \ 'for the rest of the scan.'], advanced: true) ] end |
#cli_browser_headers_options ⇒ Array<OptParseValidator::OptBase>
95 96 97 98 99 100 101 |
# File 'app/controllers/core/cli_options.rb', line 95 def [ OptString.new(['--user-agent VALUE', '--ua']), OptHeaders.new(['--headers HEADERS', 'Additional headers to append in requests'], advanced: true), OptString.new(['--vhost VALUE', 'The virtual host (Host header) to use in requests'], advanced: true) ] end |
#cli_browser_options ⇒ Array<OptParseValidator::OptBase>
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
# File 'app/controllers/core/cli_options.rb', line 62 def + [ OptBoolean.new(['--random-user-agent', '--rua', 'Use a random user-agent for each scan']), OptFilePath.new(['--user-agents-list FILE-PATH', 'List of agents to use with --random-user-agent'], exists: true, advanced: true, default: APP_DIR.join('user_agents.txt')), OptCredentials.new(['--http-auth login:password', 'Basic HTTP authentication, beware that the $ character must be properly escaped.']), OptCredentials.new(['--wp-auth login:password', 'WordPress admin credentials used to query the REST API for an authoritative ' \ 'inventory of installed plugins and themes (/wp-json/wp/v2/plugins and /themes). ' \ 'When provided, plugin/theme enumeration via -e is bypassed. ' \ 'The password MUST be a WordPress Application Password (WP >= 5.6, ' \ 'created at Users -> Profile -> Application Passwords). ' \ 'Real account passwords are rejected by WordPress core over Basic Auth.']), OptPositiveInteger.new(['-t', '--max-threads VALUE', 'The max threads to use'], default: 5), OptPositiveInteger.new(['--throttle MilliSeconds', 'Milliseconds to wait before doing another web request. ' \ 'If used, the max threads will be set to 1.']), OptPositiveInteger.new(['--request-timeout SECONDS', 'The request timeout in seconds'], default: 60), OptPositiveInteger.new(['--connect-timeout SECONDS', 'The connection timeout in seconds'], default: 30), OptBoolean.new(['--disable-tls-checks', 'Disables SSL/TLS certificate verification, and downgrade to TLS1.0+ ' \ '(requires cURL 7.66 for the latter)']) ] + + + end |
#cli_browser_proxy_options ⇒ Array<OptParseValidator::OptBase>
104 105 106 107 108 109 110 111 112 113 |
# File 'app/controllers/core/cli_options.rb', line 104 def [ OptProxy.new(['--proxy protocol://IP:port', 'Supported protocols depend on the cURL installed. ' \ 'Note: with socks5://, hostnames are resolved locally before being ' \ 'sent to the proxy; use socks5h:// to have the proxy resolve them ' \ '(required for .onion addresses when proxying through Tor).']), OptCredentials.new(['--proxy-auth login:password']) ] end |
#cli_options ⇒ Array<OptParseValidator::Opt>
11 12 13 14 15 16 17 18 19 20 21 22 23 |
# File 'app/controllers/core.rb', line 11 def [OptURL.new(['--url URL', 'The URL of the blog to scan'], required_unless: %i[update help hh version], default_protocol: 'http')] + .drop(2) + # drop the base --url and --force [ OptChoice.new(['--server SERVER', 'Force the supplied server module to be loaded'], choices: %w[apache iis nginx], normalize: %i[downcase to_sym], advanced: true), OptBoolean.new(['--force', 'Do not check if the target is running WordPress or returns a 403']), OptBoolean.new(['--[no-]update', 'Whether or not to update the Database']) ] end |
#handle_follow_redirect(effective_url, effective_uri) ⇒ Object
Handles –follow-redirect option
168 169 170 171 172 173 |
# File 'app/controllers/core.rb', line 168 def handle_follow_redirect(effective_url, effective_uri) return unless WPScan::ParsedCli.follow_redirect && target.url != effective_url target.url = effective_url target.scope << effective_uri.host end |
#handle_redirection(res) ⇒ Object
Checks for redirects; an out-of-scope redirect raises Error::HTTPRedirect.
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'app/controllers/core.rb', line 123 def handle_redirection(res) effective_url = target.homepage_res.effective_url # get and follow location of target.url effective_uri = Addressable::URI.parse(effective_url) is_saml = saml_request?(effective_uri, target.homepage_res) if WPScan::ParsedCli.expect_saml && !is_saml && !@saml_authenticated puts 'SAML authentication was expected but not required.' puts # New line to serve as buffer before the scan results start end if is_saml raise Error::SAMLAuthenticationFailed if @saml_authenticated handle_saml_authentication(effective_uri) return check_target_availability end handle_scheme_redirect(effective_url, effective_uri) handle_follow_redirect(effective_url, effective_uri) return if target.in_scope?(effective_url) raise Error::HTTPRedirect, effective_url unless WPScan::ParsedCli.ignore_main_redirect # Sets homepage_res back to unfollowed response when ignore_main_redirect is used target.homepage_res = res end |
#handle_saml_authentication(effective_uri) ⇒ Void
Drives an interactive SAML login via a headless browser, injects the resulting session cookies into the shared Browser, and clears the target’s cached homepage so the rest of the scan runs against the authenticated session.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'app/controllers/core.rb', line 105 def handle_saml_authentication(effective_uri) raise Error::SAMLAuthenticationFailed if WPScan::ParsedCli. && !WPScan::ParsedCli.expect_saml raise Error::SAMLAuthenticationRequired unless WPScan::ParsedCli.expect_saml = BrowserAuthenticator.authenticate(effective_uri.to_s) browser = WPScan::Browser.instance browser. = [browser., ].compact.reject(&:empty?).join('; ') # Discard the pre-auth homepage so subsequent finders refetch with the new cookies. target.reset_homepage_cache! @saml_authenticated = true end |
#handle_scheme_redirect(effective_url, effective_uri) ⇒ Object
Handles scheme-only redirects (http => https or vice versa)
155 156 157 158 159 160 161 162 |
# File 'app/controllers/core.rb', line 155 def handle_scheme_redirect(effective_url, effective_uri) # http://a.com => https://a.com (or the opposite) if !WPScan::ParsedCli.ignore_main_redirect && target.uri.domain == effective_uri.domain && target.uri.path == effective_uri.path && target.uri.scheme != effective_uri.scheme target.url = effective_url end end |
#load_server_module ⇒ Symbol
Loads the related server module into the target and includes it on WpItem (needed to check if directory listing is enabled etc.).
234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'app/controllers/core.rb', line 234 def load_server_module server = target.server || :Apache # auto-detect case ParsedCli.server when :apache server = :Apache when :iis server = :IIS when :nginx server = :Nginx end mod = WPScan::Target::Server.const_get(server) target.extend mod Model::WpItem.include mod server end |
#local_db ⇒ DB::Updater
176 177 178 |
# File 'app/controllers/core.rb', line 176 def local_db @local_db ||= DB::Updater.new(DB_DIR) end |
#maybe_output_banner_help_and_version ⇒ Object
50 51 52 53 54 55 56 57 |
# File 'app/controllers/core.rb', line 50 def output('banner') if WPScan::ParsedCli. output('help', help: option_parser.simple_help, simple: true) if WPScan::ParsedCli.help output('help', help: option_parser.full_help, simple: false) if WPScan::ParsedCli.hh output('version') if WPScan::ParsedCli.version exit(WPScan::ExitCode::OK) if WPScan::ParsedCli.help || WPScan::ParsedCli.hh || WPScan::ParsedCli.version end |
#mixed_cli_options ⇒ Object
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'app/controllers/core/cli_options.rb', line 37 def [ OptBoolean.new(['-h', '--help', 'Display the simple help and exit']), OptBoolean.new(['--hh', 'Display the full help and exit']), OptBoolean.new(['--version', 'Display the version and exit']), OptBoolean.new(['--ignore-main-redirect', 'Ignore the main redirect (if any) and scan the target url. ' \ 'Has no effect if --follow-redirect is set.'], advanced: true), OptBoolean.new(['--follow-redirect', 'Automatically update the URL to the destination of the redirect'], advanced: true), OptBoolean.new(['-v', '--verbose', 'Verbose mode']), OptBoolean.new(['--[no-]banner', 'Whether or not to display the banner'], default: true), OptPositiveInteger.new(['--max-scan-duration SECONDS', 'Abort the scan if it exceeds the time provided in seconds'], advanced: true), OptPositiveInteger.new(['--max-log-file-size MiB', 'Skip inspection of PHP log files (debug.log, error_log, ...) ' \ 'when their advertised or transferred size exceeds this value, in MiB. ' \ 'Guards against memory exhaustion on sites serving huge log files.'], default: 20, advanced: true) ] end |
#run ⇒ Object
254 255 256 257 258 259 260 261 262 263 264 |
# File 'app/controllers/core.rb', line 254 def run @start_time = Time.now @start_memory = WPScan.start_memory output('started', url: target.url, ip: target.ip, effective_url: target.homepage_url, command_line: WPScan.command_line, hostname: Socket.gethostname) end |
#saml_request?(effective_uri, homepage_res = nil) ⇒ Boolean
Checks whether the response or its redirect chain contains a SAMLRequest, indicating that the target requires SAML authentication.
86 87 88 89 90 91 92 93 94 95 96 |
# File 'app/controllers/core.rb', line 86 def saml_request?(effective_uri, homepage_res = nil) return false unless effective_uri return true if effective_uri.to_s.match?(/[?&]SAMLRequest/i) # SAML flows often bounce through intermediate pages before the IdP; # walk the redirect chain to catch a SAMLRequest in any Location header. !!homepage_res&.redirections&.any? do |redirect_response| redirect_response.headers['Location']&.match?(/SAMLRequest/i) end end |
#setup_cache ⇒ Object
25 26 27 28 29 30 31 32 |
# File 'app/controllers/core.rb', line 25 def setup_cache return unless WPScan::ParsedCli.cache_dir storage_path = File.join(WPScan::ParsedCli.cache_dir, Digest::MD5.hexdigest(target.url)) Typhoeus::Config.cache = Cache::Typhoeus.new(storage_path) Typhoeus::Config.cache.clean if WPScan::ParsedCli.clear_cache end |
#update_db ⇒ Object
201 202 203 204 205 206 207 208 |
# File 'app/controllers/core.rb', line 201 def update_db @updating_db = true output('db_update_started') output('db_update_finished', updated: local_db.update, verbose: ParsedCli.verbose) @updating_db = false exit(0) unless ParsedCli.url end |
#update_db_required? ⇒ Boolean
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'app/controllers/core.rb', line 181 def update_db_required? if local_db.missing_files? raise Error::MissingDatabaseFile if ParsedCli.update == false return true end return ParsedCli.update unless ParsedCli.update.nil? return false unless user_interaction? && local_db.outdated? output('@notice', msg: 'It seems like you have not updated the database for some time.') print '[?] Do you want to update now? [Y]es [N]o, default: [N]' $stdout.flush response = $stdin.gets.to_s.strip !!/^y/i.match?(response) end |
#updating_db? ⇒ Boolean
Returns Whether the DB update is currently in progress.
211 212 213 |
# File 'app/controllers/core.rb', line 211 def updating_db? @updating_db end |