wpscan 3.7.10 → 3.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -16
  3. data/app/controllers/enumeration/cli_options.rb +2 -3
  4. data/app/controllers/password_attack.rb +21 -21
  5. data/app/finders/db_exports/known_locations.rb +1 -1
  6. data/app/finders/interesting_findings/mu_plugins.rb +1 -1
  7. data/app/finders/interesting_findings/multisite.rb +2 -2
  8. data/app/finders/main_theme/css_style_in_homepage.rb +2 -2
  9. data/app/finders/passwords/wp_login.rb +1 -1
  10. data/app/finders/passwords/xml_rpc.rb +2 -2
  11. data/app/finders/passwords/xml_rpc_multicall.rb +45 -12
  12. data/app/finders/plugin_version/readme.rb +5 -7
  13. data/app/finders/theme_version/style.rb +1 -1
  14. data/app/finders/users.rb +1 -1
  15. data/app/finders/users/login_error_messages.rb +1 -1
  16. data/app/finders/users/rss_generator.rb +1 -1
  17. data/app/finders/users/wp_json_api.rb +1 -1
  18. data/app/finders/users/yoast_seo_author_sitemap.rb +1 -1
  19. data/app/finders/wp_items/urls_in_page.rb +3 -3
  20. data/app/models/interesting_finding.rb +1 -1
  21. data/app/models/plugin.rb +1 -1
  22. data/app/models/theme.rb +2 -2
  23. data/app/models/timthumb.rb +6 -6
  24. data/app/models/wp_item.rb +1 -1
  25. data/app/models/wp_version.rb +1 -1
  26. data/app/views/cli/password_attack/users.erb +1 -1
  27. data/app/views/cli/vulnerability.erb +3 -0
  28. data/app/views/json/finding.erb +3 -0
  29. data/lib/wpscan/db/updater.rb +12 -14
  30. data/lib/wpscan/errors/wordpress.rb +6 -0
  31. data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
  32. data/lib/wpscan/finders/dynamic_finder/version/config_parser.rb +5 -7
  33. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +1 -1
  34. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  35. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  36. data/lib/wpscan/helper.rb +1 -1
  37. data/lib/wpscan/references.rb +1 -1
  38. data/lib/wpscan/target.rb +4 -4
  39. data/lib/wpscan/target/platform/wordpress.rb +8 -7
  40. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  41. data/lib/wpscan/version.rb +1 -1
  42. data/lib/wpscan/vulnerability.rb +4 -3
  43. metadata +9 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e90db053602c7fcca669e5c0e31d93b6ee50ffd189ad6c60015461c63c0d5f9d
4
- data.tar.gz: 556b07a69059567771ecf05f357396e11f8efa7538dfd1f638e9a2a420f5638e
3
+ metadata.gz: 658d14ca9901acb16c2da34ed977e5ad75296c4c8aba3e66ba897ffb5dd55f86
4
+ data.tar.gz: 788c5a27c5bf10dba5b35e977c3efdfafca97944485cf605f03e34faf65ae68b
5
5
  SHA512:
6
- metadata.gz: c170ba773c4209f5f93551118cccf5f5be744f3a4d1e0746cec11ae959959f2e386b7f1848d96b72b86aece27abf3431760c131736ba1d55c535eeab563ef1ec
7
- data.tar.gz: 1b0b3bebf70efa0ac6111b97032b464a8e3188799d016ac5ebbd9c9cdfcfc4d8803291052b3d883a985aa10a77282187f07957080c2b1121b2eb83d3f2031522
6
+ metadata.gz: be04200a71b1d710d47e0a8be2b914731803a48c94a8da7bfa1ad777de8f1e103fe9ac785589d70ca905c389c369bd4567118bae6d3585f88f7bf12c52c053af
7
+ data.tar.gz: 42f54d70d4aea2433c5b619abb5c516e731f7cfefafe9d4e0d8876956eb8470012e211b826c4790b1c7144ff091431906597b2e2a7c1cd63f25b07ab87013caf
data/README.md CHANGED
@@ -7,10 +7,10 @@
7
7
  <h3 align="center">WPScan</h3>
8
8
 
9
9
  <p align="center">
10
- WordPress Vulnerability Scanner
10
+ WordPress Security Scanner
11
11
  <br>
12
12
  <br>
13
- <a href="https://wpscan.org/" title="homepage" target="_blank">Homepage</a> - <a href="https://wpscan.io/" title="wpscan.io" target="_blank">WPScan.io</a> - <a href="https://wpvulndb.com/" title="vulnerability database" target="_blank">Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress plugin" target="_blank">WordPress Plugin</a>
13
+ <a href="https://wpscan.org/" title="homepage" target="_blank">Homepage</a> - <a href="https://wpscan.io/" title="wpscan.io" target="_blank">WPScan.io</a> - <a href="https://wpvulndb.com/" title="vulnerability database" target="_blank">Vulnerability Database</a> - <a href="https://wordpress.org/plugins/wpscan/" title="wordpress security plugin" target="_blank">WordPress Security Plugin</a>
14
14
  </p>
