Skip to content

Commit 9b2b042

Browse files
Land rapid7#18875, Add conditional option validation depending on SESSION/RHOST connection
2 parents 2eaec5b + 2df926a commit 9b2b042

File tree

16 files changed

+380
-37
lines changed

16 files changed

+380
-37
lines changed

lib/msf/core/module/options.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,18 @@ def register_options(options, owner = self.class)
7373
# @param name [String] Name for the group
7474
# @param description [String] Description of the group
7575
# @param option_names [Array<String>] List of datastore option names
76+
# @param required_options [Array<String>] List of required datastore option names
7677
# @param merge [Boolean] whether to merge or overwrite the groups option names
77-
def register_option_group(name:, description:, option_names: [], merge: true)
78+
def register_option_group(name:, description:, option_names: [], required_options: [], merge: true)
7879
existing_group = options.groups[name]
7980
if merge && existing_group
8081
existing_group.description = description
8182
existing_group.add_options(option_names)
8283
else
83-
option_group = Msf::OptionGroup.new(name: name, description: description, option_names: option_names)
84+
option_group = Msf::OptionGroup.new(name: name,
85+
description: description,
86+
option_names: option_names,
87+
required_options: required_options)
8488
options.add_group(option_group)
8589
end
8690
end

lib/msf/core/option_group.rb

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,18 @@ class OptionGroup
99
attr_accessor :description
1010
# @return [Array<String>] List of datastore option names
1111
attr_accessor :option_names
12+
# @return [Array<String>] List of options that if present must have a value set
13+
attr_accessor :required_options
1214

1315
# @param name [String] Name for the group
1416
# @param description [String] Description to be displayed to the user
1517
# @param option_names [Array<String>] List of datastore option names
16-
def initialize(name:, description:, option_names: [])
18+
# @param required_options [Array<String>] List of options that if present must have a value set
19+
def initialize(name:, description:, option_names: [], required_options: [])
1720
self.name = name
1821
self.description = description
1922
self.option_names = option_names
23+
self.required_options = required_options
2024
end
2125

2226
# @param option_name [String] Name of the datastore option to be added to the group
@@ -28,5 +32,19 @@ def add_option(option_name)
2832
def add_options(option_names)
2933
@option_names.concat(option_names)
3034
end
35+
36+
# Validates that any registered and required options are set
37+
#
38+
# @param options [Array<Msf::OptBase>] A modules registered options
39+
# @param datastore [Msf::DataStore|Msf::DataStoreWithFallbacks] A modules datastore
40+
def validate(options, datastore)
41+
issues = {}
42+
required_options.each do |option_name|
43+
if options[option_name] && !datastore[option_name]
44+
issues[option_name] = "#{option_name} must be specified"
45+
end
46+
end
47+
raise Msf::OptionValidateError.new(issues.keys.to_a, reasons: issues) unless issues.empty?
48+
end
3149
end
3250
end

lib/msf/core/optional_session.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,52 @@
77
module Msf
88
module OptionalSession
99
include Msf::SessionCompatibility
10+
11+
# Validates options depending on whether we are using SESSION or an RHOST for our connection
12+
def validate
13+
super
14+
return unless optional_session_enabled?
15+
16+
# If the session is set use that by default regardless of rhost being (un)set
17+
if session
18+
validate_session
19+
elsif rhost
20+
validate_rhost
21+
else
22+
raise Msf::OptionValidateError.new(message: 'A SESSION or RHOST must be provided')
23+
end
24+
end
25+
26+
def session
27+
return nil unless optional_session_enabled?
28+
29+
super
30+
end
31+
32+
protected
33+
34+
# Used to validate options when RHOST has been set
35+
def validate_rhost
36+
validate_group('RHOST')
37+
end
38+
39+
# Used to validate options when SESSION has been set
40+
def validate_session
41+
issues = {}
42+
if session_types && !session_types.include?(session.type)
43+
issues['SESSION'] = "Incompatible session type: #{session.type}. This module works with: #{session_types.join(', ')}."
44+
end
45+
raise Msf::OptionValidateError.new(issues.keys.to_a, reasons: issues) unless issues.empty?
46+
47+
validate_group('SESSION')
48+
end
49+
50+
# Validates the options within an option group
51+
#
52+
# @param group_name [String] Name of the option group
53+
def validate_group(group_name)
54+
option_group = options.groups[group_name]
55+
option_group.validate(options, datastore) if option_group
56+
end
1057
end
1158
end

