wpscan 3.8.2 → 3.8.7

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/password_attack.rb +22 -22
  3. data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
  4. data/app/finders/interesting_findings/mu_plugins.rb +1 -1
  5. data/app/finders/interesting_findings/multisite.rb +2 -2
  6. data/app/finders/main_theme/css_style_in_homepage.rb +2 -2
  7. data/app/finders/main_theme/urls_in_homepage.rb +1 -1
  8. data/app/finders/passwords/wp_login.rb +1 -1
  9. data/app/finders/passwords/xml_rpc.rb +1 -1
  10. data/app/finders/passwords/xml_rpc_multicall.rb +35 -9
  11. data/app/finders/plugin_version/readme.rb +2 -2
  12. data/app/finders/theme_version/style.rb +1 -1
  13. data/app/finders/users.rb +3 -1
  14. data/app/finders/users/author_sitemap.rb +36 -0
  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/yoast_seo_author_sitemap.rb +1 -21
  18. data/app/finders/wp_items/urls_in_page.rb +5 -5
  19. data/app/models/interesting_finding.rb +22 -3
  20. data/app/models/plugin.rb +1 -1
  21. data/app/models/theme.rb +2 -2
  22. data/app/models/wp_item.rb +1 -1
  23. data/app/models/wp_version.rb +1 -1
  24. data/lib/wpscan/db/dynamic_finders/base.rb +1 -1
  25. data/lib/wpscan/db/dynamic_finders/plugin.rb +1 -1
  26. data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
  27. data/lib/wpscan/errors/wordpress.rb +6 -0
  28. data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -3
  29. data/lib/wpscan/finders/dynamic_finder/version/config_parser.rb +1 -1
  30. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +1 -1
  31. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  32. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  33. data/lib/wpscan/helper.rb +1 -1
  34. data/lib/wpscan/target.rb +4 -4
  35. data/lib/wpscan/target/platform/wordpress.rb +8 -7
  36. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  37. data/lib/wpscan/version.rb +1 -1
  38. metadata +11 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '095c33e6d410081b90f0ea858284cd4c2040b551830fd1756ab7f70dcae34022'
4
- data.tar.gz: b8f36a805212d33d7448ebba76a908a2a0cf51e72d1b3df6ce3e434987359864
3
+ metadata.gz: cfd5e9c7cc41b0b10e4fe74b8b0d876f09cbb4916d94ec3bb23124dac9c5ad5b
4
+ data.tar.gz: f0aa65190aa0fd9fa3b9b4fbc0296824004d0a7766820a7024b687d3b2b73825
5
5
  SHA512:
6
- metadata.gz: 921466d7d508f0d6f6dddd8e53bab8bf1ce0a7202c778f477ce669c724c9a5348a3e94befafc51e18a331dcc8566946c330493c69c415ba8701612bc59efe4ad
7
- data.tar.gz: eba875df92089460d02b2bf8b4d00b47149f3d176ff203767dbe02b4a20612db0e868c3b3f17e1e5b3a1f16096f1d89d704ecbbee854cc5f2de7a3b39fea6855
6
+ metadata.gz: e8e747c12daf21501a65252709bd305d4d8aa148fe14fca66d3af5aadb0c2188e554f2fa7f34cab7936e0428c9c1c43437ed2b6fb140ad91c9914a8f5c902972
7
+ data.tar.gz: 44eef7565acae4f7db9eb7ac30e6e8bff302d7ad5c2d46c019449d8308266cd8ffbec4eb74ac7b69653426971c9f4d5ce0efa5cd9c4da4930212f7fb2a5cbcf5
@@ -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
@@ -81,8 +88,8 @@ module WPScan
81
88
  def xmlrpc_get_users_blogs_enabled?
82
89
  if xmlrpc&.enabled? &&
83
90
  xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
84
- xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
85
- .run.body !~ /XML\-RPC services are disabled/
91
+ !xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
92
+ .run.body.match?(/>\s*405\s*</)
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
@@ -9,7 +9,7 @@ module WPScan
9
9
  def aggressive(_opts = {})
10
10
  path = 'installer-log.txt'
11
11
 
12
- return unless /DUPLICATOR INSTALL-LOG/.match?(target.head_and_get(path).body)
12
+ return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.match?(target.head_and_get(path).body)
13
13
 
14
14
  Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
15
15
  end
@@ -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
  def passive(opts = {})
14
14
  found = []
15
15
 
16
- slugs = items_from_links('themes', false) + items_from_codes('themes', false)
16
+ slugs = items_from_links('themes', uniq: false) + items_from_codes('themes', uniq: false)
17
17
 
18
18
  slugs.each_with_object(Hash.new(0)) { |slug, counts| counts[slug] += 1 }.each do |slug, occurences|
19
19
  found << Model::Theme.new(slug, target, opts.merge(found_by: found_by, confidence: 2 * occurences))
@@ -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,7 +12,7 @@ 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)
@@ -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
 
@@ -75,9 +101,9 @@ module WPScan
75
101
  progress_bar.stop
76
102
  break
77
103
  end
78
-
104
+
79
105
  begin
