Skip to content

Commit

Permalink
Switch to an enum option for the signing
Browse files Browse the repository at this point in the history
  • Loading branch information
smcintyre-r7 committed May 2, 2024
1 parent 6d915db commit a9fd264
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 48 deletions.
33 changes: 18 additions & 15 deletions lib/metasploit/framework/ldap/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,16 @@ def ldap_connect_opts(rhost, rport, connect_timeout, ssl: true, opts: {})

case opts[:ldap_auth]
when Msf::Exploit::Remote::AuthOption::SCHANNEL
raise Msf::ValidationError, 'The SSL option must be enabled when using SCHANNEL authentication.' unless ssl

connect_opts.merge!(ldap_auth_opts_schannel(opts))
connect_opts.merge!(ldap_auth_opts_schannel(opts, ssl))
when Msf::Exploit::Remote::AuthOption::KERBEROS
connect_opts.merge!(ldap_auth_opts_kerberos(opts))
when Msf::Exploit::Remote::AuthOption::NTLM
connect_opts.merge!(ldap_auth_opts_ntlm(opts))
connect_opts.merge!(ldap_auth_opts_ntlm(opts, ssl))
when Msf::Exploit::Remote::AuthOption::PLAINTEXT
connect_opts.merge!(ldap_auth_opts_plaintext(opts))
when Msf::Exploit::Remote::AuthOption::AUTO
if opts[:username].present? && opts[:domain].present?
connect_opts.merge!(ldap_auth_opts_ntlm(opts))
connect_opts.merge!(ldap_auth_opts_ntlm(opts, ssl))
elsif opts[:username].present?
connect_opts.merge!(ldap_auth_opts_plaintext(opts))
end
Expand All @@ -149,15 +147,15 @@ def ldap_connect_opts(rhost, rport, connect_timeout, ssl: true, opts: {})

private

def ldap_auth_opts_kerberos(opts)
def ldap_auth_opts_kerberos(opts, ssl)
auth_opts = {}
raise Msf::ValidationError, 'The Ldap::Rhostname option is required when using Kerberos authentication.' if opts[:ldap_rhostname].blank?
raise Msf::ValidationError, 'The LDAP::Rhostname option is required when using Kerberos authentication.' if opts[:ldap_rhostname].blank?
raise Msf::ValidationError, 'The DOMAIN option is required when using Kerberos authentication.' if opts[:domain].blank?

offered_etypes = Msf::Exploit::Remote::AuthOption.as_default_offered_etypes(opts[:ldap_krb_offered_enc_types])
raise Msf::ValidationError, 'At least one encryption type is required when using Kerberos authentication.' if offered_etypes.empty?

use_gss_checksum = opts[:should_encrypt]
sign_and_seal = opts.fetch(:sign_and_seal, !ssl)
kerberos_authenticator = Msf::Exploit::Remote::Kerberos::ServiceAuthenticator::LDAP.new(
host: opts[:domain_controller_rhost].blank? ? nil : opts[:domain_controller_rhost],
hostname: opts[:ldap_rhostname],
Expand All @@ -170,7 +168,7 @@ def ldap_auth_opts_kerberos(opts)
ticket_storage: opts[:kerberos_ticket_storage],
offered_etypes: offered_etypes,
mutual_auth: true,
use_gss_checksum: use_gss_checksum
use_gss_checksum: sign_and_seal
)

encryptor = SpnegoKerberosEncryptor.new(kerberos_authenticator)
Expand All @@ -184,14 +182,14 @@ def ldap_auth_opts_kerberos(opts)
challenge_response: true
}

if opts[:should_encrypt]
if sign_and_seal
auth_opts[:auth][:auth_context_setup] = encryptor.method(:kerberos_setup)
end

auth_opts
end

def ldap_auth_opts_ntlm(opts)
def ldap_auth_opts_ntlm(opts, ssl)
auth_opts = {}
flags = RubySMB::NTLM::NEGOTIATE_FLAGS[:UNICODE] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:REQUEST_TARGET] |
Expand All @@ -202,7 +200,8 @@ def ldap_auth_opts_ntlm(opts)
RubySMB::NTLM::NEGOTIATE_FLAGS[:TARGET_INFO] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:VERSION_INFO]