lib/msf/core/optional_session/mssql.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ def initialize(info = {})
1515
)
1616
)
1717

18-
if framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE)
18+
if optional_session_enabled?
1919
register_option_group(name: 'SESSION',
2020
description: 'Used when connecting via an existing SESSION',
2121
option_names: ['SESSION'])
2222
register_option_group(name: 'RHOST',
2323
description: 'Used when making a new connection via RHOSTS',
24-
option_names: RHOST_GROUP_OPTIONS)
24+
option_names: RHOST_GROUP_OPTIONS,
25+
required_options: RHOST_GROUP_OPTIONS)
2526
register_options(
2627
[
2728
Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]),
@@ -36,10 +37,8 @@ def initialize(info = {})
3637
end
3738
end
3839

39-
def session
40-
return nil unless framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE)
41-
42-
super
40+
def optional_session_enabled?
41+
framework.features.enabled?(Msf::FeatureManager::MSSQL_SESSION_TYPE)
4342
end
4443
end
4544
end

lib/msf/core/optional_session/mysql.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ def initialize(info = {})
1515
)
1616
)
1717

18-
if framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)
18+
if optional_session_enabled?
1919
register_option_group(name: 'SESSION',
2020
description: 'Used when connecting via an existing SESSION',
2121
option_names: ['SESSION'])
2222
register_option_group(name: 'RHOST',
2323
description: 'Used when making a new connection via RHOSTS',
24-
option_names: RHOST_GROUP_OPTIONS)
24+
option_names: RHOST_GROUP_OPTIONS,
25+
required_options: RHOST_GROUP_OPTIONS)
2526
register_options(
2627
[
2728
Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]),
@@ -34,10 +35,8 @@ def initialize(info = {})
3435
end
3536
end
3637

37-
def session
38-
return nil unless framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)
39-
40-
super
38+
def optional_session_enabled?
39+
framework.features.enabled?(Msf::FeatureManager::MYSQL_SESSION_TYPE)
4140
end
4241
end
4342
end

lib/msf/core/optional_session/postgresql.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ def initialize(info = {})
1515
)
1616
)
1717

18-
if framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE)
18+
if optional_session_enabled?
1919
register_option_group(name: 'SESSION',
2020
description: 'Used when connecting via an existing SESSION',
2121
option_names: ['SESSION'])
2222
register_option_group(name: 'RHOST',
2323
description: 'Used when making a new connection via RHOSTS',
24-
option_names: RHOST_GROUP_OPTIONS)
24+
option_names: RHOST_GROUP_OPTIONS,
25+
required_options: RHOST_GROUP_OPTIONS)
2526
register_options(
2627
[
2728
Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]),
@@ -36,10 +37,8 @@ def initialize(info = {})
3637
end
3738
end
3839

39-
def session
40-
return nil unless framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE)
41-
42-
super
40+
def optional_session_enabled?
41+
framework.features.enabled?(Msf::FeatureManager::POSTGRESQL_SESSION_TYPE)
4342
end
4443
end
4544
end

lib/msf/core/optional_session/smb.rb

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ def initialize(info = {})
1515
)
1616
)
1717

