Class: WPScan::Controller::Enumeration

Inherits:
Base
  • Object
show all
Defined in:
app/controllers/enumeration.rb,
app/controllers/enumeration/cli_options.rb,
app/controllers/enumeration/enum_methods.rb

Overview

Enumeration Methods

Constant Summary collapse

WP_AUTH_SUPPRESSED_CHOICES =

Plugin/theme enumeration choices skipped when –wp-auth is supplied (authoritative inventory already fetched by AuthenticatedInventory).

%i[
  vulnerable_plugins all_plugins popular_plugins
  vulnerable_themes all_themes popular_themes
].freeze

Constants included from OptParseValidator

OptParseValidator::VERSION

Instance Method Summary collapse

Methods inherited from Base

#==, #after_scan, #datastore, #formatter, #option_parser, option_parser=, #output, #render, reset, #target, #tmp_directory, #user_interaction?

Instance Method Details

#before_scanObject



10
11
12
13
14
15
16
17
18
19
20
21
# File 'app/controllers/enumeration.rb', line 10

def before_scan
  enum = ParsedCli.enumerate || {}

  # Plugin/theme enumeration is bypassed when --wp-auth is set, so the API token
  # requirement for `vp`/`vt` is irrelevant in that case.
  return if ParsedCli.wp_auth

  # Check if vulnerable plugin/theme enumeration is requested without an API token
  return unless (enum[:vulnerable_plugins] || enum[:vulnerable_themes]) && DB::VulnApi.token.nil?

  raise Error::ApiTokenRequiredForVulnerableEnumeration
end

#cli_backup_folders_optsArray<OptParseValidator::OptBase>

Returns:



137
138
139
140
141
142
143
144
# File 'app/controllers/enumeration/cli_options.rb', line 137

def cli_backup_folders_opts
  [
    OptFilePath.new(
      ['--backup-folders-list FILE-PATH', 'List of backup folders to use'],
      exists: true, default: DB_DIR.join('backup_folders.txt'), advanced: true
    )
  ]
end

#cli_config_backups_optsArray<OptParseValidator::OptBase>

Returns:



117
118
119
120
121
122
123
124
# File 'app/controllers/enumeration/cli_options.rb', line 117

def cli_config_backups_opts
  [
    OptFilePath.new(
      ['--config-backups-list FILE-PATH', 'List of config backups\' filenames to use'],
      exists: true, default: DB_DIR.join('config_backups.txt'), advanced: true
    )
  ]
end

#cli_db_exports_optsArray<OptParseValidator::OptBase>

Returns:



127
128
129
130
131
132
133
134
# File 'app/controllers/enumeration/cli_options.rb', line 127

def cli_db_exports_opts
  [
    OptFilePath.new(
      ['--db-exports-list FILE-PATH', 'List of DB exports\' paths to use'],
      exists: true, default: DB_DIR.join('db_exports.txt'), advanced: true
    )
  ]
end

#cli_enum_choicesArray<OptParseValidator::OptBase>

Returns:



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/controllers/enumeration/cli_options.rb', line 14

def cli_enum_choices
  [
    OptMultiChoices.new(
      ['-e', '--enumerate [OPTS]', 'Enumeration Process',
       'Note: --plugins-list overrides vp/ap/p; --themes-list overrides vt/at/t.'],
      choices: {
        vp: OptBoolean.new(['--vulnerable-plugins']),
        ap: OptBoolean.new(['--all-plugins']),
        p: OptBoolean.new(['--popular-plugins']),
        vt: OptBoolean.new(['--vulnerable-themes']),
        at: OptBoolean.new(['--all-themes']),
        t: OptBoolean.new(['--popular-themes']),
        tt: OptBoolean.new(['--timthumbs']),
        cb: OptBoolean.new(['--config-backups']),
        dbe: OptBoolean.new(['--db-exports']),
        bf: OptBoolean.new(['--backup-folders']),
        u: OptIntegerRange.new(['--users', 'User IDs range. e.g: u1-5'], value_if_empty: '1-10'),
        m: OptIntegerRange.new(['--medias',
                                'Media IDs range. e.g m1-15',
                                'Note: Permalink setting must be set to "Plain" for those to be detected'],
                               value_if_empty: '1-100')
      },
      value_if_empty: 'vp,vt,tt,cb,dbe,bf,u,m',
      incompatible: [%i[vp ap p], %i[vt at t]]
    ),
    OptRegexp.new(
      [
        '--exclude-content-based REGEXP_OR_STRING',
        'Exclude all responses matching the Regexp (case insensitive) during parts of the enumeration.',
        'Both the headers and body are checked. Regexp delimiters are not required.'
      ], options: Regexp::IGNORECASE
    )
  ]