if opts[:should_encrypt]
sign_and_seal = opts.fetch(:sign_and_seal, !ssl)
if sign_and_seal
flags = flags |
RubySMB::NTLM::NEGOTIATE_FLAGS[:SIGN] |
RubySMB::NTLM::NEGOTIATE_FLAGS[:SEAL] |
Expand Down Expand Up @@ -234,7 +233,7 @@ def ldap_auth_opts_ntlm(opts)
challenge_response: negotiate
}

if opts[:should_encrypt]
if sign_and_seal
auth_opts[:auth][:auth_context_setup] = encryptor.method(:ntlm_setup)
end

Expand All @@ -243,6 +242,8 @@ def ldap_auth_opts_ntlm(opts)

def ldap_auth_opts_plaintext(opts)
auth_opts = {}
raise Msf::ValidationError, 'Can not sign and seal when using Plaintext authentication.' if opts.fetch(:sign_and_seal, false)

auth_opts[:auth] = {
method: :simple,
username: opts[:username],
Expand All @@ -251,10 +252,12 @@ def ldap_auth_opts_plaintext(opts)
auth_opts
end

def ldap_auth_opts_schannel(opts)
def ldap_auth_opts_schannel(opts, ssl)
auth_opts = {}
pfx_path = opts[:ldap_cert_file]
raise Msf::ValidationError, 'The LDAP::CertFile option is required when using SCHANNEL authentication.' if pfx_path.blank?
raise Msf::ValidationError, 'The SSL option must be enabled when using Schannel authentication.' unless ssl
raise Msf::ValidationError, 'The LDAP::CertFile option is required when using Schannel authentication.' if pfx_path.blank?
raise Msf::ValidationError, 'Can not sign and seal when using Schannel authentication.' if opts.fetch(:sign_and_seal, false)

unless ::File.file?(pfx_path) && ::File.readable?(pfx_path)
raise Msf::ValidationError, 'Failed to load the PFX certificate file. The path was not a readable file.'
Expand Down
43 changes: 27 additions & 16 deletions lib/msf/core/exploit/remote/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,7 @@ def initialize(info = {})
OptBool.new('SSL', [false, 'Enable SSL on the LDAP connection', false]),
Msf::OptString.new('DOMAIN', [false, 'The domain to authenticate to']),
Msf::OptString.new('USERNAME', [false, 'The username to authenticate with'], aliases: ['BIND_DN']),
Msf::OptString.new('PASSWORD', [false, 'The password to authenticate with'], aliases: ['BIND_PW']),
OptBool.new('REQUIRE_SIGNING', [true, 'Use signed and encrypted LDAP', true])
Msf::OptString.new('PASSWORD', [false, 'The password to authenticate with'], aliases: ['BIND_PW'])
])

register_advanced_options(
Expand All @@ -42,7 +41,8 @@ def initialize(info = {})
*kerberos_storage_options(protocol: 'LDAP'),
*kerberos_auth_options(protocol: 'LDAP', auth_methods: Msf::Exploit::Remote::AuthOption::LDAP_OPTIONS),
Msf::OptPath.new('LDAP::CertFile', [false, 'The path to the PKCS12 (.pfx) certificate file to authenticate with'], conditions: ['LDAP::Auth', '==', Msf::Exploit::Remote::AuthOption::SCHANNEL]),
OptFloat.new('LDAP::ConnectTimeout', [true, 'Timeout for LDAP connect', 10.0])
OptFloat.new('LDAP::ConnectTimeout', [true, 'Timeout for LDAP connect', 10.0]),
OptEnum.new('LDAP::Signing', [true, 'Use signed and sealed (encrypted) LDAP', 'auto', %w[ disabled auto required ]])
]
)
end
Expand Down Expand Up @@ -80,26 +80,37 @@ def get_connect_opts
password: datastore['PASSWORD'],
domain: datastore['DOMAIN'],
domain_controller_rhost: datastore['DomainControllerRhost'],
should_encrypt: datastore['REQUIRE_SIGNING'],
ldap_auth: datastore['LDAP::Auth'],
ldap_cert_file: datastore['LDAP::CertFile'],
ldap_rhostname: datastore['Ldap::Rhostname'],
ldap_krb_offered_enc_types: datastore['Ldap::KrbOfferedEncryptionTypes'],
ldap_krb5_cname: datastore['Ldap::Krb5Ccname'],
ldap_rhostname: datastore['LDAP::Rhostname'],
ldap_krb_offered_enc_types: datastore['LDAP::KrbOfferedEncryptionTypes'],
ldap_krb5_cname: datastore['LDAP::Krb5Ccname'],
proxies: datastore['Proxies'],
framework_module: self
}
case datastore['LDAP::Signing']
when 'required'
opts[:sign_and_seal] = true
when 'disabled'
opts[:sign_and_seal] = false
end

