Files
metasploit-gs/lib/rex/proto/ldap/server.rb
T
2024-02-01 08:49:16 -06:00

414 lines
14 KiB
Ruby

# -*- coding: binary -*-
require 'rex/socket'
require 'net/ldap'
module Rex
module Proto
module LDAP
class Server
attr_reader :serve_udp, :serve_tcp, :sock_options, :udp_sock, :tcp_sock, :syntax, :ldif
module LdapClient
attr_accessor :authenticated
#
# Initialize LDAP client state
#
def init_ldap_client
self.authenticated = false
end
end
class MockLdapClient
attr_reader :peerhost, :peerport, :srvsock
#
# Create mock LDAP client
#
# @param host [String] PeerHost IP address
# @param port [Fixnum] PeerPort integer
# @param sock [Socket] Connection socket
def initialize(host, port, sock)
@peerhost = host
@peerport = port
@srvsock = sock
end
#
# Test method to prevent GC/ObjectSpace abuse via class lookups
#
def mock_ldap_client?
true
end
def write(data)
srvsock.sendto(data, peerhost, peerport)
end
end
include Rex::IO::GramServer
#
# Create LDAP Server
#
# @param lhost [String] Listener address
# @param lport [Fixnum] Listener port
# @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, auth_provider = nil, ctx = {}, dblock = nil, sblock = nil)
@serve_udp = udp
@serve_tcp = tcp
@sock_options = {
'LocalHost' => lhost,
'LocalPort' => lport,
'Context' => ctx,
'Comm' => comm
}
@ldif = ldif
self.listener_thread = nil
self.dispatch_request_proc = dblock
self.send_response_proc = sblock
@auth_provider = auth_provider
end
#
# Check if server is running
#
def running?
listener_thread and listener_thread.alive?
end
#
# Start the LDAP server
#
def start
if serve_udp
@udp_sock = Rex::Socket::Udp.create(sock_options)
self.listener_thread = Rex::ThreadFactory.spawn('UDPLDAPServerListener', false) do
monitor_listener
end
end
if serve_tcp
@tcp_sock = Rex::Socket::TcpServer.create(sock_options)
tcp_sock.on_client_connect_proc = proc do |cli|
on_client_connect(cli)
end
tcp_sock.on_client_data_proc = proc do |cli|
on_client_data(cli)
end
# Close UDP socket if TCP socket fails
begin
tcp_sock.start
rescue StandardError => e
stop
raise e
end
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
#
# Stop the LDAP server
#
def stop
ensure_close = [udp_sock, tcp_sock].compact
begin
listener_thread.kill if listener_thread.respond_to?(:kill)
self.listener_thread = nil
ensure
while csock = ensure_close.shift
csock.stop if csock.respond_to?(:stop)
csock.close unless csock.respond_to?(:close) && csock.closed?
end
end
end
#
# Process client request, handled with dispatch_request_proc if set
#
# @param cli [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request
# @param data [String] raw LDAP request data
def dispatch_request(cli, data)
if dispatch_request_proc
dispatch_request_proc.call(cli, data)
else
default_dispatch_request(cli, data)
end
end
#
# Default LDAP request dispatcher
#
# @param client [Rex::Socket::Tcp, Rex::Socket::Udp] Client sending the request
# @param data [String] raw LDAP request data
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 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)
client.close
raise e
end
end
#
# 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 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, 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,
tag_sequence.to_ber_appsequence(tag)
].to_ber_sequence
end
#
# 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_result(filter, msgid, attrflt = :all)
if @ldif.nil? || @ldif.empty?
attrs = []
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
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
#
# Returns the hardcore alias for the LDAP service
#
def self.hardcore_alias(*args)
"#{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.
#
def alias
'LDAP Server'
end
protected
#
# This method monitors the listener socket for new connections and calls
# the +on_client_connect+ callback routine.
#
def monitor_listener
loop do
rds = [udp_sock]
wds = []
eds = [udp_sock]
r, = ::IO.select(rds, wds, eds, 1)
next unless (!r.nil? && (r[0] == udp_sock))
buf, host, port = udp_sock.recvfrom(65535)
# Mock up a client object for sending back data
cli = MockLdapClient.new(host, port, r[0])
cli.extend(LdapClient)
cli.init_ldap_client
dispatch_request(cli, buf)
end
end
#
# Processes request coming from client
#
# @param cli [Rex::Socket::Tcp] Client sending request
def on_client_data(cli)
data = cli.read(65535)
raise ::EOFError if !data
raise ::EOFError if data.empty?
dispatch_request(cli, data)
rescue EOFError => e
tcp_sock.close_client(cli) if cli
raise e
end
#
# Extend client for LDAP state
#
def on_client_connect(cli)
cli.extend(LdapClient)
cli.init_ldap_client
end
end
end
end
end