80
- progress_bar.total = progress_bar.progress + ((passwords.size - wordlist_index) / current_passwords_size.round(1)).ceil
106
+ progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
81
107
  rescue ProgressBar::InvalidProgressError
82
108
  end
83
109
  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
 
@@ -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,8 @@ 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/author_sitemap'
10
+ require_relative 'users/yoast_seo_author_sitemap'
10
11
 
11
12
  module WPScan
12
13
  module Finders
@@ -22,6 +23,7 @@ module WPScan
22
23
  Users::WpJsonApi.new(target) <<
23
24
  Users::OembedApi.new(target) <<
24
25
  Users::RSSGenerator.new(target) <<
26
+ Users::AuthorSitemap.new(target) <<
25
27
  Users::YoastSeoAuthorSitemap.new(target) <<
26
28
  Users::AuthorIdBruteForcing.new(target) <<
27
29
  Users::LoginErrorMessages.new(target)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WPScan
4
+ module Finders
5
+ module Users
6
+ # Since WP 5.5, /wp-sitemap-users-1.xml is generated and contains
7
+ # the usernames of accounts who made a post
8
+ class AuthorSitemap < CMSScanner::Finders::Finder
9
+ # @param [ Hash ] opts
10
+ #
11
+ # @return [ Array<User> ]
12
+ def aggressive(_opts = {})
13
+ found = []
14
+
15
+ Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
16
+ username = user_tag.text.to_s[%r{/author/([^/]+)/}, 1]
17
+
18
+ next unless username && !username.strip.empty?
19
+
20
+ found << Model::User.new(username,
21
+ found_by: found_by,
22
+ confidence: 100,
23
+ interesting_entries: [sitemap_url])
24
+ end
25
+
26
+ found
27
+ end
28
+
29
+ # @return [ String ] The URL of the sitemap
30
+ def sitemap_url
31
+ @sitemap_url ||= target.url('wp-sitemap-users-1.xml')
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -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
 
@@ -5,27 +5,7 @@ module WPScan
5
5
  module Users
6
6
  # The YOAST SEO plugin has an author-sitemap.xml which can leak usernames
7
7
  # See https://github.com/wpscanteam/wpscan/issues/1228
8
- class YoastSeoAuthorSitemap < CMSScanner::Finders::Finder
9
- # @param [ Hash ] opts
10
- #
11
- # @return [ Array<User> ]
12
- def aggressive(_opts = {})
13
- found = []
14
-
15
- Browser.get(sitemap_url).html.xpath('//url/loc').each do |user_tag|
16
- username = user_tag.text.to_s[%r{/author/([^\/]+)/}, 1]
17
-
18
- next unless username && !username.strip.empty?
19
-
20
- found << Model::User.new(username,
21
- found_by: found_by,
22
- confidence: 100,
23
- interesting_entries: [sitemap_url])
24
- end
25
-
26
- found
27
- end
28
-
8
+ class YoastSeoAuthorSitemap < AuthorSitemap
29
9
  # @return [ String ] The URL of the author-sitemap
30
10
  def sitemap_url
31
11
  @sitemap_url ||= target.url('author-sitemap.xml')
@@ -9,7 +9,7 @@ module WPScan
9
9
  # @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
10
10
  #
11
11
  # @return [ Array<String> ] The plugins/themes detected in the href, src attributes of the page
12
- def items_from_links(type, uniq = true)
12
+ def items_from_links(type, uniq: true)
13
13
  found = []
