Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

161 lines
5.9 KiB
Python

#!/usr/bin/env python3
# For authorized testing in lab/CTF environments only
"""Active Directory attack simulation agent using Impacket and ldap3."""
import argparse
import sys
import json
import logging
from datetime import datetime
try:
from impacket.smbconnection import SMBConnection
except ImportError:
sys.exit("impacket is required: pip install impacket")
try:
import ldap3
from ldap3 import Server, Connection, ALL, SUBTREE
except ImportError:
sys.exit("ldap3 is required: pip install ldap3")
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def ldap_enum_users(dc_ip: str, domain: str, username: str, password: str) -> list:
"""Enumerate domain users via LDAP, returning accounts with SPNs and no preauth."""
base_dn = ",".join(f"DC={part}" for part in domain.split("."))
server = Server(dc_ip, get_info=ALL, use_ssl=False)
conn = Connection(server, user=f"{domain}\\{username}", password=password, auto_bind=True)
conn.search(
base_dn,
"(objectClass=user)",
search_scope=SUBTREE,
attributes=[
"sAMAccountName", "servicePrincipalName", "userAccountControl",
"memberOf", "adminCount", "pwdLastSet", "lastLogon",
],
)
users = []
for entry in conn.entries:
uac = int(str(entry.userAccountControl)) if entry.userAccountControl else 0
spn_list = list(entry.servicePrincipalName) if entry.servicePrincipalName else []
no_preauth = bool(uac & 0x400000)
users.append({
"samaccountname": str(entry.sAMAccountName),
"spns": spn_list,
"no_preauth": no_preauth,
"admin_count": str(entry.adminCount) if entry.adminCount else "0",
})
conn.unbind()
logger.info("Enumerated %d domain users via LDAP", len(users))
return users
def find_kerberoastable(users: list) -> list:
"""Filter users with service principal names set (Kerberoastable)."""
targets = [u for u in users if u["spns"]]
logger.info("Found %d Kerberoastable accounts", len(targets))
return targets
def find_asrep_roastable(users: list) -> list:
"""Filter users with Kerberos pre-authentication disabled."""
targets = [u for u in users if u["no_preauth"]]
logger.info("Found %d AS-REP roastable accounts", len(targets))
return targets
def enum_groups(dc_ip: str, domain: str, username: str, password: str) -> dict:
"""Enumerate high-value group memberships via LDAP."""
base_dn = ",".join(f"DC={part}" for part in domain.split("."))
server = Server(dc_ip, get_info=ALL)
conn = Connection(server, user=f"{domain}\\{username}", password=password, auto_bind=True)
high_value_groups = [
"Domain Admins", "Enterprise Admins", "Schema Admins",
"Backup Operators", "Account Operators",
]
results = {}
for group_name in high_value_groups:
conn.search(
base_dn,
f"(&(objectClass=group)(cn={group_name}))",
attributes=["member"],
)
members = []
if conn.entries:
members = list(conn.entries[0].member) if conn.entries[0].member else []
results[group_name] = members
logger.info("Group '%s' has %d members", group_name, len(members))
conn.unbind()
return results
def check_smb_signing(target_ip: str) -> bool:
"""Check if SMB signing is required on the target host."""
try:
smb = SMBConnection(target_ip, target_ip, sess_port=445, timeout=5)
smb.negotiateSession()
signing = smb.isSigningRequired()
smb.close()
return signing
except Exception as exc:
logger.warning("SMB connect failed on %s: %s", target_ip, exc)
return True
def generate_report(users: list, groups: dict, dc_ip: str) -> dict:
"""Compile AD assessment findings into a structured report."""
kerberoastable = find_kerberoastable(users)
asrep = find_asrep_roastable(users)
smb_signing = check_smb_signing(dc_ip)
report = {
"assessment_date": datetime.utcnow().isoformat(),
"total_users": len(users),
"kerberoastable_accounts": [u["samaccountname"] for u in kerberoastable],
"asrep_roastable_accounts": [u["samaccountname"] for u in asrep],
"high_value_groups": {g: len(m) for g, m in groups.items()},
"dc_smb_signing_required": smb_signing,
"risk_summary": [],
}
if kerberoastable:
report["risk_summary"].append(
f"CRITICAL: {len(kerberoastable)} accounts are Kerberoastable"
)
if asrep:
report["risk_summary"].append(
f"HIGH: {len(asrep)} accounts lack Kerberos pre-authentication"
)
if not smb_signing:
report["risk_summary"].append("HIGH: SMB signing not required on DC - relay attacks possible")
return report
def main():
parser = argparse.ArgumentParser(description="AD Attack Simulation Agent")
parser.add_argument("--dc-ip", required=True, help="Domain Controller IP")
parser.add_argument("--domain", required=True, help="Domain FQDN (e.g., corp.local)")
parser.add_argument("--username", required=True, help="Low-privilege domain username")
parser.add_argument("--password", required=True, help="Domain user password")
parser.add_argument("--output", default="ad_assessment.json", help="Output JSON report path")
args = parser.parse_args()
logger.info("Starting AD attack simulation against %s", args.domain)
users = ldap_enum_users(args.dc_ip, args.domain, args.username, args.password)
groups = enum_groups(args.dc_ip, args.domain, args.username, args.password)
report = generate_report(users, groups, args.dc_ip)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
logger.info("Report saved to %s", args.output)
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()