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

187 lines
7.1 KiB
Python

#!/usr/bin/env python3
# For authorized penetration testing and educational environments only.
# Usage against targets without prior mutual consent is illegal.
# It is the end user's responsibility to obey all applicable local, state and federal laws.
"""BloodHound Attack Path Analysis Agent - Queries Neo4j for AD attack paths to Domain Admin."""
import json
import logging
import argparse
from datetime import datetime
from neo4j import GraphDatabase
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def connect_neo4j(uri, username, password):
"""Connect to Neo4j database containing BloodHound data."""
driver = GraphDatabase.driver(uri, auth=(username, password))
driver.verify_connectivity()
logger.info("Connected to Neo4j at %s", uri)
return driver
def find_domain_admins(driver):
"""Find all members of the Domain Admins group."""
query = (
"MATCH (u:User)-[:MemberOf*1..]->(g:Group) "
"WHERE g.name STARTS WITH 'DOMAIN ADMINS' "
"RETURN u.name AS user, u.enabled AS enabled, u.lastlogon AS lastlogon"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d Domain Admin members", len(results))
return results
def find_shortest_paths_to_da(driver, start_user=None):
"""Find shortest attack paths from owned users to Domain Admin."""
if start_user:
query = (
"MATCH p=shortestPath((u:User {name: $user})-[*1..]->(g:Group)) "
"WHERE g.name STARTS WITH 'DOMAIN ADMINS' "
"RETURN p, length(p) AS hops"
)
params = {"user": start_user}
else:
query = (
"MATCH p=shortestPath((u:User {owned: true})-[*1..]->(g:Group)) "
"WHERE g.name STARTS WITH 'DOMAIN ADMINS' "
"RETURN u.name AS start, length(p) AS hops "
"ORDER BY hops ASC LIMIT 20"
)
params = {}
with driver.session() as session:
results = [dict(record) for record in session.run(query, params)]
logger.info("Found %d attack paths to DA", len(results))
return results
def find_kerberoastable_users(driver):
"""Find users with SPNs set (Kerberoastable) that have paths to high-value targets."""
query = (
"MATCH (u:User) WHERE u.hasspn = true AND u.enabled = true "
"RETURN u.name AS user, u.serviceprincipalnames AS spns, "
"u.admincount AS admincount, u.pwdlastset AS pwdlastset"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d Kerberoastable users", len(results))
return results
def find_asrep_roastable(driver):
"""Find users with Kerberos pre-auth disabled (AS-REP Roastable)."""
query = (
"MATCH (u:User) WHERE u.dontreqpreauth = true AND u.enabled = true "
"RETURN u.name AS user, u.enabled AS enabled"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d AS-REP Roastable users", len(results))
return results
def find_unconstrained_delegation(driver):
"""Find computers with unconstrained delegation enabled."""
query = (
"MATCH (c:Computer) WHERE c.unconstraineddelegation = true "
"RETURN c.name AS computer, c.operatingsystem AS os, c.enabled AS enabled"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d unconstrained delegation computers", len(results))
return results
def find_local_admin_paths(driver, target_computer):
"""Find users with local admin rights on a target computer."""
query = (
"MATCH p=(u:User)-[:AdminTo|MemberOf*1..]->(c:Computer {name: $computer}) "
"RETURN u.name AS user, length(p) AS hops "
"ORDER BY hops ASC LIMIT 50"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query, {"computer": target_computer})]
logger.info("Found %d users with admin access to %s", len(results), target_computer)
return results
def find_gpo_attack_paths(driver):
"""Find GPO-based attack paths that could lead to privilege escalation."""
query = (
"MATCH (g:GPO)-[:GpLink]->(ou:OU)-[:Contains*1..]->(c:Computer) "
"MATCH (u:User)-[:GenericAll|GenericWrite|WriteOwner|WriteDacl]->(g) "
"WHERE u.enabled = true "
"RETURN u.name AS user, g.name AS gpo, c.name AS affected_computer "
"LIMIT 50"
)
with driver.session() as session:
results = [dict(record) for record in session.run(query)]
logger.info("Found %d GPO attack paths", len(results))
return results
def assess_ad_risk(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths):
"""Calculate overall AD security risk score."""
score = 0
if len(paths) > 0:
score += 30
if len(kerberoastable) > 5:
score += 20
if len(asrep) > 0:
score += 15
if len(unconstrained) > 1:
score += 15
if len(gpo_paths) > 0:
score += 20
risk = "Critical" if score >= 60 else "High" if score >= 40 else "Medium" if score >= 20 else "Low"
return {"score": score, "risk_level": risk}
def generate_report(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths, risk):
"""Generate BloodHound analysis report."""
report = {
"timestamp": datetime.utcnow().isoformat(),
"domain_admins": da_members,
"attack_paths_to_da": paths[:20],
"kerberoastable_users": kerberoastable,
"asrep_roastable": asrep,
"unconstrained_delegation": unconstrained,
"gpo_attack_paths": gpo_paths[:20],
"risk_assessment": risk,
}
print(f"BLOODHOUND REPORT: Risk={risk['risk_level']} Score={risk['score']}")
return report
def main():
parser = argparse.ArgumentParser(description="BloodHound Attack Path Analysis Agent")
parser.add_argument("--neo4j-uri", default="bolt://localhost:7687")
parser.add_argument("--neo4j-user", default="neo4j")
parser.add_argument("--neo4j-password", required=True)
parser.add_argument("--start-user", help="Specific user to find paths from")
parser.add_argument("--output", default="bloodhound_report.json")
args = parser.parse_args()
driver = connect_neo4j(args.neo4j_uri, args.neo4j_user, args.neo4j_password)
da_members = find_domain_admins(driver)
paths = find_shortest_paths_to_da(driver, args.start_user)
kerberoastable = find_kerberoastable_users(driver)
asrep = find_asrep_roastable(driver)
unconstrained = find_unconstrained_delegation(driver)
gpo_paths = find_gpo_attack_paths(driver)
risk = assess_ad_risk(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths)
report = generate_report(da_members, paths, kerberoastable, asrep, unconstrained, gpo_paths, risk)
driver.close()
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()