wpscan 3.7.11 → 3.8.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +9 -15
  3. data/app/controllers/enumeration/cli_options.rb +2 -3
  4. data/app/controllers/password_attack.rb +22 -22
  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 +1 -1
  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/yoast_seo_author_sitemap.rb +1 -1
  18. data/app/finders/wp_items/urls_in_page.rb +3 -3
  19. data/app/models/plugin.rb +1 -1
  20. data/app/models/theme.rb +2 -2
  21. data/app/models/timthumb.rb +6 -6
  22. data/app/models/wp_item.rb +1 -1
  23. data/app/models/wp_version.rb +1 -1
  24. data/app/views/cli/password_attack/users.erb +1 -1
  25. data/app/views/cli/vulnerability.erb +3 -0
  26. data/app/views/json/finding.erb +3 -0
  27. data/lib/wpscan/db/updater.rb +12 -14
  28. data/lib/wpscan/errors/wordpress.rb +6 -0
  29. data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
  30. data/lib/wpscan/finders/dynamic_finder/version/config_parser.rb +5 -7
  31. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +1 -1
  32. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  33. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  34. data/lib/wpscan/helper.rb +1 -1
  35. data/lib/wpscan/references.rb +1 -1
  36. data/lib/wpscan/target.rb +4 -4
  37. data/lib/wpscan/target/platform/wordpress.rb +8 -7
  38. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  39. data/lib/wpscan/version.rb +1 -1
  40. data/lib/wpscan/vulnerability.rb +4 -3
  41. metadata +9 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac618e64d1e183b142cf2cbe0cea1a3f8835a51a32ff88af53b57dd0683df623
4
- data.tar.gz: f6ae91504098cf000f875768c0dcb18989df9007a81cea9802ea44d2b5080675
3
+ metadata.gz: 6874e488eedc61ce59fd7f285a5a0f92037dc26b2c61d3c99071108888836753
4
+ data.tar.gz: 2ebdafbc40f8289567ed0cfea3a75f2e32efa2af2da843a60232da2d14fbdf5c
5
5
  SHA512:
6
- metadata.gz: 522daf1656b75c7771713fcdec05c3f412493307fe5f2870b419d5c177dcaebbbbf80abf2c3badd4398af69967bcec33f2ca42830eed0d9c1847b967a07421fb
7
- data.tar.gz: 91f246f20360685c716cd406041d254976c95a1723a9f967a0b90fce6804450e1dcbca8b419d44fd612fa9f5128f391ffda5e2f19d4547d0bf6af5b7a2b17967
6
+ metadata.gz: 3d811df1f9d9bce85806cd384098d60ba99660979b9c4d9bdec256fac0bcbb6da33329303e1b9c577e61946b7e639a56bd3b6c520e9c4fa911bd60724d004777
7
+ data.tar.gz: 8bba285823138ffbd3f5050c4eab699938076ff7e1805d07ccf62e8d6cf3dd2acb64be3931e1ba98201ed5494f13abfaece130b2b2d8a29d447d6fc88f1f2125
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.
@@ -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
@@ -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
 
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
 
@@ -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
 
@@ -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
@@ -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
@@ -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.11'
5
+ VERSION = '3.8.4'
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.11
4
+ version: 3.8.4
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-13 00:00:00.000000000 Z
11
+ date: 2020-07-20 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.6
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.6
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
  - - ">="