result = ldap_connect_opts(rhost, rport, datastore['LDAP::ConnectTimeout'], ssl: datastore['SSL'], opts: opts)
begin
result = ldap_connect_opts(rhost, rport, datastore['LDAP::ConnectTimeout'], ssl: datastore['SSL'], opts: opts)
rescue Msf::ValidationError => e
fail_with(Msf::Module::Failure::BadConfig, e.message)
end

# Now that the options have been resolved (including auto possibly resolving to NTLM), check whether this is a valid config
if result[:auth] &&
result[:auth][:method] == :sasl &&
result[:auth][:mechanism] == 'GSS-SPNEGO' &&
datastore['SSL'] &&
datastore['REQUIRE_SIGNING']
# Domain Controllers don't seem to support signing and connection over SSL. Gotta pick one or the other.
fail_with(Msf::Module::Failure::BadConfig, 'SSL not supported with signing. Set either SSL or REQUIRE_SIGNING, but not both.')
if result[:auth] && datastore['LDAP::Signing'] == 'required'
unless (result[:auth][:method] == :sasl && result[:auth][:mechanism] == 'GSS-SPNEGO')
fail_with(Msf::Module::Failure::BadConfig, 'The authentication configuration does not support signing. Change either LDAP::Auth or LDAP::Signing.')
end

if result[:encryption]
# Domain Controllers don't seem to support signing and connection over SSL. Gotta pick one or the other.
fail_with(Msf::Module::Failure::BadConfig, 'SSL not supported with signing. Change either SSL or LDAP::Signing.')
end
end

result
Expand Down Expand Up @@ -257,7 +268,7 @@ def validate_bind_success!(ldap)
fail_with(Msf::Module::Failure::NoTarget, 'Target does not support the simple authentication mechanism!')
when 8
signing_statement = ''
signing_statement = 'May require LDAP signing to be enabled (`set REQUIRE_SIGNING true`). ' unless datastore['REQUIRE_SIGNING']
signing_statement = 'May require LDAP signing to be enabled (`set LDAP::EnableSigning true`). ' unless datastore['LDAP::EnableSigning']

fail_with(Msf::Module::Failure::NoTarget, "Server requires a stronger form of authentication! #{signing_statement}The error was: #{bind_result[:error_message].strip}")
when 14
Expand Down
6 changes: 3 additions & 3 deletions lib/rex/proto/ldap/ldap_exception.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module Rex::Proto::LDAP
class LdapException < RuntimeError
end
end
class LdapException < RuntimeError
end
end
28 changes: 14 additions & 14 deletions lib/rex/proto/sasl.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
module Rex::Proto::Sasl
# Wrap the data in a SASL structure, per RFC 4422 (basically just prepends a big-endian encoded 32-bit integer representing the length)
def wrap_sasl(data)
length = [data.length].pack('N')
# Wrap the data in a SASL structure, per RFC 4422 (basically just prepends a big-endian encoded 32-bit integer representing the length)
def wrap_sasl(data)
length = [data.length].pack('N')

length + data
end

# Unwraps the data from a SASL structure, per RFC 4422
def unwrap_sasl(data)
length = data[0,4].unpack('N')[0]
if length != data.length + 4
raise ArgumentError.new('Invalid SASL structure')
end
length + data
end

data[4,length]
# Unwraps the data from a SASL structure, per RFC 4422
def unwrap_sasl(data)
length = data[0,4].unpack('N')[0]
if length != data.length + 4
raise ArgumentError.new('Invalid SASL structure')
end
end

data[4,length]
end
end

0 comments on commit a9fd264

Please sign in to comment.