15
15
 
16
16
  <p align="center">
@@ -31,7 +31,11 @@
31
31
  - RubyGems - Recommended: latest
32
32
  - Nokogiri might require packages to be installed via your package manager depending on your OS, see https://nokogiri.org/tutorials/installing_nokogiri.html
33
33
 
34
- ### From RubyGems (Recommended)
34
+ ### In a Pentesting distribution
35
+
36
+ When using a pentesting distubution (such as Kali Linux), it is recommended to install/update wpscan via the package manager if available.
37
+
38
+ ### From RubyGems
35
39
 
36
40
  ```shell
37
41
  gem install wpscan
@@ -39,18 +43,6 @@ gem install wpscan
39
43
 
40
44
  On MacOSX, if a ```Gem::FilePermissionError``` is raised due to the Apple's System Integrity Protection (SIP), either install RVM and install wpscan again, or run ```sudo gem install -n /usr/local/bin wpscan``` (see [#1286](https://github.com/wpscanteam/wpscan/issues/1286))
41
45
 
42
- ### From sources (NOT Recommended)
43
-
44
- Prerequisites: Git
45
-
46
- ```shell
47
- git clone https://github.com/wpscanteam/wpscan
48
-
49
- cd wpscan/
50
-
51
- bundle install && rake install
52
- ```
53
-
54
46
  # Updating
55
47
 
56
48
  You can update the local database by using ```wpscan --update```
@@ -77,6 +69,8 @@ docker run -it --rm wpscanteam/wpscan --url https://target.tld/ --enumerate u1-1
77
69
 
78
70
  # Usage
79
71
 
72
+ Full user documentation can be found here; https://github.com/wpscanteam/wpscan/wiki/WPScan-User-Documentation
73
+
80
74
  ```wpscan --url blog.tld``` This will scan the blog using default options with a good compromise between speed and accuracy. For example, the plugins will be checked passively but their version with a mixed detection mode (passively + aggressively). Potential config backup files will also be checked, along with other interesting findings.
81
75
 
82
76
  If a more stealthy approach is required, then ```wpscan --stealthy --url blog.tld``` can be used.
@@ -130,7 +124,7 @@ cli_options:
130
124
  api_token: YOUR_API_TOKEN
131
125
  ```
132
126
 
133
- ## Load APi Token From ENV
127
+ ## Load API Token From ENV (since v3.7.10)
134
128
 
135
129
  The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
136
130
 
@@ -51,7 +51,7 @@ module WPScan
51
51
  OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
52
52
  OptChoice.new(
53
53
  ['--plugins-detection MODE',
54
- 'Use the supplied mode to enumerate Plugins, instead of the global (--detection-mode) mode.'],
54
+ 'Use the supplied mode to enumerate Plugins.'],
55
55
  choices: %w[mixed passive aggressive], normalize: :to_sym, default: :passive
56
56
  ),
57
57
  OptBoolean.new(
@@ -62,8 +62,7 @@ module WPScan
62
62
  ),
63
63
  OptChoice.new(
64
64
  ['--plugins-version-detection MODE',
65
- 'Use the supplied mode to check plugins versions instead of the --detection-mode ' \
66
- 'or --plugins-detection modes.'],
65
+ 'Use the supplied mode to check plugins\' versions.'],
67
66
  choices: %w[mixed passive aggressive], normalize: :to_sym, default: :mixed
68
67
  ),
69
68
  OptInteger.new(
@@ -23,27 +23,32 @@ module WPScan
23
23
  ]
24
24
  end
25
25
 
