Adds rhost url support behind a feature flag

Tidy up test

Return a string instead of a URI object

Code review comments

Rubcocop
This commit is contained in:
dwelch-r7 2020-06-17 14:59:19 +01:00
parent 3fcdbd9402
commit e7061439ef
21 changed files with 254 additions and 80 deletions

View File

@ -43,14 +43,23 @@ class DataStore < Hash
end
end
super(k,v)
if v.is_a? Hash
v.each { |key, value| self[key] = value }
else
super(k,v)
end
end
#
# Case-insensitive wrapper around hash lookup
#
def [](k)
super(find_key_case(k))
k = find_key_case(k)
if options[k].respond_to? :calculate_value
options[k].calculate_value(self)
else
super(k)
end
end
#

View File

@ -30,6 +30,10 @@ module Exploit::Remote::HttpClient
], self.class
)
if framework.features.enabled?("RHOST_HTTP_URL")
register_options([Opt::RHOST_HTTP_URL])
end
register_advanced_options(
[
OptString.new('UserAgent', [false, 'The User-Agent header to use for all requests',

View File

@ -20,6 +20,11 @@ module Msf
name: 'wrapped_tables',
description: 'When enabled Metasploit will wordwrap all tables to fit into the available terminal width',
default_value: false
}.freeze,
{
name: 'RHOST_HTTP_URL',
description: 'When enabled in supported modules you can specify a URL as a target',
default_value: false
}.freeze
].freeze

View File

@ -14,88 +14,83 @@ module Msf
# register_advanced_options([Opt::Proxies])
#
module Opt
# @return [OptAddress]
def self.CHOST(default=nil, required=false, desc="The local client address")
def self.CHOST(default = nil, required = false, desc = 'The local client address')
Msf::OptAddress.new(__method__.to_s, [ required, desc, default ])
end
# @return [OptPort]
def self.CPORT(default=nil, required=false, desc="The local client port")
def self.CPORT(default = nil, required = false, desc = 'The local client port')
Msf::OptPort.new(__method__.to_s, [ required, desc, default ])
end
# @return [OptAddressLocal]
def self.LHOST(default=nil, required=true, desc="The listen address (an interface may be specified)")
def self.LHOST(default = nil, required = true, desc = 'The listen address (an interface may be specified)')
Msf::OptAddressLocal.new(__method__.to_s, [ required, desc, default ])
end
# @return [OptPort]
def self.LPORT(default=nil, required=true, desc="The listen port")
def self.LPORT(default = nil, required = true, desc = 'The listen port')
Msf::OptPort.new(__method__.to_s, [ required, desc, default ])
end
# @return [OptString]
def self.Proxies(default=nil, required=false, desc="A proxy chain of format type:host:port[,type:host:port][...]")
def self.Proxies(default = nil, required = false, desc = 'A proxy chain of format type:host:port[,type:host:port][...]')
Msf::OptString.new(__method__.to_s, [ required, desc, default ])
end
# @return [OptAddressRange]
def self.RHOSTS(default=nil, required=true, desc="The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'")
def self.RHOSTS(default = nil, required = true, desc = "The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'")
Msf::OptAddressRange.new('RHOSTS', [ required, desc, default ])
end
def self.RHOST(default=nil, required=true, desc="The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'")
def self.RHOST(default = nil, required = true, desc = "The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'")
Msf::OptAddressRange.new('RHOSTS', [ required, desc, default ], aliases: [ 'RHOST' ])
end
# @return [OptPort]
def self.RPORT(default=nil, required=true, desc="The target port")
def self.RPORT(default = nil, required = true, desc = 'The target port')
Msf::OptPort.new(__method__.to_s, [ required, desc, default ])
end
# @return [OptEnum]
def self.SSLVersion
Msf::OptEnum.new('SSLVersion',
'Specify the version of SSL/TLS to be used (Auto, TLS and SSL23 are auto-negotiate)',
enums: Rex::Socket::SslTcp.supported_ssl_methods
)
'Specify the version of SSL/TLS to be used (Auto, TLS and SSL23 are auto-negotiate)',
enums: Rex::Socket::SslTcp.supported_ssl_methods)
end
def self.RHOST_HTTP_URL(default = nil, required = false, desc = 'The target URL, only applicable if there is a single URL')
Msf::OptHTTPRhostURL.new(__method__.to_s, [required, desc, default ])
end
def self.stager_retry_options
[
OptInt.new('StagerRetryCount',
'The number of times the stager should retry if the first connect fails',
default: 10,
aliases: ['ReverseConnectRetries']
),
'The number of times the stager should retry if the first connect fails',
default: 10,
aliases: ['ReverseConnectRetries']),
OptInt.new('StagerRetryWait',
'Number of seconds to wait for the stager between reconnect attempts',
default: 5
)
'Number of seconds to wait for the stager between reconnect attempts',
default: 5)
]
end
def self.http_proxy_options
[
OptString.new('HttpProxyHost', 'An optional proxy server IP address or hostname',
aliases: ['PayloadProxyHost']
),
aliases: ['PayloadProxyHost']),
OptPort.new('HttpProxyPort', 'An optional proxy server port',
aliases: ['PayloadProxyPort']
),
aliases: ['PayloadProxyPort']),
OptString.new('HttpProxyUser', 'An optional proxy server username',
aliases: ['PayloadProxyUser'],
max_length: Rex::Payloads::Meterpreter::Config::PROXY_USER_SIZE - 1
),
aliases: ['PayloadProxyUser'],
max_length: Rex::Payloads::Meterpreter::Config::PROXY_USER_SIZE - 1),
OptString.new('HttpProxyPass', 'An optional proxy server password',
aliases: ['PayloadProxyPass'],
max_length: Rex::Payloads::Meterpreter::Config::PROXY_PASS_SIZE - 1
),
aliases: ['PayloadProxyPass'],
max_length: Rex::Payloads::Meterpreter::Config::PROXY_PASS_SIZE - 1),
OptEnum.new('HttpProxyType', 'The type of HTTP proxy',
enums: ['HTTP', 'SOCKS'],
aliases: ['PayloadProxyType']
)
enums: ['HTTP', 'SOCKS'],
aliases: ['PayloadProxyType'])
]
end
@ -114,6 +109,7 @@ module Msf
Proxies = Proxies()
RHOST = RHOST()
RHOSTS = RHOSTS()
RHOST_HTTP_URL = RHOST_HTTP_URL()
RPORT = RPORT()
SSLVersion = SSLVersion()
end