18-
if framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
18+
if optional_session_enabled?
1919
register_option_group(name: 'SESSION',
2020
description: 'Used when connecting via an existing SESSION',
2121
option_names: ['SESSION'])
2222
register_option_group(name: 'RHOST',
2323
description: 'Used when making a new connection via RHOSTS',
24-
option_names: RHOST_GROUP_OPTIONS)
24+
option_names: RHOST_GROUP_OPTIONS,
25+
required_options: RHOST_GROUP_OPTIONS)
2526
register_options(
2627
[
2728
Msf::OptInt.new('SESSION', [ false, 'The session to run this module on' ]),
@@ -34,10 +35,8 @@ def initialize(info = {})
3435
end
3536
end
3637

37-
def session
38-
return nil unless framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
39-
40-
super
38+
def optional_session_enabled?
39+
framework.features.enabled?(Msf::FeatureManager::SMB_SESSION_TYPE)
4140
end
4241
end
4342
end

lib/msf/ui/formatter/option_validate_error.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ def self.print_error(mod, error)
1313
raise ArgumentError, "invalid error type #{error.class}, expected ::Msf::OptionValidateError" unless error.is_a?(::Msf::OptionValidateError)
1414

1515
if error.reasons.empty?
16-
mod.print_error("#{error.class} The following options failed to validate: #{error.options.join(', ')}")
16+
if error.message
17+
mod.print_error("#{error.class} #{error.message}")
18+
else
19+
mod.print_error("#{error.class} The following options failed to validate: #{error.options.join(', ')}")
20+
end
1721
else
1822
mod.print_error("#{error.class} The following options failed to validate:")
1923
error.options.sort.each do |option_name|

spec/lib/msf/core/option_group_spec.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,92 @@
4747
end
4848
end
4949
end
50+
51+
describe '#validate' do
52+
let(:required_option_name) { 'required_name' }
53+
let(:option_names) { ['not_required_name', required_option_name] }
54+
let(:required_names) { [required_option_name] }
55+
let(:options) { instance_double(Msf::OptionContainer) }
56+
let(:datastore) { instance_double(Msf::DataStoreWithFallbacks) }
57+
58+
context 'when there are no required options' do
59+
subject { described_class.new(name: 'name', description: 'description', option_names: option_names) }
60+
61+
context 'when no values are set for the options' do
62+
63+
before(:each) do
64+
allow(options).to receive(:[]).and_return(instance_double(Msf::OptBase))
65+
allow(datastore).to receive(:[]).and_return(nil)
66+
end
67+
68+
it 'validates the options in the group' do
69+
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
70+
end
71+
end
72+
73+
context 'when values are set for the options' do
74+
75+
before(:each) do
76+
allow(options).to receive(:[]).and_return(instance_double(Msf::OptBase))
77+
allow(datastore).to receive(:[]).and_return('OptionValue')
78+
end
79+
80+
it 'validates the options in the group' do
81+
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
82+
end
83+
end
84+
85+
context 'when the options have not been registered' do
86+
87+
before(:each) do
88+
allow(options).to receive(:[]).and_return(nil)
89+
end
90+
91+
it 'does not attempt to validate the options' do
92+
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
93+
end
94+
end
95+
end
96+
97+
context 'when there is a required option' do
98+
subject { described_class.new(name: 'name', description: 'description', option_names: option_names, required_options: required_names) }
99+
let(:error_message) { "The following options failed to validate: #{required_option_name}." }
100+
101+
context 'when no values are set for the options' do
102+
103+
before(:each) do
104+
allow(options).to receive(:[]).and_return(instance_double(Msf::OptBase))
105+
allow(datastore).to receive(:[]).and_return(nil)
106+
end
107+
108+
it 'raises an error only for the required option' do
109+
expect { subject.validate(options, datastore) }.to raise_error(Msf::OptionValidateError).with_message(error_message)
110+
end
111+
end
112+
113+
context 'when values are set for the options' do
114+
115+
before(:each) do
116+
allow(options).to receive(:[]).and_return(instance_double(Msf::OptBase))
117+
allow(datastore).to receive(:[]).and_return('OptionValue')
118+
end
119+
120+
it 'validates the options in the group' do
121+
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
122+
end
123+
end
124+
125+
context 'when the options have not been registered' do
126+
127+
before(:each) do
128+
allow(options).to receive(:[]).and_return(nil)
129+
end
130+
131+
it 'does not attempt to validate the options' do
132+
expect { subject.validate(options, datastore) }.not_to raise_error(Msf::OptionValidateError)
133+
end
134+
end
135+
end
136+
137+
end
50138
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding:binary -*-
2+
# frozen_string_literal: true
3+
4+
require 'spec_helper'
5+
6+
RSpec.describe Msf::OptionalSession::MSSQL do
7+
subject(:mod) do
8+
mod = ::Msf::Module.new
9+
mod.extend described_class
10+
mod
11+
end
12+
13+
before(:each) do
14+
allow(Msf::FeatureManager.instance).to receive(:enabled?).and_call_original
15+
allow(Msf::FeatureManager.instance).to receive(:enabled?).with(Msf::FeatureManager::MSSQL_SESSION_TYPE).and_return(true)
16+
end
17+
18+
it_behaves_like Msf::OptionalSession
19+
end

0 commit comments

Comments
 (0)