26
- def run
27
- return unless ParsedCli.passwords
28
-
29
- if user_interaction?
30
- output('@info',
31
- msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
32
- end
33
-
34
- attack_opts = {
26
+ def attack_opts
27
+ @attack_opts ||= {
35
28
  show_progression: user_interaction?,
36
29
  multicall_max_passwords: ParsedCli.multicall_max_passwords
37
30
  }
31
+ end
32
+
33
+ def run
34
+ return unless ParsedCli.passwords
38
35
 
39
36
  begin
40
37
  found = []
41
38
 
42
- attacker.attack(users, passwords(ParsedCli.passwords), attack_opts) do |user|
39
+ if user_interaction?
40
+ output('@info',
41
+ msg: "Performing password attack on #{attacker.titleize} against #{users.size} user/s")
42
+ end
43
+
44
+ attacker.attack(users, ParsedCli.passwords, attack_opts) do |user|
43
45
  found << user
44
46
 
45
47
  attacker.progress_bar.log("[SUCCESS] - #{user.username} / #{user.password}")
46
48
  end
49
+ rescue Error::NoLoginInterfaceDetected => e
50
+ # TODO: Maybe output that in JSON as well.
51
+ output('@notice', msg: e.to_s) if user_interaction?
47
52
  ensure
48
53
  output('users', users: found)
49
54
  end
@@ -65,6 +70,8 @@ module WPScan
65
70
 
66
71
  case ParsedCli.password_attack
67
72
  when :wp_login
73
+ raise Error::NoLoginInterfaceDetected unless target.login_url
74
+
68
75
  Finders::Passwords::WpLogin.new(target)
69
76
  when :xmlrpc
70
77
  raise Error::XMLRPCNotDetected unless xmlrpc
@@ -82,7 +89,7 @@ module WPScan
82
89
  if xmlrpc&.enabled? &&
83
90
  xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
84
91
  xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
85
- .run.body !~ /XML\-RPC services are disabled/
92
+ .run.body !~ /XML-RPC services are disabled/
86
93
 
87
94
  true
88
95
  else
@@ -100,8 +107,10 @@ module WPScan
100
107
  else
101
108
  Finders::Passwords::XMLRPC.new(xmlrpc)
102
109
  end
103
- else
110
+ elsif target.login_url
104
111
  Finders::Passwords::WpLogin.new(target)
112
+ else
113
+ raise Error::NoLoginInterfaceDetected
105
114
  end
106
115
  end
107
116
 
@@ -113,15 +122,6 @@ module WPScan
113
122
  acc << Model::User.new(elem.chomp)
114
123
  end
115
124
  end
116
-
117
- # @param [ String ] wordlist_path
118
- #
119
- # @return [ Array<String> ]
120
- def passwords(wordlist_path)
121
- @passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
122
- acc << elem.chomp
123
- end
124
- end
125
125
  end
126
126
  end
127
127
  end
@@ -40,7 +40,7 @@ module WPScan
40
40
  # @return [ Hash ]
41
41
  def potential_urls(opts = {})
42
42
  urls = {}
43
- domain_name = PublicSuffix.domain(target.uri.host)[/(^[\w|-]+)/, 1]
43
+ domain_name = (PublicSuffix.domain(target.uri.host) || target.uri.host)[/(^[\w|-]+)/, 1]
44
44
 
45
45
  File.open(opts[:list]).each_with_index do |path, index|
46
46
  path.gsub!('{domain_name}', domain_name)
@@ -7,7 +7,7 @@ module WPScan
7
7
  class MuPlugins < CMSScanner::Finders::Finder
8
8
  # @return [ InterestingFinding ]
9
9
  def passive(_opts = {})
10
- pattern = %r{#{target.content_dir}/mu\-plugins/}i
10
+ pattern = %r{#{target.content_dir}/mu-plugins/}i
11
11
 
12
12
  target.in_scope_uris(target.homepage_res, '(//@href|//@src)[contains(., "mu-plugins")]') do |uri|
13
13
  next unless uri.path&.match?(pattern)
@@ -12,8 +12,8 @@ module WPScan
12
12
  location = res.headers_hash['location']
13
13
 
14
14
  return unless [200, 302].include?(res.code)
15
- return if res.code == 302 && location =~ /wp-login\.php\?action=register/
16
- return unless res.code == 200 || res.code == 302 && location =~ /wp-signup\.php/
15
+ return if res.code == 302 && location&.include?('wp-login.php?action=register')
16
+ return unless res.code == 200 || res.code == 302 && location&.include?('wp-signup.php')
17
17
 
18
18
  target.multisite = true
19
19
 
@@ -21,7 +21,7 @@ module WPScan
21
21
 
22
22
  def passive_from_css_href(res, opts)
23
23
  target.in_scope_uris(res, '//link/@href[contains(., "style.css")]') do |uri|
24
- next unless uri.path =~ %r{/themes/([^\/]+)/style.css\z}i
24
+ next unless uri.path =~ %r{/themes/([^/]+)/style.css\z}i
25
25
 
26
26
  return create_theme(Regexp.last_match[1], uri.to_s, opts)
27
27
  end
@@ -33,7 +33,7 @@ module WPScan
33
33
  code = tag.text.to_s
34
34
  next if code.empty?
35
35
 
36
- next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'\( ]*}i
36
+ next unless code =~ %r{#{item_code_pattern('themes')}\\?/style\.css[^"'( ]*}i
37
37
 
38
38
  return create_theme(Regexp.last_match[1], Regexp.last_match[0].strip, opts)
39
39
  end
@@ -13,7 +13,7 @@ module WPScan
13
13
 
14
14
  def valid_credentials?(response)
15
15
  response.code == 302 &&
16
- [*response.headers['Set-Cookie']]&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
16
+ Array(response.headers['Set-Cookie'])&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
17
17
  end
18
18
 
19
19
  def errored_response?(response)
@@ -12,11 +12,11 @@ module WPScan
12
12
  end
13
13
 
14
14
  def valid_credentials?(response)
15
- response.code == 200 && response.body =~ /blogName/
15
+ response.code == 200 && response.body.include?('blogName')
16
16
  end
17
17
 
18
18
  def errored_response?(response)
19
- response.code != 200 && response.body !~ /login_error/i
19
+ response.code != 200 && response.body !~ /Incorrect username or password/i
20
20
  end
21
21
  end
22
22
  end
@@ -22,8 +22,30 @@ module WPScan
22
22
  target.multi_call(methods, cache_ttl: 0).run
23
23
  end
24
24
 
25
+ # @param [ IO ] file
26
+ # @param [ Integer ] passwords_size
27
+ # @return [ Array<String> ] The passwords from the last checked position in the file until there are
28
+ # passwords_size passwords retrieved
29
+ def passwords_from_wordlist(file, passwords_size)
30
+ pwds = []
31
+ added_pwds = 0
32
+
33
+ return pwds if passwords_size.zero?
34
+
35
+ # Make sure that the main code does not call #sysseek or #count etc
36
+ # otherwise the file descriptor will be set to somwehere else
37
+ file.each_line(chomp: true) do |line|
38
+ pwds << line
39
+ added_pwds += 1
40
+
41
+ break if added_pwds == passwords_size
42
+ end
43
+
44
+ pwds
45
+ end
46
+
25
47
  # @param [ Array<Model::User> ] users
26
- # @param [ Array<String> ] passwords
48
+ # @param [ String ] wordlist_path
27
49
  # @param [ Hash ] opts
28
50
  # @option opts [ Boolean ] :show_progression
29
51
  # @option opts [ Integer ] :multicall_max_passwords
@@ -33,18 +55,22 @@ module WPScan
33
55
  # TODO: Make rubocop happy about metrics etc
34
56
  #
35
57
  # rubocop:disable all
36
- def attack(users, passwords, opts = {})
37
- wordlist_index = 0
58
+ def attack(users, wordlist_path, opts = {})
59
+ checked_passwords = 0
60
+ wordlist = File.open(wordlist_path)
61
+ wordlist_size = wordlist.count
38
62
  max_passwords = opts[:multicall_max_passwords]
39
63
  current_passwords_size = passwords_size(max_passwords, users.size)
40
64
 
41
- create_progress_bar(total: (passwords.size / current_passwords_size.round(1)).ceil,
65
+ create_progress_bar(total: (wordlist_size / current_passwords_size.round(1)).ceil,
42
66
  show_progression: opts[:show_progression])
43
67
 
68
+ wordlist.sysseek(0) # reset the descriptor to the beginning of the file as it changed with #count
69
+
44
70
  loop do
45
- current_users = users.select { |user| user.password.nil? }
46
- current_passwords = passwords[wordlist_index, current_passwords_size]
47
- wordlist_index += current_passwords_size
71
+ current_users = users.select { |user| user.password.nil? }
72
+ current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
73
+ checked_passwords += current_passwords_size
48
74
 
49
75
  break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
50
76
 
@@ -76,16 +102,19 @@ module WPScan
76
102
  break
77
103
  end
78
104
 
79
- progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
105
+ begin
106
+ progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
107
+ rescue ProgressBar::InvalidProgressError
108
+ end
80
109
  end
81
110
  end
82
111
  # Maybe a progress_bar.stop ?
83
112
  end
84
- # rubocop:disable all
113
+ # rubocop:enable all
85
114
 
86
115
  def passwords_size(max_passwords, users_size)
87
116
  return 1 if max_passwords < users_size
88
- return 0 if users_size == 0
117
+ return 0 if users_size.zero?
89
118
 
90
119
  max_passwords / users_size
91
120
  end
@@ -94,9 +123,13 @@ module WPScan
94
123
  def check_and_output_errors(res)
95
124
  progress_bar.log("Incorrect response: #{res.code} / #{res.return_message}") unless res.code == 200
96
125
 
97
- progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)') if res.body =~ /parse error. not well formed/i
126
+ if /parse error. not well formed/i.match?(res.body)
127
+ progress_bar.log('Parsing error, might be caused by a too high --max-passwords value (such as >= 2k)')
128
+ end
129
+
130
+ return unless /requested method [^ ]+ does not exist/i.match?(res.body)
98
131
 
99
- progress_bar.log('The requested method is not supported') if res.body =~ /requested method [^ ]+ does not exist/i
132
+ progress_bar.log('The requested method is not supported')
100
133
  end
101
134
  end
102
135
  end
@@ -48,7 +48,7 @@ module WPScan
48
48
  #
49
49
  # @return [ String, nil ] The version number detected from the stable tag
50
50
  def from_stable_tag(body)
51
- return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z\.-]+)/i
51
+ return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z.-]+)/i
52
52
 
53
53
  number = Regexp.last_match[1]
54
54
 
@@ -59,7 +59,7 @@ module WPScan
59
59
  #
60
60
  # @return [ String, nil ] The best version number detected from the changelog section
61
61
  def from_changelog_section(body)
62
- extracted_versions = body.scan(%r{[=]+\s+(?:v(?:ersion)?\s*)?([0-9\.-]+)[ \ta-z0-9\(\)\.\-\/]*[=]+}i)
62
+ extracted_versions = body.scan(%r{=+\s+(?:v(?:ersion)?\s*)?([0-9.-]+)[ \ta-z0-9().\-/]*=+}i)
63
63
 
64
64
  return if extracted_versions.nil? || extracted_versions.empty?
65
65
 
@@ -68,11 +68,9 @@ module WPScan
68
68
  extracted_versions = extracted_versions.select { |x| x =~ /[0-9]+/ }
69
69
 
70
70
  sorted = extracted_versions.sort do |x, y|
71
- begin
72
- Gem::Version.new(x) <=> Gem::Version.new(y)
73
- rescue StandardError
74
- 0
75
- end
71
+ Gem::Version.new(x) <=> Gem::Version.new(y)
72
+ rescue StandardError
73
+ 0
76
74
  end
77
75
 
78
76
  sorted.last
@@ -30,7 +30,7 @@ module WPScan
30
30
 
31
31
  # @return [ Version ]
32
32
  def style_version
33
- return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z\.-]+)/i
33
+ return unless Browser.get(target.style_url).body =~ /Version:[\t ]*(?!trunk)([0-9a-z.-]+)/i
34
34
 
