wpscan 3.8.1 → 3.8.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -13
  3. data/app/controllers/password_attack.rb +22 -22
  4. data/app/finders/db_exports/known_locations.rb +1 -1
  5. data/app/finders/interesting_findings/mu_plugins.rb +1 -1
  6. data/app/finders/interesting_findings/multisite.rb +2 -2
  7. data/app/finders/main_theme/css_style_in_homepage.rb +2 -2
  8. data/app/finders/main_theme/urls_in_homepage.rb +1 -1
  9. data/app/finders/passwords/wp_login.rb +1 -1
  10. data/app/finders/passwords/xml_rpc.rb +1 -1
  11. data/app/finders/passwords/xml_rpc_multicall.rb +45 -12
  12. data/app/finders/plugin_version/readme.rb +2 -2
  13. data/app/finders/theme_version/style.rb +1 -1
  14. data/app/finders/users.rb +3 -1
  15. data/app/finders/users/author_sitemap.rb +36 -0
  16. data/app/finders/users/login_error_messages.rb +1 -1
  17. data/app/finders/users/rss_generator.rb +1 -1
  18. data/app/finders/users/yoast_seo_author_sitemap.rb +1 -21
  19. data/app/finders/wp_items/urls_in_page.rb +5 -5
  20. data/app/models/interesting_finding.rb +22 -3
  21. data/app/models/plugin.rb +1 -1
  22. data/app/models/theme.rb +2 -2
  23. data/app/models/wp_item.rb +1 -1
  24. data/app/models/wp_version.rb +1 -1
  25. data/lib/wpscan/db/dynamic_finders/base.rb +1 -1
  26. data/lib/wpscan/db/dynamic_finders/plugin.rb +1 -1
  27. data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
  28. data/lib/wpscan/errors/wordpress.rb +6 -0
  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 +9 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f45c3ef9c7678acbc8608e823e3bd251dad461cde0b524fc57aa68447790f302
4
- data.tar.gz: 4d3946d3dabbbb94f3eccf83956d31545155071588bc5aafb5f5aea452257172
3
+ metadata.gz: f98f734f7109df65f502a120586451cb9cdcd1f741a03db2d664e5e2b0ebde05
4
+ data.tar.gz: da340ec87c3ac0603b0ffe9fb518067c9cea0596d59bedc15e0dabd7f7e7cfb1
5
5
  SHA512:
6
- metadata.gz: 0adff2352b84b2f791c2b71fa63ec95d03ff3244baa86f04de0fc469193099708065fce65c09a54891edd61ef71d647d711e7627835c5d480ce3f97fe1a8339e
7
- data.tar.gz: 677af33b40cbdab1435d65780c4ee89eed0757bdc9981a002572bb0478391eade2eca9706c6288b9083c7b2ff6db0857495914cbde0960b09b5a937438ea24c1
6
+ metadata.gz: 6d45b4fbc1a60f0f804b4fe59815da1ca324693c1546cd1cd91b75fea0aab363fef93ab240ef1009417e8cb7b5230642b6ff464acd90a0bbb917f0ecce915171
7
+ data.tar.gz: 4e0c504291a53f475f834f6b36a14e6634fca79f5cfc03c2375815a509c154047c8da98e25da026d30edd8c4e3bbb2ded4a29d4a678d0e2fe5d10646a6f09f9b
data/README.md CHANGED
@@ -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.
@@ -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
@@ -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
  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
 
@@ -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
 
@@ -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
@@ -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.1'
5
+ VERSION = '3.8.6'
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.1
4
+ version: 3.8.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - WPScanTeam
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-16 00:00:00.000000000 Z
11
+ date: 2020-08-08 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.0
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.0
26
+ version: 0.12.1
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.81.0
103
+ version: 0.89.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.81.0
110
+ version: 0.89.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
@@ -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