wpscan 3.8.0 → 3.8.5

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 +9 -15
  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/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 +45 -12
  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 +1 -1
  14. data/app/finders/users/login_error_messages.rb +1 -1
  15. data/app/finders/users/rss_generator.rb +1 -1
  16. data/app/finders/users/yoast_seo_author_sitemap.rb +1 -1
  17. data/app/finders/wp_items/urls_in_page.rb +3 -3
  18. data/app/models/interesting_finding.rb +22 -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/vulnerability.erb +3 -0
  25. data/app/views/json/finding.erb +3 -0
  26. data/lib/wpscan/errors/wordpress.rb +6 -0
  27. data/lib/wpscan/finders/dynamic_finder/version/config_parser.rb +1 -1
  28. data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +1 -1
  29. data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
  30. data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
  31. data/lib/wpscan/helper.rb +1 -1
  32. data/lib/wpscan/references.rb +1 -1
  33. data/lib/wpscan/target.rb +4 -4
  34. data/lib/wpscan/target/platform/wordpress.rb +8 -7
  35. data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
  36. data/lib/wpscan/version.rb +1 -1
  37. data/lib/wpscan/vulnerability.rb +4 -3
  38. metadata +8 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1a64c9f7a8530afcf1845d98ebdd35198ca21b8dc785f8adc4c20b08f4d4671
4
- data.tar.gz: 2fd12843b4b3b40c48d0e3020505e78fed646e279d8222d17b245072b0ffd258
3
+ metadata.gz: 374d883728b24244fefce17ac5dc35d2fb8ae1d34b161e46cfbe4019d6bcb93e
4
+ data.tar.gz: e38d72546d42547c7bfe79d1883db8e7d12dc04ccfc181c572c8b395661d6b65
5
5
  SHA512:
6
- metadata.gz: a9a9b2471e89e6ad9a1b253a9c19eb35524fa6722ba61f65eeebc8624472ee5b25bfadb50f15d066b70888fdf75e88a89b5506a31f987b1e3de14889b92ab93b
7
- data.tar.gz: f4712583e27d1a48598cdd8ebf5e13a7a4c089f57cea2967d0d76810fff22769b3170df149d091449a5714405314c24dfd26c73f111f66c2bae1728eeb733a23
6
+ metadata.gz: 8b44e8757063dfc2e9aef1a0144c07fb8e5bb4f102e7681442fbdf1b86883bc2677030564b193388f174ddb743fc4dd6ed94e7d40827e8fe28883787a7cf2ca0
7
+ data.tar.gz: e8149cb867feb810b996bf3df2ce1df85b7ab8e329c5b8577eb7cf9d516887935f6f6802411adbca4552ba9ac1999dcd1f32ecd4489076123dab75171865c91c
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.
@@ -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
 
@@ -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
@@ -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
@@ -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
 
@@ -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 -%>
@@ -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
@@ -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.8.0'
5
+ VERSION = '3.8.5'
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.8.0
4
+ version: 3.8.5
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-12 00:00:00.000000000 Z
11
+ date: 2020-07-25 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.9.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.9.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.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.81.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