end

#cli_medias_optsArray<OptParseValidator::OptBase>

Returns:



147
148
149
# File 'app/controllers/enumeration/cli_options.rb', line 147

def cli_medias_opts
  []
end

#cli_optionsObject



7
8
9
10
11
# File 'app/controllers/enumeration/cli_options.rb', line 7

def cli_options
  cli_enum_choices + cli_plugins_opts + cli_themes_opts +
    cli_timthumbs_opts + cli_config_backups_opts + cli_db_exports_opts +
    cli_backup_folders_opts + cli_medias_opts + cli_users_opts
end

#cli_plugins_optsArray<OptParseValidator::OptBase>

Returns:



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'app/controllers/enumeration/cli_options.rb', line 50

def cli_plugins_opts
  [
    OptSmartList.new(['--plugins-list LIST', 'List of plugins to enumerate'], advanced: true),
    OptChoice.new(
      ['--plugins-detection MODE',
       'Use the supplied mode to enumerate Plugins.'],
      choices: %w[mixed passive aggressive], normalize: :to_sym
    ),
    OptBoolean.new(
      ['--plugins-version-all',
       'Check all the plugins version locations according to the choosen mode (--detection-mode, ' \
       '--plugins-detection and --plugins-version-detection)'],
      advanced: true
    ),
    OptChoice.new(
      ['--plugins-version-detection MODE',
       'Use the supplied mode to check plugins\' versions.'],
      choices: %w[mixed passive aggressive], normalize: :to_sym
    ),
    OptInteger.new(
      ['--plugins-threshold THRESHOLD',
       'Raise an error when the number of detected plugins via known locations reaches the threshold. ' \
       'Set to 0 to ignore the threshold.'], default: 100, advanced: true
    )
  ]
end

#cli_themes_optsArray<OptParseValidator::OptBase>

Returns:



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'app/controllers/enumeration/cli_options.rb', line 78

def cli_themes_opts
  [
    OptSmartList.new(['--themes-list LIST', 'List of themes to enumerate'], advanced: true),
    OptChoice.new(
      ['--themes-detection MODE',
       'Use the supplied mode to enumerate Themes, instead of the global (--detection-mode) mode.'],
      choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
    ),
    OptBoolean.new(
      ['--themes-version-all',
       'Check all the themes version locations according to the choosen mode (--detection-mode, ' \
       '--themes-detection and --themes-version-detection)'],
      advanced: true
    ),
    OptChoice.new(
      ['--themes-version-detection MODE',
       'Use the supplied mode to check themes versions instead of the --detection-mode ' \
       'or --themes-detection modes.'],
      choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
    ),
    OptInteger.new(
      ['--themes-threshold THRESHOLD',
       'Raise an error when the number of detected themes via known locations reaches the threshold. ' \
       'Set to 0 to ignore the threshold.'], default: 20, advanced: true
    )
  ]
end

#cli_timthumbs_optsArray<OptParseValidator::OptBase>

Returns:



107
108
109
110
111
112
113
114
# File 'app/controllers/enumeration/cli_options.rb', line 107