35
35
  Model::Version.new(
36
36
  Regexp.last_match[1],
@@ -6,7 +6,7 @@ require_relative 'users/oembed_api'
6
6
  require_relative 'users/rss_generator'
7
7
  require_relative 'users/author_id_brute_forcing'
8
8
  require_relative 'users/login_error_messages'
9
- require_relative 'users/yoast_seo_author_sitemap.rb'
9
+ require_relative 'users/yoast_seo_author_sitemap'
10
10
 
11
11
  module WPScan
12
12
  module Finders
@@ -37,7 +37,7 @@ module WPScan
37
37
  # usernames from the potential Users found
38
38
  unames = opts[:found].map(&:username)
39
39
 
40
- [*opts[:list]].each { |uname| unames << uname.chomp }
40
+ Array(opts[:list]).each { |uname| unames << uname.chomp }
41
41
 
42
42
  unames.uniq
43
43
  end
@@ -13,7 +13,7 @@ module WPScan
13
13
  urls.each do |url|
14
14
  res = Browser.get_and_follow_location(url)
15
15
 
16
- next unless res.code == 200 && res.body =~ /<dc\:creator>/i
16
+ next unless res.code == 200 && res.body =~ /<dc:creator>/i
17
17
 
18
18
  potential_usernames = []
19
19
 
@@ -21,7 +21,7 @@ module WPScan
21
21
  loop do
22
22
  current_page += 1
23
23
 
24
- res = Typhoeus.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
24
+ res = Browser.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
25
25
 
26
26
  total_pages ||= res.headers['X-WP-TotalPages'].to_i
27
27
 
@@ -13,7 +13,7 @@ module WPScan
13
13
  found = []
14
14
 
15
15
  Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
16
- username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
16
+ username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
17
17
 
18
18
  next unless username && !username.strip.empty?
19
19
 
@@ -55,7 +55,7 @@ module WPScan
55
55
  #
56
56
  # @return [ Regexp ]
57
57
  def item_code_pattern(type)
58
- @item_code_pattern ||= %r{["'\( ]#{item_url_pattern(type)}([^\\\/\)"']+)}i
58
+ @item_code_pattern ||= %r{["'( ]#{item_url_pattern(type)}([^\\/)"']+)}i
59
59
  end
60
60
 
61
61
  # @param [ String ] type
@@ -66,9 +66,9 @@ module WPScan
66
66
  item_url = type == 'plugins' ? target.plugins_url : target.content_url
67
67
 
68
68
  url = /#{item_url.gsub(/\A(?:https?)/i, 'https?').gsub('/', '\\\\\?\/')}/i
69
- item_dir = %r{(?:#{url}|\\?\/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
69
+ item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
70
70
 
71
- type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?\/}i
71
+ type == 'plugins' ? item_dir : %r{#{item_dir}#{type}\\?/}i
72
72
  end
73
73
  end
74
74
  end
@@ -20,7 +20,7 @@ module WPScan
20
20
  class DebugLog < InterestingFinding
21
21
  # @ return [ Hash ]
22
22
  def references
23
- @references ||= { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
23
+ @references ||= { url: ['https://codex.wordpress.org/Debugging_in_WordPress'] }
24
24
  end
25
25
  end
26
26
 
@@ -38,7 +38,7 @@ module WPScan
38
38
 
39
39
  # @return [ Array<String> ]
40
40
  def potential_readme_filenames
41
- @potential_readme_filenames ||= [*(DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super)]
41
+ @potential_readme_filenames ||= Array((DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super))
42
42
  end
43
43
  end
44
44
  end
@@ -45,7 +45,7 @@ module WPScan
45
45
  # @return [ Theme ]
46
46
  def parent_theme
47
47
  return unless template
48
- return unless style_body =~ /^@import\surl\(["']?([^"'\)]+)["']?\);\s*$/i
48
+ return unless style_body =~ /^@import\surl\(["']?([^"')]+)["']?\);\s*$/i
49
49
 
50
50
  opts = detection_opts.merge(
51
51
  style_url: url(Regexp.last_match[1]),
@@ -101,7 +101,7 @@ module WPScan
101
101
  #
102
102
  # @return [ String ]
103
103
  def parse_style_tag(body, tag)
104
- value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n\*]+)/i, 1]
104
+ value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n*]+)/i, 1]
105
105
 
106
106
  value && !value.strip.empty? ? value.strip : nil
107
107
  end
@@ -40,9 +40,9 @@ module WPScan
40
40
  def rce_132_vuln
41
41
  Vulnerability.new(
42
42
  'Timthumb <= 1.32 Remote Code Execution',
43
- { exploitdb: ['17602'] },
44
- 'RCE',
45
- '1.33'
43
+ references: { exploitdb: ['17602'] },
44
+ type: 'RCE',
45
+ fixed_in: '1.33'
46
46
  )
47
47
  end
48
48
 
@@ -50,12 +50,12 @@ module WPScan
50
50
  def rce_webshot_vuln
51
51
  Vulnerability.new(
52
52
  'Timthumb <= 2.8.13 WebShot Remote Code Execution',
53
- {
53
+ references: {
54
54
  url: ['http://seclists.org/fulldisclosure/2014/Jun/117', 'https://github.com/wpscanteam/wpscan/issues/519'],
55
55
  cve: '2014-4663'
56
56
  },
57
- 'RCE',
58
- '2.8.14'
57
+ type: 'RCE',
58
+ fixed_in: '2.8.14'
59
59
  )
60
60
  end
61
61
 
@@ -39,7 +39,7 @@ module WPScan
39
39
 
40
40
  @vulnerabilities = []
41
41
 
42
- [*db_data['vulnerabilities']].each do |json_vuln|
42
+ Array(db_data['vulnerabilities']).each do |json_vuln|
43
43
  vulnerability = Vulnerability.load_from_json(json_vuln)
44
44
  @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
45
45
  end
@@ -53,7 +53,7 @@ module WPScan
53
53
 
54
54
  @vulnerabilities = []
55
55
 
56
- [*db_data['vulnerabilities']].each do |json_vuln|
56
+ Array(db_data['vulnerabilities']).each do |json_vuln|
57
57
  @vulnerabilities << Vulnerability.load_from_json(json_vuln)
58
58
  end
59
59
 
@@ -2,7 +2,7 @@
2
2
  <% if @users.empty? -%>
3
3
  <%= notice_icon %> No Valid Passwords Found.
4
4
  <% else -%>
5
- <%= notice_icon %> Valid Combinations Found:
5
+ <%= critical_icon %> Valid Combinations Found:
6
6
  <% @users.each do |user| -%>
7
7
  | Username: <%= user.username %>, Password: <%= user.password %>
8
8
  <% end -%>
@@ -1,4 +1,7 @@
1
1
  | <%= critical_icon %> Title: <%= @v.title %>
2
+ <% if @v.cvss -%>
3
+ | CVSS: <%= @v.cvss[:score] %> (<%= @v.cvss[:vector] %>)
4
+ <% end -%>
2
5
  <% if @v.fixed_in -%>
3
6
  | Fixed in: <%= @v.fixed_in %>
4
7
  <% end -%>
@@ -19,6 +19,9 @@
19
19
  <% vulns.each_with_index do |v, index| -%>
20
20
  {
21
21
  "title": <%= v.title.to_json %>,
22
+ <% if v.cvss -%>
23
+ "cvss": <%= v.cvss.to_json %>,
24
+ <% end -%>
22
25
  "fixed_in": <%= v.fixed_in.to_json %>,
23
26
  "references": <%= v.references.to_json %>
24
27
  }<% unless index == last_index -%>,<% end -%>
@@ -139,24 +139,22 @@ module WPScan
139
139
  updated = []
140
140
 
141
141
  FILES.each do |filename|
142
- begin
143
- db_checksum = remote_file_checksum(filename)
142
+ db_checksum = remote_file_checksum(filename)
144
143
 
145
- # Checking if the file needs to be updated
146
- next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
144
+ # Checking if the file needs to be updated
145
+ next if File.exist?(local_file_path(filename)) && db_checksum == local_file_checksum(filename)
147
146
 
148
- create_backup(filename)
149
- dl_checksum = download(filename)
147
+ create_backup(filename)
148
+ dl_checksum = download(filename)
150
149
 
151
- raise Error::ChecksumsMismatch, filename unless dl_checksum == db_checksum
150
+ raise Error::ChecksumsMismatch, filename unless dl_checksum == db_checksum
152
151
 
153
- updated << filename
154
- rescue StandardError => e
155
- restore_backup(filename)
156
- raise e
157
- ensure
158
- delete_backup(filename) if File.exist?(backup_file_path(filename))
159
- end
152
+ updated << filename
153
+ rescue StandardError => e
154
+ restore_backup(filename)
155
+ raise e
156
+ ensure
157
+ delete_backup(filename) if File.exist?(backup_file_path(filename))
160
158
  end
161
159
 
162
160
  File.write(last_update_file, Time.now)
@@ -29,5 +29,11 @@ module WPScan
29
29
  ' use the --scope option or make sure the --url value given is the correct one'
30
30
  end
31
31
  end
32
+
33
+ class NoLoginInterfaceDetected < Standard
34
+ def to_s
35
+ 'Could not find a login interface to perform the password attack against'
36
+ end
37
+ end
32
38
  end
33
39
  end
@@ -17,7 +17,7 @@ module WPScan
17
17
  end
18
18
 
19
19
  # Needed to have inheritance of the @child_class_constants
20
- # If inheritance is not needed, then the #child_class_constant can be used in the classe definition, ie
20
+ # If inheritance is not needed, then the #child_class_constant can be used in the class definition, ie
21
21
  # child_class_constant :FILES, PATTERN: /aaa/i
22
22
  # @return [ Hash ]
23
23
  def self.child_class_constants
@@ -11,7 +11,7 @@ module WPScan
11
11
 
12
12
  def self.child_class_constants
13
13
  @child_class_constants ||= super.merge(
14
- PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 70
14
+ PARSER: nil, KEY: nil, PATTERN: /(?<v>\d+\.[.\d]+)/, CONFIDENCE: 70
15
15
  )
16
16
  end
17
17
 
@@ -21,13 +21,11 @@ module WPScan
21
21
  parsers = ALLOWED_PARSERS.include?(self.class::PARSER) ? [self.class::PARSER] : ALLOWED_PARSERS
22
22
 
23
23
  parsers.each do |parser|
24
- begin
25
- parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
24
+ parsed = parser.respond_to?(:safe_load) ? parser.safe_load(body) : parser.load(body)
26
25
 
27
- return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
28
- rescue StandardError
29
- next
30
- end
26
+ return parsed if parsed.is_a?(Hash) || parsed.is_a?(Array)
27
+ rescue StandardError
28
+ next
31
29
  end
32
30
 
33
31
  nil # Make sure nil is returned in case none of the parsers managed to parse the body correctly
@@ -9,7 +9,7 @@ module WPScan
9
9
  # @return [ Hash ]
10
10
  def self.child_class_constants
11
11
  @child_class_constants ||= super().merge(
12
- XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)\=(?<v>\d+\.[\.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
12
+ XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)=(?<v>\d+\.[.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
13
13
  )
14
14
  end
15
15
 
@@ -9,7 +9,7 @@ module WPScan
9
9
  # @return [ Hash ]
10
10
  def self.child_class_constants
11
11
  @child_class_constants ||= super().merge(
12
- XPATH: nil, PATTERN: /\A(?<v>\d+\.[\.\d]+)/, CONFIDENCE: 60
12
+ XPATH: nil, PATTERN: /\A(?<v>\d+\.[.\d]+)/, CONFIDENCE: 60
13
13
  )
14
14
  end
15
15
 
@@ -33,7 +33,7 @@ module WPScan
33
33
 
34
34
  # @return [ Hash ]
35
35
  def self.child_class_constants
36
- @child_class_constants ||= super().merge(PATTERN: /ver\=(?<v>\d+\.[\.\d]+)/i)
36
+ @child_class_constants ||= super().merge(PATTERN: /ver=(?<v>\d+\.[.\d]+)/i)
37
37
  end
38
38
  end
39
39
 
@@ -13,7 +13,7 @@ end
13
13
  #
14
14
  # @return [ Symbol ]
15
15
  def classify_slug(slug)
16
- classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/\-{1,}/, '_').camelize.to_s
16
+ classified = slug.to_s.gsub(/[^a-z\d\-]/i, '-').gsub(/-{1,}/, '_').camelize.to_s
17
17
  classified = "D_#{classified}" if /\d/.match?(classified[0])
18
18
 
19
19
  classified.to_sym
@@ -2,7 +2,7 @@
2
2
 
3
3
  module WPScan
4
4
  # References module (which should be included along with the CMSScanner::References)
5
- # to allow the use of the wpvulndb reference
5
+ # to allow the use of the wpvulndb reference.
6
6
  module References
7
7
  extend ActiveSupport::Concern
8
8
 
@@ -19,13 +19,13 @@ module WPScan
19
19
  # @return [ Boolean ]
20
20
  def vulnerable?
21
21
  [@wp_version, @main_theme, @plugins, @themes, @timthumbs].each do |e|
22
- [*e].each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
22
+ Array(e).each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
23
23
  end
24
24
 
25
- return true unless [*@config_backups].empty?
26
- return true unless [*@db_exports].empty?
25
+ return true unless Array(@config_backups).empty?
26
+ return true unless Array(@db_exports).empty?
27
27
 
28
- [*@users].each { |u| return true if u.password }
28
+ Array(@users).each { |u| return true if u.password }
29
29
 
30
30
  false
31
31
  end
@@ -11,9 +11,9 @@ module WPScan
11
11
  module WordPress
12
12
  include CMSScanner::Target::Platform::PHP
13
13
 
14
- WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu\-)?plugins|uploads))|wp-includes)/}i.freeze
15
- WP_JSON_OEMBED_PATTERN = %r{/wp\-json/oembed/}i.freeze
16
- WP_ADMIN_AJAX_PATTERN = %r{\\?/wp\-admin\\?/admin\-ajax\.php}i.freeze
14
+ WORDPRESS_PATTERN = %r{/(?:(?:wp-content/(?:themes|(?:mu-)?plugins|uploads))|wp-includes)/}i.freeze
15
+ WP_JSON_OEMBED_PATTERN = %r{/wp-json/oembed/}i.freeze
16
+ WP_ADMIN_AJAX_PATTERN = %r{\\?/wp-admin\\?/admin-ajax\.php}i.freeze
17
17
 
