wpscan 4.0.0 → 4.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a35937f16511c0e2d5fe332f45489703704f91e2d38d48b46d4f42fb5fe153f
4
- data.tar.gz: 2ced1bd5e24ce8d891d298d383c9a79b03d6d05fd614de60da7cf46808138a62
3
+ metadata.gz: b6e5b02dd5de5e0624c1dca1b8bb3bad6f16ff008fe054957e80c3ce33807a70
4
+ data.tar.gz: fb7cec3911f60cabbf70ac8e34672e4a41fe68a64bd76e9d7b55f0f5e901d012
5
5
  SHA512:
6
- metadata.gz: 2ebbec3ada4d857b9be598636dbeaa65d524f3be1733204920f8fe23d03bf8ef6e3a9e446a3dbbfabda70d6da41562c7bb28115d5133497b002ad69ec6464731
7
- data.tar.gz: d909b0d2ee28f28fb698c07301a4d42c6118cc6e4d7245b05e0ba329bc90a8a52f2af189c10dc437a7973db5ff4afd46604e5ce0e3889bdb6efdaaeccc879ed3
6
+ metadata.gz: 724a6be1ce5d754a9891781e7101bf5b504c056107d1a7dc4cb0bb107a03044ccfff73dd648f32e14bcc0d8299b8b7872e2cbdd8f813d928a2abd64402ed28f0
7
+ data.tar.gz: 9c31899c0110f312f496a934f585e661103a364cf535da47a49d9002594eb047b942715a299cf275fdc85c1143a725cb5ee9d3f3d86dcbe29a3e53f7a400c432
@@ -21,7 +21,7 @@ module WPScan
21
21
 
22
22
  slug = Regexp.last_match[1]&.strip
23
23
 
24
- found << slug unless slug&.empty?
24
+ found << slug unless slug && slug.empty?
25
25
  end
26
26
 
27
27
  uniq ? found.uniq.sort : found.sort
@@ -42,7 +42,7 @@ module WPScan
42
42
  'Timthumb <= 1.32 Remote Code Execution',
43
43
  references: { exploitdb: ['17602'] },
44
44
  type: 'RCE',
45
- fixed_in: '1.33'
45
+ affected_versions: [{ fixed_in: '1.33' }]
46
46
  )
47
47
  end
48
48
 
@@ -55,7 +55,7 @@ module WPScan
55
55
  cve: '2014-4663'
56
56
  },
57
57
  type: 'RCE',
58
- fixed_in: '2.8.14'
58
+ affected_versions: [{ fixed_in: '2.8.14' }]
59
59
  )
60
60
  end
61
61
 
@@ -34,7 +34,7 @@ module WPScan
34
34
 
35
35
  # @param [ Version, String ] other
36
36
  def <=>(other)
37
- other = self.class.new(other) unless other.is_a?(self.class) # handle potential '.1' version
37
+ other = Version.new(other) unless other.is_a?(Version) # handle potential '.1' version
38
38
 
39
39
  Gem::Version.new(number) <=> Gem::Version.new(other.number)
40
40
  rescue ArgumentError
@@ -41,23 +41,25 @@ module WPScan
41
41
 
42
42
  Array(db_data['vulnerabilities']).each do |json_vuln|
43
43
  vulnerability = Vulnerability.load_from_json(json_vuln)
44
+ vulnerability.detected_version = version || nil
44
45
  @vulnerabilities << vulnerability if vulnerable_to?(vulnerability)
45
46
  end
46
47
 
47
48
  @vulnerabilities
48
49
  end
49
50
 
50
- # Checks if the wp_item is vulnerable to a specific vulnerability
51
+ # Checks if the wp_item is vulnerable to a specific vulnerability.
52
+ # A vuln with multiple backported ranges matches when any single range
53
+ # covers the detected version.
51
54
  #
52
55
  # @param [ Vulnerability ] vuln Vulnerability to check the item against
53
56
  #
54
57
  # @return [ Boolean ]
55
58
  def vulnerable_to?(vuln)
56
- return false if version && vuln&.introduced_in && version < vuln.introduced_in
59
+ return true unless version
60
+ return true if vuln.nil?
57
61
 
58
- return true unless version && vuln&.fixed_in && !vuln.fixed_in.empty?
59
-
60
- version < vuln.fixed_in
62
+ vuln.affects?(version)
61
63
  end
62
64
 
63
65
  # @return [ String ]
@@ -58,7 +58,9 @@ module WPScan
58
58
  @vulnerabilities = []
59
59
 
60
60
  Array(db_data['vulnerabilities']).each do |json_vuln|
61
- @vulnerabilities << Vulnerability.load_from_json(json_vuln)
61
+ vulnerability = Vulnerability.load_from_json(json_vuln)
62
+ vulnerability.detected_version = self
63
+ @vulnerabilities << vulnerability
62
64
  end
63
65
 
64
66
  @vulnerabilities
@@ -12,7 +12,7 @@ module WPScan
12
12
 
13
13
  # @return [ Addressable::URI ]