def cli_timthumbs_opts
  [
    OptFilePath.new(
      ['--timthumbs-list FILE-PATH', 'List of timthumbs\' location to use'],
      exists: true, default: DB_DIR.join('timthumbs-v3.txt'), advanced: true
    )
  ]
end

#cli_users_optsArray<OptParseValidator::OptBase>

Returns:



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'app/controllers/enumeration/cli_options.rb', line 152

def cli_users_opts
  [
    OptSmartList.new(
      ['--users-list LIST',
       'List of users to check during the users enumeration from the Login Error Messages'],
      advanced: true
    ),
    OptChoice.new(
      ['--users-detection MODE',
       'Use the supplied mode to enumerate Users, instead of the global (--detection-mode) mode.'],
      choices: %w[mixed passive aggressive], normalize: :to_sym, advanced: true
    ),
    OptRegexp.new(
      [
        '--exclude-usernames REGEXP_OR_STRING',
        'Exclude usernames matching the Regexp/string (case insensitive). Regexp delimiters are not required.'
      ], options: Regexp::IGNORECASE
    )
  ]
end

#default_opts(type) ⇒ Hash

Parameters:

  • type (String)

    (plugins, themes etc)

Returns:

  • (Hash)


50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'app/controllers/enumeration/enum_methods.rb', line 50

def default_opts(type)
  mode = ParsedCli.options[:"#{type}_detection"] || ParsedCli.detection_mode

  {
    mode: mode,
    exclude_content: ParsedCli.exclude_content_based,
    show_progression: user_interaction?,
    version_detection: {
      mode: ParsedCli.options[:"#{type}_version_detection"] || mode,
      confidence_threshold: ParsedCli.options[:"#{type}_version_all"] ? 0 : 100
    }
  }
end

#enum_backup_foldersObject



256
257
258
259
260
261
# File 'app/controllers/enumeration/enum_methods.rb', line 256

def enum_backup_folders
  opts = { list: ParsedCli.backup_folders_list, show_progression: user_interaction? }

  output('@info', msg: 'Enumerating Backup Folders') if user_interaction?
  output('backup_folders', backup_folders: target.backup_folders(opts))
end

#enum_config_backupsObject



242
243
244
245
246
247
# File 'app/controllers/enumeration/enum_methods.rb', line 242

def enum_config_backups
  opts = { list: ParsedCli.config_backups_list, show_progression: user_interaction? }

  output('@info', msg: 'Enumerating Config Backups') if user_interaction?
  output('config_backups', config_backups: target.config_backups(opts))
end

#enum_db_exportsObject



249
250
251
252
253
254
# File 'app/controllers/enumeration/enum_methods.rb', line 249

def enum_db_exports
  opts = { list: ParsedCli.db_exports_list, show_progression: user_interaction? }

  output('@info', msg: 'Enumerating DB Exports') if user_interaction?
  output('db_exports', db_exports: target.db_exports(opts))
end

#enum_detection_message(detection_mode) ⇒ String

Parameters:

  • detection_mode (Symbol)

Returns:

  • (String)


37
38
39
40
41
42
43
44
45
# File 'app/controllers/enumeration/enum_methods.rb', line 37

def enum_detection_message(detection_mode)
  detection_method = if detection_mode == :mixed
                       'Passive and Aggressive'
                     else
                       detection_mode.to_s.capitalize
                     end

  "(via #{detection_method} Methods)"
end

#enum_mediasObject



263
264
265
266
267
268
269
270
271
272
# File 'app/controllers/enumeration/enum_methods.rb', line 263

def enum_medias
  opts = { range: ParsedCli.enumerate[:medias], show_progression: user_interaction? }

  if user_interaction?
    output('@info',
           msg: 'Enumerating Medias (Permalink setting must be set to "Plain" for those to be detected)')
  end

  output('medias', medias: target.medias(opts))
end

#enum_message(type, detection_mode) ⇒ String

Returns The related enumration message depending on the ParsedCli and type supplied.

