wpscan 3.7.10 → 3.8.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -16
- data/app/controllers/enumeration/cli_options.rb +2 -3
- data/app/controllers/password_attack.rb +21 -21
- 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 +2 -2
- 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/wp_json_api.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/interesting_finding.rb +1 -1
- 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: 658d14ca9901acb16c2da34ed977e5ad75296c4c8aba3e66ba897ffb5dd55f86
|
4
|
+
data.tar.gz: 788c5a27c5bf10dba5b35e977c3efdfafca97944485cf605f03e34faf65ae68b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: be04200a71b1d710d47e0a8be2b914731803a48c94a8da7bfa1ad777de8f1e103fe9ac785589d70ca905c389c369bd4567118bae6d3585f88f7bf12c52c053af
|
7
|
+
data.tar.gz: 42f54d70d4aea2433c5b619abb5c516e731f7cfefafe9d4e0d8876956eb8470012e211b826c4790b1c7144ff091431906597b2e2a7c1cd63f25b07ab87013caf
|
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.
|
@@ -130,7 +124,7 @@ cli_options:
|
|
130
124
|
api_token: YOUR_API_TOKEN
|
131
125
|
```
|
132
126
|
|
133
|
-
## Load
|
127
|
+
## Load API Token From ENV (since v3.7.10)
|
134
128
|
|
135
129
|
The API Token will be automatically loaded from the ENV variable `WPSCAN_API_TOKEN` if present. If the `--api-token` CLI option is also provided, the value from the CLI will be used.
|
136
130
|
|
@@ -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
|
@@ -82,7 +89,7 @@ module WPScan
|
|
82
89
|
if xmlrpc&.enabled? &&
|
83
90
|
xmlrpc.available_methods.include?('wp.getUsersBlogs') &&
|
84
91
|
xmlrpc.method_call('wp.getUsersBlogs', [SecureRandom.hex[0, 6], SecureRandom.hex[0, 4]])
|
85
|
-
.run.body !~ /XML
|
92
|
+
.run.body !~ /XML-RPC services are disabled/
|
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)
|
@@ -12,11 +12,11 @@ module WPScan
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def valid_credentials?(response)
|
15
|
-
response.code == 200 && response.body
|
15
|
+
response.code == 200 && response.body.include?('blogName')
|
16
16
|
end
|
17
17
|
|
18
18
|
def errored_response?(response)
|
19
|
-
response.code != 200 && response.body !~ /
|
19
|
+
response.code != 200 && response.body !~ /Incorrect username or password/i
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -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
|
@@ -21,7 +21,7 @@ module WPScan
|
|
21
21
|
loop do
|
22
22
|
current_page += 1
|
23
23
|
|
24
|
-
res =
|
24
|
+
res = Browser.get(api_url, params: { per_page: MAX_PER_PAGE, page: current_page })
|
25
25
|
|
26
26
|
total_pages ||= res.headers['X-WP-TotalPages'].to_i
|
27
27
|
|
@@ -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
|
@@ -20,7 +20,7 @@ module WPScan
|
|
20
20
|
class DebugLog < InterestingFinding
|
21
21
|
# @ return [ Hash ]
|
22
22
|
def references
|
23
|
-
@references ||= { url: 'https://codex.wordpress.org/Debugging_in_WordPress' }
|
23
|
+
@references ||= { url: ['https://codex.wordpress.org/Debugging_in_WordPress'] }
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
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[/#{Regexp.escape(tag)}:[\t ]*([^\r\n
|
104
|
+
value = body[/#{Regexp.escape(tag)}:[\t ]*([^\r\n*]+)/i, 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.3
|
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-17 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
|
- - ">="
|