14
14
  def self.uri
15
- @uri ||= Addressable::URI.parse('https://wpscan.com/api/v3/')
15
+ @uri ||= Addressable::URI.parse('https://wpscan.com/api/v4/')
16
16
  end
17
17
 
18
18
  # @param [ String ] path
@@ -21,7 +21,11 @@ module WPScan
21
21
  # @return [ Hash ]
22
22
  def self.get(path, params = {})
23
23
  return {} unless token
24
- return {} if path.end_with?('/latest') # Remove this when api/v4 is up
24
+ # Guard against a slug or WordPress version equal to the literal
25
+ # string "latest": such a value would map to the
26
+ # /{plugins,themes,wordpresses,all}/latest list endpoints, which
27
+ # return a payload the per-entity parser below cannot handle.
28
+ return {} if path.end_with?('/latest')
25
29
 
26
30
  # Typhoeus.get is used rather than Browser.get to avoid merging irrelevant params from the CLI
27
31
  res = Typhoeus.get(uri.join(path), default_request_params.merge(params))
data/lib/wpscan/scan.rb CHANGED
@@ -97,10 +97,9 @@ module WPScan
97
97
  # depending on the findings / errors
98
98
  # :nocov:
99
99
  def exit_hook
100
- # Avoid hooking the exit when rspec is running, otherwise it will always return 0
101
- # and Travis won't detect failed builds. Couldn't find a better way, even though
102
- # some people managed to https://github.com/rspec/rspec-core/pull/410
103
- return if defined?(RSpec)
100
+ # Avoid registering this hook when the RSpec suite is running, otherwise it
101
+ # can override RSpec's exit status and hide failed builds.
102
+ return if rspec_running?
104
103
 
105
104
  at_exit do
106
105
  exit(run_error_exit_code) if run_error
@@ -112,6 +111,10 @@ module WPScan
112
111
  end
113
112
  # :nocov:
114
113
 
114
+ def rspec_running?
115
+ defined?(RSpec)
116
+ end
117
+
115
118
  # @return [ Integer ] The exit code related to the run_error
116
119
  def run_error_exit_code
117
120
  return WPScan::ExitCode::CLI_OPTION_ERROR if run_error.is_a?(OptParseValidator::Error) ||
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Version
4
4
  module WPScan
5
- VERSION = '4.0.0'
5
+ VERSION = '4.0.1'
6
6
  end
@@ -5,7 +5,12 @@ module WPScan
5
5
  class Vulnerability
6
6
  include References
7
7
 
8
- attr_reader :title, :type, :fixed_in, :introduced_in, :cvss, :poc, :uuid
8
+ attr_reader :title, :type, :affected_versions, :cvss, :poc, :uuid
9
+
10
+ # Version of the scanned item (plugin/theme/WordPress), used to resolve
11
+ # which backported range to surface as the scalar fixed_in / introduced_in.
12
+ # Left nil/false when the version could not be detected.
13
+ attr_accessor :detected_version
9
14
 
10
15
  # @param [ String ] title
11
16
  # @param [ Hash ] references
@@ -17,21 +22,21 @@ module WPScan
17
22
  # @option references [ Array<String>, String ] :metasploit The related metasploit module(s)
18
23
  # @option references [ Array<String> ] :youtube
19
24
  # @param [ String ] type
20
- # @param [ String ] fixed_in
21
- # @param [ String ] introduced_in
25
+ # @param [ Array<Hash> ] affected_versions One entry per backported fix range,
26
+ # each with :fixed_in and :introduced_in keys (either may be nil).
22
27
  # @param [ HashSymbol ] cvss
23
28
  # @option cvss [ String ] :score
24
29
  # @option cvss [ String ] :vector
25
30
  # rubocop:disable Metrics/ParameterLists
26
- def initialize(title, references: {}, type: nil, fixed_in: nil, introduced_in: nil, cvss: nil, poc: nil, uuid: nil)
31
+ def initialize(title, references: {}, type: nil, affected_versions: nil, cvss: nil, poc: nil, uuid: nil)
27
32
  # rubocop:enable Metrics/ParameterLists
28
- @title = title
29
- @type = type
30
- @fixed_in = fixed_in
31
- @introduced_in = introduced_in
32
- @cvss = { score: cvss[:score], vector: cvss[:vector] } if cvss
33
- @poc = poc
34
- @uuid = uuid
33
+ @title = title
34
+ @type = type
35
+ @affected_versions = normalize_affected_versions(affected_versions)
36
+ @cvss = { score: cvss[:score], vector: cvss[:vector] } if cvss
37
+ @poc = poc
38
+ @uuid = uuid
39
+ @detected_version = nil
35
40
 
36
41
  self.references = references
37
42
  end
@@ -51,14 +56,78 @@ module WPScan
51
56
  json_data['title'],
52
57
  references: references,
53
58
  type: json_data['vuln_type'],
54
- fixed_in: json_data['fixed_in'],
55
- introduced_in: json_data['introduced_in'],
59
+ affected_versions: json_data['affected_versions'],
56
60
  cvss: json_data['cvss']&.symbolize_keys,