Parameters:

  • type (String)

    (plugins or themes)

  • detection_mode (Symbol)

Returns:

  • (String)

    The related enumration message depending on the ParsedCli and type supplied



19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'app/controllers/enumeration/enum_methods.rb', line 19

def enum_message(type, detection_mode)
  return unless %w[plugins themes].include?(type)

  enumerate = ParsedCli.enumerate || {}
  details = if enumerate[:"vulnerable_#{type}"]
              'Vulnerable'
            elsif enumerate[:"all_#{type}"]
              'All'
            else
              'Most Popular'
            end

  "Enumerating #{details} #{type.capitalize} #{enum_detection_message(detection_mode)}"
end

#enum_pluginsObject



119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'app/controllers/enumeration/enum_methods.rb', line 119

def enum_plugins
  opts = default_opts('plugins').merge(
    list: plugins_list_from_opts(ParsedCli.options),
    threshold: ParsedCli.plugins_threshold,
    sort: true
  )

  output('@info', msg: enum_message('plugins', opts[:mode])) if user_interaction?

  enum_wp_items(
    'plugin', target_method: :plugins, opts: opts,
              only_vulnerable: ParsedCli.enumerate&.dig(:vulnerable_plugins)
  )
end

#enum_plugins?(opts) ⇒ Boolean

Returns Wether or not to enumerate the plugins.

Parameters:

  • opts (Hash)

Returns:

  • (Boolean)

    Wether or not to enumerate the plugins



113
114
115
116
117
# File 'app/controllers/enumeration/enum_methods.rb', line 113

def enum_plugins?(opts)
  return false if ParsedCli.wp_auth

  ParsedCli.plugins_list || opts[:popular_plugins] || opts[:all_plugins] || opts[:vulnerable_plugins]
end

#enum_themesObject



159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'app/controllers/enumeration/enum_methods.rb', line 159

def enum_themes
  opts = default_opts('themes').merge(
    list: themes_list_from_opts(ParsedCli.options),
    threshold: ParsedCli.themes_threshold,
    sort: true
  )

  output('@info', msg: enum_message('themes', opts[:mode])) if user_interaction?

  enum_wp_items(
    'theme', target_method: :themes, opts: opts,
             only_vulnerable: ParsedCli.enumerate&.dig(:vulnerable_themes)
  )
end

#enum_themes?(opts) ⇒ Boolean

Returns Wether or not to enumerate the themes.

Parameters:

  • opts (Hash)

Returns:

  • (Boolean)

    Wether or not to enumerate the themes



153
154
155
156
157
# File 'app/controllers/enumeration/enum_methods.rb', line 153

def enum_themes?(opts)
  return false if ParsedCli.wp_auth

  ParsedCli.themes_list || opts[:popular_themes] || opts[:all_themes] || opts[:vulnerable_themes]
end

#enum_timthumbsObject



235
236
237
238
239
240
# File 'app/controllers/enumeration/enum_methods.rb', line 235

def enum_timthumbs
  opts = { list: ParsedCli.timthumbs_list, show_progression: user_interaction? }

  output('@info', msg: 'Enumerating Timthumbs') if user_interaction?
  output('timthumbs', timthumbs: target.timthumbs(opts))
end

#enum_usersObject



281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'app/controllers/enumeration/enum_methods.rb', line 281

def enum_users
  opts = default_opts('users').merge(
    range: enum_users_range,
    list: ParsedCli.users_list
  )

  output('@info', msg: "Enumerating Users #{enum_detection_message(opts[:mode])}") if user_interaction?

  stream = stream_findings?
  exclude = ParsedCli.exclude_usernames

  users = target.users(opts) do |user|
    next unless stream
    next if exclude&.match?(user.username)

    output('user', user: user)
  end || []

  if stream
    output('@notice', msg: users.empty? ? 'No Users Found.' : "#{users.size} user(s) Identified.")
  else
    output('users', users: users)
  end
end

#enum_users?(opts) ⇒ Boolean