18
18
  # These methods are used in the associated interesting_findings finders
19
19
  # to keep the boolean state of the finding rather than re-check the whole thing again
@@ -139,15 +139,16 @@ module WPScan
139
139
  # the first time the method is called, and the effective_url is then used
140
140
  # if suitable, otherwise the default wp-login will be.
141
141
  #
142
- # @return [ String ] The URL to the login page
142
+ # @return [ String, false ] The URL to the login page or false if not detected
143
143
  def login_url
144
- return @login_url if @login_url
144
+ return @login_url unless @login_url.nil?
145
145
 
146
- @login_url = url('wp-login.php')
146
+ @login_url = url('wp-login.php') # TODO: url(ParsedCli.login_uri)
147
147
 
148
148
  res = Browser.get_and_follow_location(@login_url)
149
149
 
150
- @login_url = res.effective_url if res.effective_url =~ /wp\-login\.php\z/i && in_scope?(res.effective_url)
150
+ @login_url = res.effective_url if res.effective_url =~ /wp-login\.php\z/i && in_scope?(res.effective_url)
151
+ @login_url = false if res.code == 404
151
152
 
152
153
  @login_url
153
154
  end
@@ -104,7 +104,7 @@ module WPScan
104
104
  return @sub_dir unless @sub_dir.nil?