57
61
  poc: json_data['poc'],
58
62
  uuid: json_data['id'].to_s # The 'id' field IS the UUID in WPScan API
59
63
  )
60
64
  end
61
65
 
66
+ # Whether this vulnerability covers a given version. True when no
67
+ # affected_versions are declared (advisory with no fix metadata) or when at
68
+ # least one backported range covers the version.
69
+ #
70
+ # @param [ #< ] version
71
+ #
72
+ # @return [ Boolean ]
73
+ def affects?(version)
74
+ return true if affected_versions.empty?
75
+
76
+ affected_versions.any? { |range| self.class.range_covers?(range, version) }
77
+ end
78
+
79
+ # Backward-compatible scalar: the fixed_in of the range relevant to the
80
+ # detected version (the branch the scanned site is actually on). nil when
81
+ # there is no fix metadata.
82
+ #
83
+ # @return [ String, nil ]
84
+ def fixed_in
85
+ relevant_range && relevant_range[:fixed_in]
86
+ end
87
+
88
+ # Backward-compatible scalar counterpart of {#fixed_in}.
89
+ #
90
+ # @return [ String, nil ]
91
+ def introduced_in
92
+ relevant_range && relevant_range[:introduced_in]
93
+ end
94
+
95
+ # The single affected-version range whose fixed_in / introduced_in we
96
+ # surface in the output. Picks the range covering the detected version so a
97
+ # site on an older but still-supported branch is matched against the right
98
+ # fix. Falls back to the highest (most recent) range when the version is
99
+ # unknown or no range covers it, mirroring the pre-v4 single-value output.
100
+ #
101
+ # @return [ Hash{Symbol => String, nil}, nil ]
102
+ def relevant_range
103
+ return nil if affected_versions.empty?
104
+
105
+ if detected_version
106
+ covering = affected_versions.find { |range| self.class.range_covers?(range, detected_version) }
107
+ return covering if covering
108
+ end
109
+
110
+ highest_range
111
+ end
112
+
113
+ # Whether a single range covers the version. A range matches when
114
+ # introduced_in is missing or <= version AND fixed_in is missing or >
115
+ # version.
116
+ #
117
+ # @param [ Hash{Symbol => String, nil} ] range
118
+ # @param [ #< ] version
119
+ #
120
+ # @return [ Boolean ]
121
+ def self.range_covers?(range, version)
122
+ introduced = range[:introduced_in]
123
+ fixed = range[:fixed_in]
124
+
125
+ return false if introduced && !introduced.to_s.empty? && version < introduced
126
+ return true if fixed.nil? || fixed.to_s.empty?
127
+
128
+ version < fixed
129
+ end
130
+
62
131
  # @param [ Vulnerability ] other
63
132
  #
64
133
  # @return [ Boolean ]
@@ -66,10 +135,47 @@ module WPScan
66
135
  title == other.title &&
67
136
  type == other.type &&
68
137
  references == other.references &&
69
- fixed_in == other.fixed_in &&
138
+ affected_versions == other.affected_versions &&
70
139
  cvss == other.cvss &&
71
140
  poc == other.poc &&
72
141
  uuid == other.uuid
73
142
  end
143
+
144
+ private
145
+
146
+ # The range with the greatest fixed_in. An unpatched range (no fixed_in)
147
+ # outranks every patched one. Used as the fallback for {#relevant_range}.
148
+ #
149
+ # @return [ Hash{Symbol => String, nil} ]
150
+ def highest_range
151
+ affected_versions.max_by { |range| version_key(range[:fixed_in]) }
152
+ end
153
+
154
+ # Comparable sort key for a fixed_in value. Returns a [tier, Gem::Version]
155
+ # tuple so a missing fixed_in (unpatched) always sorts highest, and patched
156
+ # ranges sort by their numeric version.
157
+ #
158
+ # @return [ Array ]
159
+ def version_key(fixed_in)
160
+ return [1, Gem::Version.new('0')] if fixed_in.nil? || fixed_in.to_s.empty?
161
+
162
+ [0, Gem::Version.new(fixed_in.to_s[/\A[\d.]+/] || '0')]
163
+ rescue ArgumentError
164
+ [0, Gem::Version.new('0')]
165
+ end
166
+
167
+ # Coerces the v4 affected_versions payload into an array of symbolised range
168
+ # hashes. Accepts either an array of hashes (with string or symbol keys) or
169
+ # nil; anything else is dropped so a malformed entry can't break parsing.
170
+ #
171
+ # @return [ Array<Hash{Symbol => String, nil}> ]
172
+ def normalize_affected_versions(value)
173
+ Array(value).filter_map do |range|
174
+ next unless range.is_a?(Hash)
175
+
176
+ range = range.transform_keys(&:to_sym)
177
+ { fixed_in: range[:fixed_in], introduced_in: range[:introduced_in] }
178
+ end
179
+ end
74
180
  end
75
181
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wpscan
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.0
4
+ version: 4.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - WPScanTeam