mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-13 06:34:57 +03:00
c47eed6a64
- 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
187 lines
7.1 KiB
Python
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()
|