Returns Wether or not to enumerate the users.

Parameters:

  • opts (Hash)

Returns:

  • (Boolean)

    Wether or not to enumerate the users



277
278
279
# File 'app/controllers/enumeration/enum_methods.rb', line 277

def enum_users?(opts)
  opts[:users] || (ParsedCli.passwords && !ParsedCli.username && !ParsedCli.usernames)
end

#enum_users_rangeRange

If the –enumerate is used, the default value is handled by the Option However, when using –passwords alone, the default has to be set by the code below

Returns:

  • (Range)

    The user ids range to enumerate



309
310
311
# File 'app/controllers/enumeration/enum_methods.rb', line 309

def enum_users_range
  ParsedCli.enumerate&.dig(:users) || cli_enum_choices[0].choices[:u].validate(nil)
end

#enum_wp_items(singular, target_method:, opts:, only_vulnerable:) ⇒ Object

Shared plugins/themes enumeration body. Streams per-item output when the active formatter supports it (and –no-stream wasn’t passed), otherwise batches the result and renders the plural view.

Parameters:

  • singular (String)

    ‘plugin’ or ‘theme’ (view name)

  • target_method (Symbol)

    :plugins or :themes

  • opts (Hash)

    Options forwarded to the target call

  • only_vulnerable (Boolean)

    Filter to vulnerable items only



182
183
184
185
186
187
188
189
190
191
# File 'app/controllers/enumeration/enum_methods.rb', line 182

def enum_wp_items(singular, target_method:, opts:, only_vulnerable:)
  stream = stream_findings?

  items = target.send(target_method, opts) do |item|
    stream_wp_item(item, singular: singular, only_vulnerable: only_vulnerable) if stream
  end

  finalize_wp_items_output(items, singular: singular, opts: opts, stream: stream,
                                  only_vulnerable: only_vulnerable)
end

#finalize_wp_items_output(items, singular:, opts:, stream:, only_vulnerable:) ⇒ Object



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'app/controllers/enumeration/enum_methods.rb', line 193

def finalize_wp_items_output(items, singular:, opts:, stream:, only_vulnerable:)
  plural = "#{singular}s"

  if !stream && user_interaction? && !items.empty?
    mode_msg = enum_detection_message(opts[:version_detection][:mode])
    output('@info', msg: "Checking #{singular.capitalize} Versions #{mode_msg}")
  end

  items.each(&:version) unless stream
  items.select!(&:vulnerable?) if only_vulnerable

  if stream
    summary = items.empty? ? "No #{plural} Found." : "#{items.size} #{singular}(s) Identified."
    output('@notice', msg: summary)
  else
    output(plural, plural.to_sym => items)
  end
end

#plugins_list_from_opts(opts) ⇒ Array<String>

Returns The plugins list associated to the cli options.

Parameters:

  • opts (Hash)

Returns:

  • (Array<String>)

    The plugins list associated to the cli options



137
138
139
140
141
142
143
144
145
146
147
148
# File 'app/controllers/enumeration/enum_methods.rb', line 137

def plugins_list_from_opts(opts)
  # List file provided by the user via the cli
  return opts[:plugins_list] if opts[:plugins_list]

  if opts[:enumerate][:all_plugins]
    DB::Plugins.all_slugs
  elsif opts[:enumerate][:popular_plugins]
    DB::Plugins.popular_slugs
  else
    DB::Plugins.vulnerable_slugs
  end
end

#resolve_list_enumerate_collisions(enum) ⇒ Object

Resolves collisions between –plugins-list/–themes-list and the corresponding –enumerate choices. The list options take precedence; colliding enumerate keys are removed from the supplied hash and a notice is emitted for each ignored choice.

Parameters:

  • enum (Hash)

    The ParsedCli.enumerate hash (mutated in place)



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'app/controllers/enumeration/enum_methods.rb', line 70

