#!/usr/bin/env python3 """Agent for detecting and testing AD CS ESC1 misconfiguration (enrollee supplies SAN).""" import argparse import json import subprocess from datetime import datetime, timezone def enumerate_certificate_templates(): """Enumerate AD CS certificate templates via certutil or Certipy.""" templates = [] try: result = subprocess.check_output( ["certutil", "-v", "-template"], text=True, errors="replace", timeout=30 ) current = {} for line in result.splitlines(): line = line.strip() if line.startswith("Template["): if current: templates.append(current) current = {"raw_lines": []} if ":" in line: key, _, val = line.partition(":") current[key.strip()] = val.strip() if current: current["raw_lines"].append(line) if current: templates.append(current) except (subprocess.SubprocessError, FileNotFoundError): pass return templates def check_esc1_vulnerable(template): """Check if a certificate template is vulnerable to ESC1.""" indicators = [] raw = "\n".join(template.get("raw_lines", [])) if "CT_FLAG_ENROLLEE_SUPPLIES_SUBJECT" in raw or "ENROLLEE_SUPPLIES_SUBJECT" in raw: indicators.append("Enrollee can supply Subject Alternative Name (SAN)") if "Client Authentication" in raw or "1.3.6.1.5.5.7.3.2" in raw: indicators.append("Template allows Client Authentication EKU") if "Domain Users" in raw or "Authenticated Users" in raw: indicators.append("Low-privileged users can enroll") if "Manager approval" not in raw and "manager approval" not in raw.lower(): indicators.append("No manager approval required") return indicators def run_certipy_find(domain, username, password, dc_ip): """Run Certipy to find vulnerable certificate templates.""" findings = [] try: result = subprocess.check_output( ["certipy", "find", "-u", f"{username}@{domain}", "-p", password, "-dc-ip", dc_ip, "-vulnerable", "-json"], text=True, errors="replace", timeout=60 ) data = json.loads(result) for tmpl in data.get("Certificate Templates", []): if "ESC1" in str(tmpl.get("Vulnerabilities", [])): findings.append({ "template": tmpl.get("Template Name", ""), "vulnerability": "ESC1", "enrollable_by": tmpl.get("Enrollment Rights", []), }) except (subprocess.SubprocessError, json.JSONDecodeError, FileNotFoundError): pass return findings def main(): parser = argparse.ArgumentParser( description="Detect AD CS ESC1 vulnerability (authorized pentesting only)" ) parser.add_argument("--certutil", action="store_true", help="Use certutil enumeration") parser.add_argument("--certipy", action="store_true", help="Use Certipy tool") parser.add_argument("--domain", help="AD domain name") parser.add_argument("--username", help="Domain username") parser.add_argument("--password", help="Domain password") parser.add_argument("--dc-ip", help="Domain controller IP") parser.add_argument("--output", "-o", help="Output JSON report") args = parser.parse_args() print("[*] AD CS ESC1 Vulnerability Detection Agent") print("[!] For authorized security testing only") report = {"timestamp": datetime.now(timezone.utc).isoformat(), "findings": []} if args.certutil: templates = enumerate_certificate_templates() for tmpl in templates: vulns = check_esc1_vulnerable(tmpl) if vulns: report["findings"].append({ "template": tmpl.get("Template", "Unknown"), "esc1_indicators": vulns, "indicator_count": len(vulns), }) print(f"[*] Templates analyzed: {len(templates)}, ESC1 vulnerable: {len(report['findings'])}") if args.certipy and args.domain: certipy_results = run_certipy_find( args.domain, args.username or "", args.password or "", args.dc_ip or "" ) report["findings"].extend(certipy_results) print(f"[*] Certipy findings: {len(certipy_results)}") report["risk_level"] = "CRITICAL" if report["findings"] else "LOW" if args.output: with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"[*] Report saved to {args.output}") else: print(json.dumps(report, indent=2)) if __name__ == "__main__": main()