wpscan 3.8.1 → 3.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -13
- data/app/controllers/password_attack.rb +22 -22
- data/app/finders/db_exports/known_locations.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 +45 -12
- 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/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 +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f98f734f7109df65f502a120586451cb9cdcd1f741a03db2d664e5e2b0ebde05
|
4
|
+
data.tar.gz: da340ec87c3ac0603b0ffe9fb518067c9cea0596d59bedc15e0dabd7f7e7cfb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6d45b4fbc1a60f0f804b4fe59815da1ca324693c1546cd1cd91b75fea0aab363fef93ab240ef1009417e8cb7b5230642b6ff464acd90a0bbb917f0ecce915171
|
7
|
+
data.tar.gz: 4e0c504291a53f475f834f6b36a14e6634fca79f5cfc03c2375815a509c154047c8da98e25da026d30edd8c4e3bbb2ded4a29d4a678d0e2fe5d10646a6f09f9b
|
data/README.md
CHANGED
@@ -31,7 +31,11 @@
|
|
31
31
|
- RubyGems - Recommended: latest
|
32
32
|
- Nokogiri might require packages to be installed via your package manager depending on your OS, see https://nokogiri.org/tutorials/installing_nokogiri.html
|
33
33
|
|
34
|
-
###
|
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
|
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
|
@@ -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
|
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
|
|
@@ -76,16 +102,19 @@ module WPScan
|
|
76
102
|
break
|
77
103
|
end
|
78
104
|
|
79
|
-
|
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:
|
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
|
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
|
-
|
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')
|
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
|
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
|
@@ -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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- WPScanTeam
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cms_scanner
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 0.
|
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,28 +100,28 @@ dependencies:
|
|
100
100
|
requirements:
|
101
101
|
- - "~>"
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version: 0.
|
103
|
+
version: 0.89.0
|
104
104
|
type: :development
|
105
105
|
prerelease: false
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
107
107
|
requirements:
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: 0.
|
110
|
+
version: 0.89.0
|
111
111
|
- !ruby/object:Gem::Dependency
|
112
112
|
name: rubocop-performance
|
113
113
|
requirement: !ruby/object:Gem::Requirement
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 1.
|
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.
|
124
|
+
version: 1.7.0
|
125
125
|
- !ruby/object:Gem::Dependency
|
126
126
|
name: simplecov
|
127
127
|
requirement: !ruby/object:Gem::Requirement
|
@@ -258,6 +258,7 @@ files:
|
|
258
258
|
- app/finders/users.rb
|
259
259
|
- app/finders/users/author_id_brute_forcing.rb
|
260
260
|
- app/finders/users/author_posts.rb
|
261
|
+
- app/finders/users/author_sitemap.rb
|
261
262
|
- app/finders/users/login_error_messages.rb
|
262
263
|
- app/finders/users/oembed_api.rb
|
263
264
|
- app/finders/users/rss_generator.rb
|