def resolve_list_enumerate_collisions(enum)
  {
    plugins_list: %i[vulnerable_plugins all_plugins popular_plugins],
    themes_list: %i[vulnerable_themes all_themes popular_themes]
  }.each do |list_opt, enum_keys|
    next unless ParsedCli.send(list_opt)

    ignored = enum_keys.select { |k| enum.key?(k) }
    next if ignored.empty?

    ignored.each { |k| enum.delete(k) }

    output(
      '@notice',
      msg: "--#{list_opt.to_s.tr('_', '-')} provided; " \
           "ignoring colliding --enumerate choice(s): #{ignored.join(', ')}"
    )
  end
end

#runObject



30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'app/controllers/enumeration.rb', line 30

def run
  enum = ParsedCli.enumerate || {}

  resolve_list_enumerate_collisions(enum)
  suppress_plugin_theme_choices_when_authenticated(enum)

  enum_plugins if enum_plugins?(enum)
  enum_themes  if enum_themes?(enum)

  %i[timthumbs config_backups db_exports backup_folders medias].each do |key|
    send(:"enum_#{key}") if enum.key?(key)
  end

  enum_users if enum_users?(enum)
end

#stream_findings?Boolean

Returns Whether enumeration findings should be streamed as they are discovered rather than batched at end of step. Streaming requires both a streaming-capable formatter (cli, cli_no_color, jsonl) and the user not having opted out via –no-stream.

Returns:

  • (Boolean)

    Whether enumeration findings should be streamed as they are discovered rather than batched at end of step. Streaming requires both a streaming-capable formatter (cli, cli_no_color, jsonl) and the user not having opted out via –no-stream.



11
12
13
# File 'app/controllers/enumeration/enum_methods.rb', line 11

def stream_findings?
  formatter.streams? && ParsedCli.stream != false
end

#stream_wp_item(item, singular:, only_vulnerable:) ⇒ Object



212
213
214
215
216
217
# File 'app/controllers/enumeration/enum_methods.rb', line 212

def stream_wp_item(item, singular:, only_vulnerable:)
  item.version
  return if only_vulnerable && !item.vulnerable?

  output(singular, singular.to_sym => item)
end

#suppress_plugin_theme_choices_when_authenticated(enum) ⇒ Object

Suppresses plugin/theme –enumerate choices and –plugins-list / –themes-list when –wp-auth was supplied, since AuthenticatedInventory already populated the target with authoritative data.

Parameters:

  • enum (Hash)

    The enumeration hash, mutated in place.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'app/controllers/enumeration/enum_methods.rb', line 95

def suppress_plugin_theme_choices_when_authenticated(enum)
  return unless ParsedCli.wp_auth

  suppressed = enum.keys & WP_AUTH_SUPPRESSED_CHOICES
  suppressed.each { |k| enum.delete(k) }

  lists_suppressed = %i[plugins_list themes_list].select { |opt| ParsedCli.send(opt) }
  return if suppressed.empty? && lists_suppressed.empty?

  ignored = (suppressed + lists_suppressed.map { |o| "--#{o.to_s.tr('_', '-')}" }).join(', ')
  output('@notice',
         msg: "--wp-auth provided; ignoring plugin/theme enumeration option(s): #{ignored} " \
              '(authoritative inventory already retrieved via the WP REST API).')
end

#themes_list_from_opts(opts) ⇒ Array<String>

Returns The themes list associated to the cli options.

Parameters:

  • opts (Hash)

Returns:

  • (Array<String>)

    The themes list associated to the cli options



222
223
224
225
226
227
228
229
230
231
232
233
# File 'app/controllers/enumeration/enum_methods.rb', line 222

def themes_list_from_opts(opts)
  # List file provided by the user via the cli
  return opts[:themes_list] if opts[:themes_list]

  if opts[:enumerate][:all_themes]
    DB::Themes.all_slugs
  elsif opts[:enumerate][:popular_themes]
    DB::Themes.popular_slugs
  else
    DB::Themes.vulnerable_slugs
  end
end