105
105
 
106
106
  # url_pattern is from CMSScanner::Target
107
- pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp\-includes/)}i
107
+ pattern = %r{#{url_pattern}(.+?)/(?:xmlrpc\.php|wp-includes/)}i
108
108
  xpath = '(//@src|//@href|//@data-src)[contains(., "xmlrpc.php") or contains(., "wp-includes/")]'
109
109
 
110
110
  [homepage_res, error_404_res].each do |page_res|
@@ -124,9 +124,9 @@ module WPScan
124
124
  def url(path = nil)
125
125
  return @uri.to_s unless path
126
126
 
127
- if %r{wp\-content/plugins}i.match?(path)
127
+ if %r{wp-content/plugins}i.match?(path)
128
128
  path = +path.gsub('wp-content/plugins', plugins_dir)
129
- elsif /wp\-content/i.match?(path)
129
+ elsif /wp-content/i.match?(path)
130
130
  path = +path.gsub('wp-content', content_dir)
131
131
  elsif path[0] != '/' && sub_dir
132
132
  path = "#{sub_dir}/#{path}"
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version
4
4
  module WPScan
5
- VERSION = '3.7.10'
5
+ VERSION = '3.8.3'
6
6
  end
@@ -18,9 +18,10 @@ module WPScan
18
18
 
19
19
  new(
20
20
  json_data['title'],
21
- references,
22
- json_data['vuln_type'],
23
- json_data['fixed_in']
21
+ references: references,
22
+ type: json_data['vuln_type'],
23
+ fixed_in: json_data['fixed_in'],
24
+ cvss: json_data['cvss']&.symbolize_keys
24
25
  )
25
26
  end
26
27
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wpscan
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.10
4
+ version: 3.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - WPScanTeam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-09 00:00:00.000000000 Z
11
+ date: 2020-07-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cms_scanner
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.8.5
19
+ version: 0.12.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.8.5
26
+ version: 0.12.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -100,28 +100,28 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.80.0
103
+ version: 0.88.0
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: 0.80.0
110
+ version: 0.88.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: rubocop-performance
113
113
  requirement: !ruby/object:Gem::Requirement
114
114
  requirements:
115
115
  - - "~>"
116
116
  - !ruby/object:Gem::Version
117
- version: 1.5.0
117
+ version: 1.7.0
118
118
  type: :development
119
119
  prerelease: false
120
120
  version_requirements: !ruby/object:Gem::Requirement
121
121
  requirements:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
- version: 1.5.0
124
+ version: 1.7.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: simplecov
127
127
  requirement: !ruby/object:Gem::Requirement
@@ -388,7 +388,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
388
388
  requirements:
389
389
  - - ">="
390
390
  - !ruby/object:Gem::Version
391
- version: '2.4'
391
+ version: '2.5'
392
392
  required_rubygems_version: !ruby/object:Gem::Requirement
393
393
  requirements:
394
394
  - - ">="