View File

@ -0,0 +1,87 @@
# -*- coding: binary -*-
module Msf
###
#
# RHOST URL option.
#
###
class OptHTTPRhostURL < OptBase
def type
'rhost http url'
end
def normalize(value)
return unless value
uri = get_uri(value)
return unless uri
option_hash = {}
# Blank this out since we don't know if this new value will have a `VHOST` to ensure we remove the old value
option_hash['VHOST'] = nil
option_hash['RHOSTS'] = uri.hostname
option_hash['RPORT'] = uri.port
option_hash['SSL'] = %w[ssl https].include?(uri.scheme)
# Both `TARGETURI` and `URI` are used as datastore options to denote the path on a uri
option_hash['TARGETURI'] = uri.path.present? ? uri.path : '/'
option_hash['URI'] = option_hash['TARGETURI']
if uri.scheme && %(http https).include?(uri.scheme)
option_hash['VHOST'] = uri.hostname unless Rex::Socket.is_ip_addr?(uri.hostname)
option_hash['HttpUsername'] = uri.user.to_s
option_hash['HttpPassword'] = uri.password.to_s
end
option_hash
end
def valid?(value, check_empty: false)
return true unless value || required
uri = get_uri(value)
return false unless uri && !uri.host.nil? && !uri.port.nil?
super
end
def calculate_value(datastore)
return unless datastore['RHOSTS']
begin
uri_type = datastore['SSL'] ? URI::HTTPS : URI::HTTP
uri = uri_type.build(host: datastore['RHOSTS'])
uri.port = datastore['RPORT']
# The datastore uses both `TARGETURI` and `URI` to denote the path of a URL, we try both here and fall back to `/`
uri.path = (datastore['TARGETURI'] || datastore['URI'] || '/')
uri.user = datastore['HttpUsername']
uri.password = datastore['HttpPassword'] if uri.user
uri.to_s
rescue URI::InvalidComponentError
nil
end
end
protected
def get_uri(value)
return unless value
return unless single_rhost?(value)
value = 'http://' + value unless value.start_with?(%r{https?://})
URI(value)
rescue URI::InvalidURIError
nil
end
def single_rhost?(value)
return true if value =~ /[^-0-9,.*\/]/
walker = Rex::Socket::RangeWalker.new(value)
return false unless walker.valid?
# if there is only a single ip then it's not a range
walker.length == 1
end
end
end

View File

@ -28,7 +28,7 @@ class OptRaw < OptBase
value
end
def valid?(value=self.value)
def valid?(value=self.value, check_empty: true)
value = normalize(value)
return false if empty_required_value?(value)
return super

View File

@ -18,6 +18,7 @@ module Msf
autoload :OptRaw, 'msf/core/opt_raw'
autoload :OptRegexp, 'msf/core/opt_regexp'
autoload :OptString, 'msf/core/opt_string'
autoload :OptHTTPRhostURL, 'msf/core/opt_http_rhost_url'
#
# The options purpose in life is to associate named options with arbitrary

View File

@ -4,9 +4,12 @@ require 'metasploit/framework/aws/client'
RSpec.describe Metasploit::Framework::Aws::Client do
subject do
s = Class.new(Msf::Auxiliary) do
mod_klass = Class.new(Msf::Auxiliary) do
include Metasploit::Framework::Aws::Client
end.new
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
s = mod_klass.new
s.datastore['Region'] = 'us-east-1'
s.datastore['RHOST'] = '127.0.0.1'
s

View File

@ -6,10 +6,12 @@ require 'msf/core/exploit/http/jboss'
RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::Base do
subject do
mod = ::Msf::Exploit.new
mod.extend Msf::Exploit::Remote::HTTP::JBoss
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include Msf::Exploit::Remote::HTTP::JBoss
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
describe "#deploy" do

View File

@ -6,10 +6,12 @@ require 'msf/core/exploit/http/jboss'
RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::BeanShellScripts do
subject do
mod = ::Msf::Exploit.new
mod.extend Msf::Exploit::Remote::HTTP::JBoss
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include Msf::Exploit::Remote::HTTP::JBoss
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
describe "#generate_bsh" do

View File

@ -7,10 +7,12 @@ require 'msf/core/exploit/http/jboss'
RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::BeanShell do
subject do
mod = ::Msf::Exploit.new
mod.extend Msf::Exploit::Remote::HTTP::JBoss
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include Msf::Exploit::Remote::HTTP::JBoss
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
before :example do

View File

@ -6,10 +6,12 @@ require 'msf/core/exploit/http/jboss'
RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::DeploymentFileRepositoryScripts do
subject do
mod = ::Msf::Exploit.new
mod.extend Msf::Exploit::Remote::HTTP::JBoss
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include Msf::Exploit::Remote::HTTP::JBoss
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
describe "#stager_jsp_with_payload" do

View File

@ -6,10 +6,12 @@ require 'msf/core/exploit/http/jboss'
RSpec.describe Msf::Exploit::Remote::HTTP::JBoss::DeploymentFileRepository do
subject do
mod = ::Msf::Exploit.new
mod.extend Msf::Exploit::Remote::HTTP::JBoss
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include Msf::Exploit::Remote::HTTP::JBoss
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
let (:base_name) do

View File

@ -6,10 +6,12 @@ require 'msf/core/exploit/http/joomla'
RSpec.describe Msf::Exploit::Remote::HTTP::Joomla::Base do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::Exploit::Remote::HTTP::Joomla
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include ::Msf::Exploit::Remote::HTTP::Joomla
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
let(:joomla_body) do

View File

@ -5,10 +5,12 @@ require 'msf/core/exploit/http/joomla'
RSpec.describe Msf::Exploit::Remote::HTTP::Joomla::Version do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::Exploit::Remote::HTTP::Joomla
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include ::Msf::Exploit::Remote::HTTP::Joomla
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
# From /joomla/language/en-GB/en-GB.xml

View File

@ -8,10 +8,12 @@ require 'msf/core/exploit/http/wordpress'
RSpec.describe Msf::Exploit::Remote::HTTP::Wordpress::Base do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::Exploit::Remote::HTTP::Wordpress
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include ::Msf::Exploit::Remote::HTTP::Wordpress
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
describe '#wordpress_and_online?' do

View File

@ -8,10 +8,12 @@ require 'msf/core/exploit/http/wordpress'
RSpec.describe Msf::Exploit::Remote::HTTP::Wordpress::Login do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::Exploit::Remote::HTTP::Wordpress
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include ::Msf::Exploit::Remote::HTTP::Wordpress
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
describe '#wordpress_login' do

View File

@ -8,10 +8,12 @@ require 'msf/core/exploit/http/wordpress'
RSpec.describe Msf::Exploit::Remote::HTTP::Wordpress::Version do
subject do
mod = ::Msf::Exploit.new
mod.extend ::Msf::Exploit::Remote::HTTP::Wordpress
mod.send(:initialize)
mod
mod_klass = Class.new(::Msf::Exploit) do
include ::Msf::Exploit::Remote::HTTP::Wordpress
end
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
describe '#wordpress_version' do

View File

@ -0,0 +1,38 @@
require 'msf/core/opt_http_rhost_url'
RSpec.describe Msf::OptHTTPRhostURL do
subject(:opt) { described_class.new('RHOST_HTTP_URL') }
valid_values = [
{ value: 'http://example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '/', 'URI' => '/', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } },
{ value: 'https://example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 443, 'SSL' => true, 'TARGETURI' => '/', 'URI' => '/', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } },
{ value: 'example.com', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '/', 'URI' => '/', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } },
{ value: 'http://user:pass@example.com:1234/somePath', normalized: { 'RHOSTS' => 'example.com', 'RPORT' => 1234, 'SSL' => false, 'TARGETURI' => '/somePath', 'URI' => '/somePath', 'VHOST' => 'example.com', 'HttpUsername' => 'user', 'HttpPassword' => 'pass' } },
{ value: 'http://127.0.0.1', normalized: { 'RHOSTS' => '127.0.0.1', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '/', 'URI' => '/', 'VHOST' => nil, 'HttpUsername' => '', 'HttpPassword' => '' } }
]
invalid_values = [
{ value: '192.0.2.0/24' },
{ value: '192.0.2.0-255' },
{ value: '192.0.2.0,1-255' },
{ value: '192.0.2.*' },
{ value: '192.0.2.0-192.0.2.255' }
]
it_behaves_like 'an option', valid_values, invalid_values, 'rhost http url'
describe '#calculate_value' do
[
{ expected_url: 'http://example.com', datastore: { 'RHOSTS' => 'example.com', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } },
{ expected_url: 'https://example.com', datastore: { 'RHOSTS' => 'example.com', 'RPORT' => 443, 'SSL' => true, 'TARGETURI' => '', 'URI' => '', 'VHOST' => 'example.com', 'HttpUsername' => '', 'HttpPassword' => '' } },
{ expected_url: 'http://user:pass@example.com:1234/somePath', datastore: { 'RHOSTS' => 'example.com', 'RPORT' => 1234, 'SSL' => false, 'TARGETURI' => '/somePath', 'URI' => '/somePath', 'VHOST' => 'example.com', 'HttpUsername' => 'user', 'HttpPassword' => 'pass' } },
{ expected_url: 'http://127.0.0.1', datastore: { 'RHOSTS' => '127.0.0.1', 'RPORT' => 80, 'SSL' => false, 'TARGETURI' => '', 'URI' => '', 'VHOST' => nil, 'HttpUsername' => '', 'HttpPassword' => '' } }
].each do |test|
context test[:datastore].to_s do
it "should return #{test[:expected_url]}" do
expect(subject.calculate_value(test[:datastore])).to eq(test[:expected_url])
end
end
end
end
end

View File

@ -55,7 +55,10 @@ RSpec.describe Md5LookupUtility do
end
subject do
Md5LookupUtility::Md5Lookup.new
mod_klass = Md5LookupUtility::Md5Lookup
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new
end
def set_expected_response(body)
@ -253,7 +256,11 @@ RSpec.describe Md5LookupUtility do
describe '#get_hash_results' do
context 'when a hash is found' do
it 'yields a result' do
search_engine = Md5LookupUtility::Md5Lookup.new
mod_klass = Md5LookupUtility::Md5Lookup
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
search_engine = mod_klass.new
allow(search_engine).to receive(:lookup).and_return(good_result)
allow(Md5LookupUtility::Md5Lookup).to receive(:new).and_return(search_engine)

View File

@ -87,7 +87,11 @@ RSpec.describe VirusTotalUtility do
let(:vt) do
file = double(File, read: malware_data)
allow(File).to receive(:open).with(filename, 'rb') {|&block| block.yield file}
VirusTotalUtility::VirusTotal.new({'api_key'=>api_key, 'sample'=>filename})
mod_klass = VirusTotalUtility::VirusTotal
features = instance_double(Msf::FeatureManager, enabled?: false)
mod_klass.framework = instance_double(Msf::Framework, features: features, datastore: {})
mod_klass.new({'api_key'=>api_key, 'sample'=>filename})
end
context ".Initializer" do