wpscan 3.8.2 → 3.8.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/password_attack.rb +22 -22
- data/app/finders/interesting_findings/duplicator_installer_log.rb +1 -1
- data/app/finders/interesting_findings/mu_plugins.rb +1 -1
- data/app/finders/interesting_findings/multisite.rb +2 -2
- data/app/finders/main_theme/css_style_in_homepage.rb +2 -2
- data/app/finders/main_theme/urls_in_homepage.rb +1 -1
- data/app/finders/passwords/wp_login.rb +1 -1
- data/app/finders/passwords/xml_rpc.rb +1 -1
- data/app/finders/passwords/xml_rpc_multicall.rb +35 -9
- data/app/finders/plugin_version/readme.rb +2 -2
- data/app/finders/theme_version/style.rb +1 -1
- data/app/finders/users.rb +3 -1
- data/app/finders/users/author_sitemap.rb +36 -0
- data/app/finders/users/login_error_messages.rb +1 -1
- data/app/finders/users/rss_generator.rb +1 -1
- data/app/finders/users/yoast_seo_author_sitemap.rb +1 -21
- data/app/finders/wp_items/urls_in_page.rb +5 -5
- data/app/models/interesting_finding.rb +22 -3
- data/app/models/plugin.rb +1 -1
- data/app/models/theme.rb +2 -2
- data/app/models/wp_item.rb +1 -1
- data/app/models/wp_version.rb +1 -1
- data/lib/wpscan/db/dynamic_finders/base.rb +1 -1
- data/lib/wpscan/db/dynamic_finders/plugin.rb +1 -1
- data/lib/wpscan/db/dynamic_finders/wordpress.rb +1 -1
- data/lib/wpscan/errors/wordpress.rb +6 -0
- data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -3
- data/lib/wpscan/finders/dynamic_finder/version/config_parser.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/query_parameter.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/xpath.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/wp_version.rb +1 -1
- data/lib/wpscan/helper.rb +1 -1
- data/lib/wpscan/target.rb +4 -4
- data/lib/wpscan/target/platform/wordpress.rb +8 -7
- data/lib/wpscan/target/platform/wordpress/custom_directories.rb +3 -3
- data/lib/wpscan/version.rb +1 -1
- metadata +11 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfd5e9c7cc41b0b10e4fe74b8b0d876f09cbb4916d94ec3bb23124dac9c5ad5b
|
4
|
+
data.tar.gz: f0aa65190aa0fd9fa3b9b4fbc0296824004d0a7766820a7024b687d3b2b73825
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e8e747c12daf21501a65252709bd305d4d8aa148fe14fca66d3af5aadb0c2188e554f2fa7f34cab7936e0428c9c1c43437ed2b6fb140ad91c9914a8f5c902972
|
7
|
+
data.tar.gz: 44eef7565acae4f7db9eb7ac30e6e8bff302d7ad5c2d46c019449d8308266cd8ffbec4eb74ac7b69653426971c9f4d5ce0efa5cd9c4da4930212f7fb2a5cbcf5
|
@@ -23,27 +23,32 @@ module WPScan
|
|
23
23
|
]
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
110
|
+
elsif target.login_url
|
104
111
|
Finders::Passwords::WpLogin.new(target)
|
112
|
+
else
|
113
|
+
raise Error::NoLoginInterfaceDetected
|
105
114
|
end
|
106
115
|
end
|
107
116
|
|
@@ -113,15 +122,6 @@ module WPScan
|
|
113
122
|
acc << Model::User.new(elem.chomp)
|
114
123
|
end
|
115
124
|
end
|
116
|
-
|
117
|
-
# @param [ String ] wordlist_path
|
118
|
-
#
|
119
|
-
# @return [ Array<String> ]
|
120
|
-
def passwords(wordlist_path)
|
121
|
-
@passwords ||= File.open(wordlist_path).reduce([]) do |acc, elem|
|
122
|
-
acc << elem.chomp
|
123
|
-
end
|
124
|
-
end
|
125
125
|
end
|
126
126
|
end
|
127
127
|
end
|
@@ -9,7 +9,7 @@ module WPScan
|
|
9
9
|
def aggressive(_opts = {})
|
10
10
|
path = 'installer-log.txt'
|
11
11
|
|
12
|
-
return unless /DUPLICATOR INSTALL-LOG
|
12
|
+
return unless /DUPLICATOR(-|\s)?(PRO|LITE)?:? INSTALL-LOG/i.match?(target.head_and_get(path).body)
|
13
13
|
|
14
14
|
Model::DuplicatorInstallerLog.new(target.url(path), confidence: 100, found_by: DIRECT_ACCESS)
|
15
15
|
end
|
@@ -7,7 +7,7 @@ module WPScan
|
|
7
7
|
class MuPlugins < CMSScanner::Finders::Finder
|
8
8
|
# @return [ InterestingFinding ]
|
9
9
|
def passive(_opts = {})
|
10
|
-
pattern = %r{#{target.content_dir}/mu
|
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
|
16
|
-
return unless res.code == 200 || res.code == 302 && location
|
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/([
|
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[^"'
|
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
|
-
|
16
|
+
Array(response.headers['Set-Cookie'])&.any? { |cookie| cookie =~ /wordpress_logged_in_/i }
|
17
17
|
end
|
18
18
|
|
19
19
|
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 [
|
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,
|
37
|
-
|
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: (
|
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
|
46
|
-
current_passwords
|
47
|
-
|
71
|
+
current_users = users.select { |user| user.password.nil? }
|
72
|
+
current_passwords = passwords_from_wordlist(wordlist, current_passwords_size)
|
73
|
+
checked_passwords += current_passwords_size
|
48
74
|
|
49
75
|
break if current_users.empty? || current_passwords.nil? || current_passwords.empty?
|
50
76
|
|
@@ -75,9 +101,9 @@ module WPScan
|
|
75
101
|
progress_bar.stop
|
76
102
|
break
|
77
103
|
end
|
78
|
-
|
104
|
+
|
79
105
|
begin
|
80
|
-
progress_bar.total = progress_bar.progress + ((
|
106
|
+
progress_bar.total = progress_bar.progress + ((wordlist_size - checked_passwords) / current_passwords_size.round(1)).ceil
|
81
107
|
rescue ProgressBar::InvalidProgressError
|
82
108
|
end
|
83
109
|
end
|
@@ -48,7 +48,7 @@ module WPScan
|
|
48
48
|
#
|
49
49
|
# @return [ String, nil ] The version number detected from the stable tag
|
50
50
|
def from_stable_tag(body)
|
51
|
-
return unless body =~ /\b(?:stable tag|version):\s*(?!trunk)([0-9a-z
|
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{
|
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
|
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],
|
data/app/finders/users.rb
CHANGED
@@ -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/
|
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
|
@@ -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 <
|
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
|
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
|
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{["'
|
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}
|
69
|
+
item_dir = %r{(?:#{url}|\\?/#{item_dir.gsub('/', '\\\\\?\/')}\\?/)}i
|
70
70
|
|
71
|
-
type == 'plugins' ? item_dir : %r{#{item_dir}#{type}
|
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
|
data/app/models/plugin.rb
CHANGED
@@ -38,7 +38,7 @@ module WPScan
|
|
38
38
|
|
39
39
|
# @return [ Array<String> ]
|
40
40
|
def potential_readme_filenames
|
41
|
-
@potential_readme_filenames ||=
|
41
|
+
@potential_readme_filenames ||= Array((DB::DynamicFinders::Plugin.df_data.dig(slug, 'Readme', 'path') || super))
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
data/app/models/theme.rb
CHANGED
@@ -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\(["']?([^"'
|
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[
|
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
|
data/app/models/wp_item.rb
CHANGED
@@ -39,7 +39,7 @@ module WPScan
|
|
39
39
|
|
40
40
|
@vulnerabilities = []
|
41
41
|
|
42
|
-
|
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
|
data/app/models/wp_version.rb
CHANGED
@@ -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
|
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
|
27
|
+
def self.finder_configs(finder_class, aggressive: false)
|
28
28
|
configs = {}
|
29
29
|
|
30
30
|
return configs unless allowed_classes.include?(finder_class)
|
@@ -29,5 +29,11 @@ module WPScan
|
|
29
29
|
' use the --scope option or make sure the --url value given is the correct one'
|
30
30
|
end
|
31
31
|
end
|
32
|
+
|
33
|
+
class NoLoginInterfaceDetected < Standard
|
34
|
+
def to_s
|
35
|
+
'Could not find a login interface to perform the password attack against'
|
36
|
+
end
|
37
|
+
end
|
32
38
|
end
|
33
39
|
end
|
@@ -56,9 +56,7 @@ module WPScan
|
|
56
56
|
|
57
57
|
homepage_result = find(target.homepage_res, opts)
|
58
58
|
|
59
|
-
|
60
|
-
return homepage_result unless homepage_result.is_a?(Array) && homepage_result.empty?
|
61
|
-
end
|
59
|
+
return homepage_result unless homepage_result.nil? || homepage_result&.is_a?(Array) && homepage_result&.empty?
|
62
60
|
|
63
61
|
find(target.error_404_res, opts)
|
64
62
|
end
|
@@ -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)
|
12
|
+
XPATH: nil, FILES: nil, PATTERN: /(?:v|ver|version)=(?<v>\d+\.[.\d]+)/i, CONFIDENCE_PER_OCCURENCE: 10
|
13
13
|
)
|
14
14
|
end
|
15
15
|
|
data/lib/wpscan/helper.rb
CHANGED
@@ -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(
|
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
|
data/lib/wpscan/target.rb
CHANGED
@@ -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
|
-
|
22
|
+
Array(e).each { |ae| return true if ae && ae.vulnerable? } # rubocop:disable Style/SafeNavigation
|
23
23
|
end
|
24
24
|
|
25
|
-
return true unless
|
26
|
-
return true unless
|
25
|
+
return true unless Array(@config_backups).empty?
|
26
|
+
return true unless Array(@db_exports).empty?
|
27
27
|
|
28
|
-
|
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
|
15
|
-
WP_JSON_OEMBED_PATTERN = %r{/wp
|
16
|
-
WP_ADMIN_AJAX_PATTERN = %r{\\?/wp
|
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
|
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
|
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
|
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
|
127
|
+
if %r{wp-content/plugins}i.match?(path)
|
128
128
|
path = +path.gsub('wp-content/plugins', plugins_dir)
|
129
|
-
elsif /wp
|
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}"
|
data/lib/wpscan/version.rb
CHANGED
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.
|
4
|
+
version: 3.8.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WPScanTeam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-09-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cms_scanner
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
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.
|
26
|
+
version: 0.12.1
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: bundler
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,42 +100,42 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 0.90.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 0.90.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: rubocop-performance
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 1.
|
117
|
+
version: 1.8.0
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 1.
|
124
|
+
version: 1.8.0
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: simplecov
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
128
128
|
requirements:
|
129
129
|
- - "~>"
|
130
130
|
- !ruby/object:Gem::Version
|
131
|
-
version: 0.
|
131
|
+
version: 0.19.0
|
132
132
|
type: :development
|
133
133
|
prerelease: false
|
134
134
|
version_requirements: !ruby/object:Gem::Requirement
|
135
135
|
requirements:
|
136
136
|
- - "~>"
|
137
137
|
- !ruby/object:Gem::Version
|
138
|
-
version: 0.
|
138
|
+
version: 0.19.0
|
139
139
|
- !ruby/object:Gem::Dependency
|
140
140
|
name: simplecov-lcov
|
141
141
|
requirement: !ruby/object:Gem::Requirement
|
@@ -258,6 +258,7 @@ files:
|
|
258
258
|
- app/finders/users.rb
|
259
259
|
- app/finders/users/author_id_brute_forcing.rb
|
260
260
|
- app/finders/users/author_posts.rb
|
261
|
+
- app/finders/users/author_sitemap.rb
|
261
262
|
- app/finders/users/login_error_messages.rb
|
262
263
|
- app/finders/users/oembed_api.rb
|
263
264
|
- app/finders/users/rss_generator.rb
|