mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 06:04:56 +03:00
c21af3347e
- Add scripts/agent.py and references/api-reference.md to all remaining skills - Update all 648 LICENSE files: copyright now reads 'Mahipal' - Add implementing-security-monitoring-with-datadog (new skill with full anatomy) - All 649 skills now have: SKILL.md, LICENSE, scripts/agent.py, references/api-reference.md
174 lines
6.9 KiB
Python
174 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Active Directory Penetration Test agent - automates AD enumeration using
|
|
ldap3 for LDAP queries, subprocess for impacket tools, and generates a
|
|
structured pentest findings report."""
|
|
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from ldap3 import Server, Connection, ALL, SUBTREE
|
|
except ImportError:
|
|
print("Install ldap3: pip install ldap3", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def connect_ldap(server_url: str, username: str, password: str, use_ssl: bool = True) -> Connection:
|
|
"""Establish authenticated LDAP connection."""
|
|
srv = Server(server_url, get_info=ALL, use_ssl=use_ssl)
|
|
conn = Connection(srv, user=username, password=password, auto_bind=True)
|
|
return conn
|
|
|
|
|
|
def get_domain_info(conn: Connection) -> dict:
|
|
"""Extract domain functional level and naming context."""
|
|
info = conn.server.info
|
|
return {
|
|
"default_naming_context": info.other.get("defaultNamingContext", [""])[0],
|
|
"forest_functionality": info.other.get("forestFunctionality", [""])[0],
|
|
"domain_functionality": info.other.get("domainFunctionality", [""])[0],
|
|
}
|
|
|
|
|
|
def enumerate_users(conn: Connection, base_dn: str) -> list[dict]:
|
|
"""Enumerate all domain users with security-relevant attributes."""
|
|
conn.search(base_dn, "(&(objectClass=user)(objectCategory=person))",
|
|
search_scope=SUBTREE,
|
|
attributes=["sAMAccountName", "userAccountControl", "adminCount",
|
|
"pwdLastSet", "lastLogon", "servicePrincipalName",
|
|
"memberOf", "description"])
|
|
users = []
|
|
for entry in conn.entries:
|
|
uac = int(str(entry.userAccountControl)) if hasattr(entry, "userAccountControl") else 0
|
|
users.append({
|
|
"username": str(entry.sAMAccountName),
|
|
"admin_count": str(entry.adminCount) if hasattr(entry, "adminCount") else "0",
|
|
"password_not_required": bool(uac & 0x0020),
|
|
"password_never_expires": bool(uac & 0x10000),
|
|
"account_disabled": bool(uac & 0x0002),
|
|
"kerberos_preauth_not_required": bool(uac & 0x400000),
|
|
"has_spn": bool(entry.servicePrincipalName),
|
|
"description": str(entry.description) if hasattr(entry, "description") else "",
|
|
})
|
|
return users
|
|
|
|
|
|
def find_asrep_roastable(users: list[dict]) -> list[dict]:
|
|
"""Identify accounts vulnerable to AS-REP Roasting."""
|
|
findings = []
|
|
for user in users:
|
|
if user["kerberos_preauth_not_required"] and not user["account_disabled"]:
|
|
findings.append({
|
|
"type": "asrep_roastable",
|
|
"severity": "high",
|
|
"account": user["username"],
|
|
"detail": "Kerberos pre-authentication disabled - AS-REP Roasting possible",
|
|
})
|
|
return findings
|
|
|
|
|
|
def find_kerberoastable(users: list[dict]) -> list[dict]:
|
|
"""Identify accounts vulnerable to Kerberoasting."""
|
|
findings = []
|
|
for user in users:
|
|
if user["has_spn"] and not user["account_disabled"]:
|
|
findings.append({
|
|
"type": "kerberoastable",
|
|
"severity": "high",
|
|
"account": user["username"],
|
|
"detail": "User account with SPN set - Kerberoasting possible",
|
|
})
|
|
return findings
|
|
|
|
|
|
def check_password_policy(conn: Connection, base_dn: str) -> list[dict]:
|
|
"""Audit domain password policy."""
|
|
conn.search(base_dn, "(objectClass=domain)", search_scope=SUBTREE,
|
|
attributes=["minPwdLength", "lockoutThreshold", "pwdHistoryLength",
|
|
"maxPwdAge", "minPwdAge", "lockoutDuration"])
|
|
findings = []
|
|
if conn.entries:
|
|
entry = conn.entries[0]
|
|
min_len = int(str(entry.minPwdLength)) if hasattr(entry, "minPwdLength") else 0
|
|
lockout = int(str(entry.lockoutThreshold)) if hasattr(entry, "lockoutThreshold") else 0
|
|
if min_len < 12:
|
|
findings.append({
|
|
"type": "weak_password_policy",
|
|
"severity": "high",
|
|
"detail": f"Minimum password length is {min_len} (recommended: 12+)",
|
|
})
|
|
if lockout == 0:
|
|
findings.append({
|
|
"type": "no_account_lockout",
|
|
"severity": "critical",
|
|
"detail": "No account lockout policy - brute force attacks possible",
|
|
})
|
|
return findings
|
|
|
|
|
|
def run_impacket_getspns(dc_ip: str, domain: str, username: str, password: str) -> dict:
|
|
"""Run impacket GetUserSPNs for Kerberoasting."""
|
|
cmd = ["python3", "-m", "impacket.examples.GetUserSPNs",
|
|
f"{domain}/{username}:{password}", "-dc-ip", dc_ip, "-request"]
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
|
|
return {"success": True, "output": result.stdout[:5000], "errors": result.stderr[:1000]}
|
|
except (subprocess.TimeoutExpired, FileNotFoundError) as e:
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def generate_report(server_url: str, username: str, password: str,
|
|
use_ssl: bool, dc_ip: str = None) -> dict:
|
|
"""Run full AD pentest enumeration and build report."""
|
|
conn = connect_ldap(server_url, username, password, use_ssl)
|
|
domain_info = get_domain_info(conn)
|
|
base_dn = domain_info["default_naming_context"]
|
|
|
|
users = enumerate_users(conn, base_dn)
|
|
findings = []
|
|
findings.extend(find_asrep_roastable(users))
|
|
findings.extend(find_kerberoastable(users))
|
|
findings.extend(check_password_policy(conn, base_dn))
|
|
|
|
conn.unbind()
|
|
|
|
from collections import Counter
|
|
severity_counts = Counter(f["severity"] for f in findings)
|
|
return {
|
|
"report": "ad_penetration_test",
|
|
"generated_at": datetime.utcnow().isoformat() + "Z",
|
|
"domain_info": domain_info,
|
|
"total_users": len(users),
|
|
"total_findings": len(findings),
|
|
"severity_summary": dict(severity_counts),
|
|
"findings": findings,
|
|
}
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="AD Penetration Test Agent")
|
|
parser.add_argument("--server", required=True, help="LDAP server URL (ldaps://dc.example.com)")
|
|
parser.add_argument("--username", required=True, help="Domain username (DOMAIN\\\\user)")
|
|
parser.add_argument("--password", required=True, help="Password")
|
|
parser.add_argument("--no-ssl", action="store_true", help="Disable SSL")
|
|
parser.add_argument("--dc-ip", help="DC IP for impacket tools")
|
|
parser.add_argument("--output", help="Output JSON file path")
|
|
args = parser.parse_args()
|
|
|
|
report = generate_report(args.server, args.username, args.password,
|
|
not args.no_ssl, args.dc_ip)
|
|
output = json.dumps(report, indent=2)
|
|
if args.output:
|
|
Path(args.output).write_text(output, encoding="utf-8")
|
|
print(f"Report written to {args.output}")
|
|
else:
|
|
print(output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|