14
14
  xpath = format(
15
15
  '(//@href|//@src|//@data-src)[contains(., "%s")]',
@@ -31,7 +31,7 @@ module WPScan
31
31
  # @param [ Boolean ] uniq Wether or not to apply the #uniq on the results
32
32
  #
33
33
  # @return [Array<String> ] The plugins/themes detected in the javascript/style of the homepage
34
- def items_from_codes(type, uniq = true)
34
+ def items_from_codes(type, uniq: true)
35
35
  found = []
36
36
 
37
37
  page_res.html.xpath('//script[not(@src)]|//style[not(@src)]').each do |tag|
@@ -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
@@ -7,10 +7,11 @@ module WPScan
7
7
  include References
8
8
  end
9
9
 
10
- #
11
- # Some classes are empty for the #type to be correctly displayed (as taken from the self.class from the parent)
12
- #
13
10
  class BackupDB < InterestingFinding
11
+ def to_s
12
+ @to_s ||= "A backup directory has been found: #{url}"
13
+ end
14
+
14
15
  # @return [ Hash ]
15
16
  def references
16
17
  @references ||= { url: ['https://github.com/wpscanteam/wpscan/issues/422'] }
@@ -18,6 +19,10 @@ module WPScan
18
19
  end
19
20
 
20
21
  class DebugLog < InterestingFinding
22
+ def to_s
23
+ @to_s ||= "Debug Log found: #{url}"
24
+ end
25
+
21
26
  # @ return [ Hash ]
22
27
  def references
23
28
  @references ||= { url: ['https://codex.wordpress.org/Debugging_in_WordPress'] }
@@ -40,6 +45,10 @@ module WPScan
40
45
  end
41
46
 
42
47
  class FullPathDisclosure < InterestingFinding
48
+ def to_s
49
+ @to_s ||= "Full Path Disclosure found: #{url}"
50
+ end
51
+
43
52
  # @return [ Hash ]
44
53
  def references
45
54
  @references ||= { url: ['https://www.owasp.org/index.php/Full_Path_Disclosure'] }
@@ -71,6 +80,9 @@ module WPScan
71
80
  end
72
81
 
73
82
  class Readme < InterestingFinding
83
+ def to_s
84
+ @to_s ||= "WordPress readme found: #{url}"
85
+ end
74
86
  end
75
87
 
76
88
  class Registration < InterestingFinding
@@ -81,6 +93,10 @@ module WPScan
81
93
  end
82
94
 
83
95
  class TmmDbMigrate < InterestingFinding
96
+ def to_s
97
+ @to_s ||= "ThemeMakers migration file found: #{url}"
98
+ end
99
+
84
100
  # @return [ Hash ]
85
101
  def references
86
102
  @references ||= { packetstorm: [131_957] }
@@ -95,6 +111,9 @@ module WPScan
95
111
  end
96
112
 
97
113
  class UploadSQLDump < InterestingFinding
114
+ def to_s
115
+ @to_s ||= "SQL Dump found: #{url}"
116
+ end
98
117
  end
99
118
 
100
119
  class WPCron < InterestingFinding
@@ -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[/\b#{Regexp.escape(tag)}:[\t ]*([^\r\n*]+)/, 1]
105
105
 
106
106
  value && !value.strip.empty? ? value.strip : nil
107
107
  end
@@ -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
 
@@ -31,7 +31,7 @@ module WPScan
31
31
 
32
32
  finder_configs(
33
33
  finder_class,
34
- Regexp.last_match[1] == 'aggressive'
34
+ aggressive: Regexp.last_match[1] == 'aggressive'
35
35
  )
36
36
  end
37
37
 
@@ -16,7 +16,7 @@ module WPScan
16
16
  # @param [ Symbol ] finder_class
17
17
  # @param [ Boolean ] aggressive
18
18
  # @return [ Hash ]
19
- def self.finder_configs(finder_class, aggressive = false)
19
+ def self.finder_configs(finder_class, aggressive: false)
20
20
  configs = {}
21
21
 
22
22
  return configs unless allowed_classes.include?(finder_class)
@@ -24,7 +24,7 @@ module WPScan
24
24
  # @param [ Symbol ] finder_class
25
25
  # @param [ Boolean ] aggressive
26
26
  # @return [ Hash ]
27
- def self.finder_configs(finder_class, aggressive = false)
27
+ def self.finder_configs(finder_class, aggressive: false)
28
28
  configs = {}
29
29
 
30
30
  return configs unless allowed_classes.include?(finder_class)
@@ -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
@@ -56,9 +56,7 @@ module WPScan
56
56
 
57
57
  homepage_result = find(target.homepage_res, opts)
58
58
 
59
- if homepage_result
60
- return homepage_result unless homepage_result.is_a?(Array) && homepage_result.empty?
61
- end
59
+ return homepage_result unless homepage_result.nil? || homepage_result&.is_a?(Array) && homepage_result&.empty?
62
60
 
63
61
  find(target.error_404_res, opts)
64
62
  end
@@ -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
 
@@ -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
@@ -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.8.2'
5
+ VERSION = '3.8.7'
6
6
  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.8.2
4
+ version: 3.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - WPScanTeam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-08 00:00:00.000000000 Z
11
+ date: 2020-09-10 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.10.1
19
+ version: 0.12.1
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.10.1
26
+ version: 0.12.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: bundler
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -100,42 +100,42 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: 0.85.0
103
+ version: 0.90.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.85.0
110
+ version: 0.90.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.6.0
117
+ version: 1.8.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.6.0
124
+ version: 1.8.0
125
125
  - !ruby/object:Gem::Dependency
126
126
  name: simplecov
127
127
  requirement: !ruby/object:Gem::Requirement
128
128
  requirements:
129
129
  - - "~>"
130
130
  - !ruby/object:Gem::Version
131
- version: 0.18.2
131
+ version: 0.19.0
132
132
  type: :development
133
133
  prerelease: false
134
134
  version_requirements: !ruby/object:Gem::Requirement
135
135
  requirements:
136
136
  - - "~>"
137
137
  - !ruby/object:Gem::Version
138
- version: 0.18.2
138
+ version: 0.19.0
139
139
  - !ruby/object:Gem::Dependency
140
140
  name: simplecov-lcov
141
141
  requirement: !ruby/object:Gem::Requirement
@@ -258,6 +258,7 @@ files:
258
258
  - app/finders/users.rb
259
259
  - app/finders/users/author_id_brute_forcing.rb
260
260
  - app/finders/users/author_posts.rb
261
+ - app/finders/users/author_sitemap.rb
261
262
  - app/finders/users/login_error_messages.rb
262
263
  - app/finders/users/oembed_api.rb
263
264
  - app/finders/users/rss_generator.rb