wpscan 3.7.11 → 3.8.4
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 +9 -15
- data/app/controllers/enumeration/cli_options.rb +2 -3
- 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/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 +5 -7
- data/app/finders/theme_version/style.rb +1 -1
- data/app/finders/users.rb +1 -1
- 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 -1
- data/app/finders/wp_items/urls_in_page.rb +3 -3
- data/app/models/plugin.rb +1 -1
- data/app/models/theme.rb +2 -2
- data/app/models/timthumb.rb +6 -6
- data/app/models/wp_item.rb +1 -1
- data/app/models/wp_version.rb +1 -1
- data/app/views/cli/password_attack/users.erb +1 -1
- data/app/views/cli/vulnerability.erb +3 -0
- data/app/views/json/finding.erb +3 -0
- data/lib/wpscan/db/updater.rb +12 -14
- data/lib/wpscan/errors/wordpress.rb +6 -0
- data/lib/wpscan/finders/dynamic_finder/finder.rb +1 -1
- data/lib/wpscan/finders/dynamic_finder/version/config_parser.rb +5 -7
- 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/references.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
- data/lib/wpscan/vulnerability.rb +4 -3
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6874e488eedc61ce59fd7f285a5a0f92037dc26b2c61d3c99071108888836753
|
|
4
|
+
data.tar.gz: 2ebdafbc40f8289567ed0cfea3a75f2e32efa2af2da843a60232da2d14fbdf5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
###
|
|
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
|
|
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
|
|
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
|
|
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
|
|
|
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
|
|
|
@@ -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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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,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
|
|
9
|
+
require_relative 'users/yoast_seo_author_sitemap'
|
|
10
10
|
|
|
11
11
|
module WPScan
|
|
12
12
|
module Finders
|
|
@@ -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/([
|
|
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{["'
|
|
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
|
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/timthumb.rb
CHANGED
|
@@ -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
|
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<% if @users.empty? -%>
|
|
3
3
|
<%= notice_icon %> No Valid Passwords Found.
|
|
4
4
|
<% else -%>
|
|
5
|
-
<%=
|
|
5
|
+
<%= critical_icon %> Valid Combinations Found:
|
|
6
6
|
<% @users.each do |user| -%>
|
|
7
7
|
| Username: <%= user.username %>, Password: <%= user.password %>
|
|
8
8
|
<% end -%>
|
data/app/views/json/finding.erb
CHANGED
|
@@ -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 -%>
|
data/lib/wpscan/db/updater.rb
CHANGED
|
@@ -139,24 +139,22 @@ module WPScan
|
|
|
139
139
|
updated = []
|
|
140
140
|
|
|
141
141
|
FILES.each do |filename|
|
|
142
|
-
|
|
143
|
-
db_checksum = remote_file_checksum(filename)
|
|
142
|
+
db_checksum = remote_file_checksum(filename)
|
|
144
143
|
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
147
|
+
create_backup(filename)
|
|
148
|
+
dl_checksum = download(filename)
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
raise Error::ChecksumsMismatch, filename unless dl_checksum == db_checksum
|
|
152
151
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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+\.[
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
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/references.rb
CHANGED
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
data/lib/wpscan/vulnerability.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -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.
|
|
391
|
+
version: '2.5'
|
|
392
392
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
393
393
|
requirements:
|
|
394
394
|
- - ">="
|