diff --git a/max.py b/max.py index 06bc60c..060e5f1 100644 --- a/max.py +++ b/max.py @@ -210,7 +210,7 @@ def get_info(args): "columns" : ["ObjectName","SID","DomainName","ForeignObjectName"] }, "unsupos" : { - "query" : "MATCH (c:Computer) WHERE toLower(c.operatingsystem) =~ '.*(2000|2003|2008|xp|vista| 7 |me).*' RETURN c.name,c.operatingsystem", + "query" : "MATCH (c:Computer) WHERE toLower(c.operatingsystem) =~ '.*(2000|2003|2008|xp|vista| 7 |me).*' RETURN c.name,c.operatingsystem", "columns" : ["ComputerName","OperatingSystem"] }, "foreignprivs" : { @@ -240,7 +240,30 @@ def get_info(args): "ownedadmins" : { "query": "match (u:User {owned: True})-[r:AdminTo|MemberOf*1..]->(c:Computer) return c.name, \"AdministratedBy\", u.name order by c, u", "columns": ["ComputerName", "HasAdmin", "UserName"] - } + }, + "sccm_objects": { + "query": """ + MATCH (n) + WHERE toLower(n.name) CONTAINS toLower('SCCM') + WITH n, + CASE + WHEN 'User' IN labels(n) THEN 'User' + WHEN 'Computer' IN labels(n) THEN 'Computer' + WHEN 'Group' IN labels(n) THEN 'Group' + WHEN 'Domain' IN labels(n) THEN 'Domain' + WHEN 'OU' IN labels(n) THEN 'Organizational Unit' + WHEN 'GPO' IN labels(n) THEN 'Group Policy Object' + ELSE 'Other' + END AS ObjectType + RETURN + n.name AS ObjectName, + ObjectType, + n.HostName AS DNSHostname, + n.description AS Description + ORDER BY ObjectType, ObjectName + """, + "columns": ["ObjectName", "ObjectType", "DNSHostname", "Description"] + }, } query = "" @@ -354,6 +377,10 @@ def get_info(args): query = queries["ownedpaths"]["query"] cols = queries["ownedpaths"]["columns"] data_format = "graph" + elif (args.sccm_objects): + query = queries["sccm_objects"] + cols = queries["ObjectName", "ObjectType", "DNSHostname", "Description"] + data_format = "graph" if args.getnote: query = query + ",n.notes" @@ -382,6 +409,272 @@ def get_info(args): for entry in entry_list: print(get_query_output(entry,args.delimeter,cols_len=len(cols))) +def run_adcs(args): + + queries = { + "esc1": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Certificate Template' + AND n.`Enrollee Supplies Subject` = true + AND n.`Client Authentication` = true + AND n.Enabled = true + RETURN n.name AS TemplateName + """, + "columns": ["TemplateName"] + }, + "esc2": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Certificate Template' + AND n.Enabled = true + AND ( + n.`Extended Key Usage` = [] + OR 'Any Purpose' IN n.`Extended Key Usage` + OR n.`Any Purpose` = true + ) + RETURN n.name AS TemplateName + """, + "columns": ["TemplateName"] + }, + "esc3": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Certificate Template' + AND n.Enabled = true + AND ( + n.`Extended Key Usage` = [] + OR 'Any Purpose' IN n.`Extended Key Usage` + OR 'Certificate Request Agent' IN n.`Extended Key Usage` + OR n.`Any Purpose` = true + ) + RETURN n.name AS TemplateName + """, + "columns": ["TemplateName"] + }, + "esc4_path": { + "query": """ + MATCH p = shortestPath((g)-[:GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner*1..]->(n:GPO)) + WHERE g <> n + AND n.type = 'Certificate Template' + AND n.Enabled = true + RETURN p + """, + "columns": ["Path"], + "data_format": "graph" + }, + "esc4_owned_path": { + "query": """ + MATCH p = allShortestPaths((g {owned: true})-[r*1..]->(n:GPO)) + WHERE g <> n + AND n.type = 'Certificate Template' + AND n.Enabled = true + AND NONE(x IN relationships(p) WHERE type(x) = 'Enroll' OR type(x) = 'AutoEnroll') + RETURN p + """, + "columns": ["Path"], + "data_format": "graph" + }, + "esc6": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Enrollment Service' + AND n.`User Specified SAN` = 'Enabled' + RETURN n.name AS CAName + """, + "columns": ["CAName"] + }, + "esc7_path": { + "query": """ + MATCH p = shortestPath((g)-[r:GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ManageCa|ManageCertificates*1..]->(n:GPO)) + WHERE g <> n + AND n.type = 'Enrollment Service' + RETURN p + """, + "columns": ["Path"], + "data_format": "graph" + }, + "esc7_owned_path": { + "query": """ + MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) + WHERE g <> n + AND n.type = 'Enrollment Service' + AND NONE(x IN relationships(p) WHERE type(x) = 'Enroll' OR type(x) = 'AutoEnroll') + RETURN p + """, + "columns": ["Path"], + "data_format": "graph" + }, + "esc8": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Enrollment Service' + AND n.`Web Enrollment` = 'Enabled' + RETURN n.name AS CAName + """, + "columns": ["CAName"] + }, + "esc9": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Certificate Template' + AND 'NoSecurityExtension' IN n.`Enrollment Flag` + AND n.Enabled = true + RETURN n.name AS TemplateName + """, + "columns": ["TemplateName"] + }, + "esc9_owned_path": { + "query": """ + MATCH p = allShortestPaths((g {owned: true})-[r*1..]->(n:GPO)) + WHERE n.type = 'Certificate Template' + AND g <> n + AND 'NoSecurityExtension' IN n.`Enrollment Flag` + AND n.Enabled = true + AND NONE(rel IN r WHERE type(rel) IN ['EnabledBy', 'Read', 'ManageCa', 'ManageCertificates']) + RETURN p + """, + "columns": ["Path"], + "data_format": "graph" + }, + "cas": { + "query": """ + MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name AS CAName, n.`DNS Name` AS DNSHostname + """, + "columns": ["CAName", "DNSHostname"], + }, + "templates": { + "query": """ + MATCH (n:GPO) WHERE n.type = 'Certificate Template' and n.Enabled = true RETURN n.name AS TemplateName + """, + "columns": ["TemplateName"], + }, + "esc1_owned_path": { + "query": """ + MATCH (u:User {owned: true})-[:MemberOf*0..]->(principal) + MATCH (principal)-[:Enroll|AutoEnroll]->(t:GPO {type: 'Certificate Template'}) + WHERE + t.`Enrollee Supplies Subject` = true AND + t.`Client Authentication` = true AND + t.Enabled = true + RETURN DISTINCT + t.name AS TemplateName, + t.description AS TemplateDescription + ORDER BY TemplateName + """, + "columns": ["TemplateName", "TemplateDescription"] + }, + "enrollment_rights": { + "query": """ + MATCH (principal)-[r:Enroll|AutoEnroll]->(template:GPO {type: 'Certificate Template'}) + RETURN + template.name AS TemplateName, + labels(principal) AS PrincipalLabels, + principal.name AS PrincipalName, + type(r) AS EnrollmentRight + ORDER BY TemplateName, PrincipalName + """, + "columns": ["TemplateName", "PrincipalLabels", "PrincipalName", "EnrollmentRight"] + }, + "vulnerable_templates": { + "query": """ + MATCH (t:GPO {type: 'Certificate Template'}) + WHERE t.Enabled = true + WITH t, + CASE + WHEN t.`Enrollee Supplies Subject` = true AND t.`Client Authentication` = true THEN 'ESC1' + ELSE null + END AS esc1, + CASE + WHEN t.`Extended Key Usage` IS NULL OR size(t.`Extended Key Usage`) = 0 OR 'Any Purpose' IN t.`Extended Key Usage` OR t.`Any Purpose` = true THEN 'ESC2' + ELSE null + END AS esc2, + CASE + WHEN t.`Extended Key Usage` IS NULL OR size(t.`Extended Key Usage`) = 0 OR 'Any Purpose' IN t.`Extended Key Usage` OR t.`Any Purpose` = true OR 'Certificate Request Agent' IN t.`Extended Key Usage` THEN 'ESC3' + ELSE null + END AS esc3, + CASE + WHEN 'NoSecurityExtension' IN t.`Enrollment Flag` THEN 'ESC9' + ELSE null + END AS esc9 + WITH t, [esc1, esc2, esc3, esc9] AS esc_list + WITH t, [esc IN esc_list WHERE esc IS NOT NULL] AS Vulnerabilities + WHERE size(Vulnerabilities) > 0 + RETURN + t.name AS TemplateName, + t.description AS TemplateDescription, + Vulnerabilities + ORDER BY TemplateName + """, + "columns": ["TemplateName", "TemplateDescription", "Vulnerabilities"] + }, + } + + # Determine which query to execute based on args + selected_query = None + + if args.esc1: + selected_query = queries["esc1"] + elif args.esc2: + selected_query = queries["esc2"] + elif args.esc3: + selected_query = queries["esc3"] + elif args.esc4_path: + selected_query = queries["esc4_path"] + elif args.esc4_owned_path: + selected_query = queries["esc4_owned_path"] + elif args.esc6: + selected_query = queries["esc6"] + elif args.esc7_path: + selected_query = queries["esc7_path"] + elif args.esc7_owned_path: + selected_query = queries["esc7_owned_path"] + elif args.esc8: + selected_query = queries["esc8"] + elif args.esc9: + selected_query = queries["esc9"] + elif args.esc9_owned_path: + selected_query = queries["esc9_owned_path"] + elif args.cas: + selected_query = queries["cas"] + elif args.templates: + selected_query = queries["templates"] + elif args.esc1_owned_path: + selected_query = queries["esc1_owned_path"] + elif args.enrollment_rights: + selected_query = queries["enrollment_rights"] + elif args.vulnerable_templates: + selected_query = queries["vulnerable_templates"] + else: + print("No valid ADCS option selected.") + return + + query = selected_query["query"] + cols = selected_query["columns"] + data_format = selected_query.get("data_format", "row") + + if args.getnote: + query = query + ", n.notes" + cols.append("Notes") + + r = do_query(args, query, data_format=data_format) + x = json.loads(r.text) + entry_list = x["results"][0]["data"] + + if len(entry_list) == 0: + print("No results found.") + else: + if data_format == "graph": + for entry in entry_list: + output = get_query_output(entry, args.delimeter, path=True) + print(output) + else: + if args.label: + print(" - ".join(cols)) + for entry in entry_list: + output = get_query_output(entry, args.delimeter, cols_len=len(cols)) + print(output) + def mark_owned(args): @@ -1456,6 +1749,7 @@ def write_html_report(self, filebase, filename): print("") + def pet_max(): messages = [ @@ -1468,7 +1762,8 @@ def pet_max(): "Hack the planet!", "10/10 would pet - @blurbdust", "dogsay > cowsay - @b1gbroth3r", - "much query, very sniff - @vexance" + "much query, very sniff - @vexance", + "i strangled the Metasploit goose - @ajm4n" ] max = """ @@ -1519,8 +1814,6 @@ def main(): addspw = switch.add_parser("add-spw",help="Create 'SharesPasswordWith' relationships with targets from a file. Adds edge indicating two objects share a password (repeated local administrator)") dpat = switch.add_parser("dpat",help="BloodHound Domain Password Audit Tool, run cracked user-password analysis tied with BloodHound through a Hashcat potfile & NTDS") petmax = switch.add_parser("pet-max",help="Pet max, hes a good boy (pet me again, I say different things)") - - # GETINFO function parameters getinfo_switch = getinfo.add_mutually_exclusive_group(required=True) getinfo_switch.add_argument("--users",dest="users",default=False,action="store_true",help="Return a list of all domain users") getinfo_switch.add_argument("--comps",dest="comps",default=False,action="store_true",help="Return a list of all domain computers") @@ -1555,12 +1848,39 @@ def main(): getinfo_switch.add_argument("--hvt-paths",dest="hvtpaths",default="",help="Return all paths from the input node to HVTs") getinfo_switch.add_argument("--owned-paths",dest="ownedpaths",default=False,action="store_true",help="Return all paths from owned objects to HVTs") getinfo_switch.add_argument("--owned-admins", dest="ownedadmins",default=False,action="store_true",help="Return all computers owned users are admins to") - + getinfo_switch.add_argument("--sccm-objects", dest="sccm_objects", default=False, action="store_true", help="Return all domain objects with 'SCCM' in the name, along with their object type, DNS hostname, and description") + getinfo.add_argument("--get-note",dest="getnote",default=False,action="store_true",help="Optional, return the \"notes\" attribute for whatever objects are returned") getinfo.add_argument("-l",dest="label",action="store_true",default=False,help="Optional, apply labels to the columns returned") getinfo.add_argument("-e","--enabled",dest="enabled",action="store_true",default=False,help="Optional, only return enabled domain users (only works for --users and --passnotreq flags as of now)") getinfo.add_argument("-d", "--delim",dest="delimeter", default="-", required=False, help="Flag to specify output delimeter between attributes (default '-')") + # adcs command + adcs_parser = switch.add_parser("adcs", help="Run AD CS ESC attack detection queries") + adcs_switch = adcs_parser.add_mutually_exclusive_group(required=True) + adcs_switch.add_argument("--esc1", dest="esc1", default=False, action="store_true", help="Find Misconfigured Certificate Templates (ESC1)") + adcs_switch.add_argument("--esc1-owned-path", dest="esc1_owned_path", default=False, action="store_true", help="Find Misconfigured Certificate Templates (ESC1) from Owned Principals") + adcs_switch.add_argument("--esc2", dest="esc2", default=False, action="store_true", help="Find Misconfigured Certificate Templates (ESC2)") + adcs_switch.add_argument("--esc3", dest="esc3", default=False, action="store_true", help="Find Enrollment Agent Templates (ESC3)") + adcs_switch.add_argument("--esc4-path", dest="esc4_path", default=False, action="store_true", help="Shortest Paths to Vulnerable Certificate Template Access Control (ESC4)") + adcs_switch.add_argument("--esc4-owned-path", dest="esc4_owned_path", default=False, action="store_true", help="Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)") + adcs_switch.add_argument("--esc6", dest="esc6", default=False, action="store_true", help="Find Certificate Authorities with User Specified SAN (ESC6)") + adcs_switch.add_argument("--esc7-path", dest="esc7_path", default=False, action="store_true", help="Shortest Paths to Vulnerable Certificate Authority Access Control (ESC7)") + adcs_switch.add_argument("--esc7-owned-path", dest="esc7_owned_path", default=False, action="store_true", help="Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)") + adcs_switch.add_argument("--esc8", dest="esc8", default=False, action="store_true", help="Find Certificate Authorities with HTTP Web Enrollment (ESC8)") + adcs_switch.add_argument("--esc9", dest="esc9", default=False, action="store_true", help="Find Unsecured Certificate Templates (ESC9)") + adcs_switch.add_argument("--esc9-owned-path", dest="esc9_owned_path", default=False, action="store_true", help="Shortest Paths to Unsecured Certificate Templates from Owned Principals (ESC9)") + adcs_switch.add_argument("--cas", dest="cas", default=False, action="store_true", help="List all Certificate Authorities") + adcs_switch.add_argument("--templates", dest="templates", default=False, action="store_true", help="List all enabled Certificate Templates") + adcs_switch.add_argument("--enrollment-rights", dest="enrollment_rights", default=False, action="store_true", help="List enrollment rights for all Certificate Templates") + adcs_switch.add_argument("--vulnerable-templates", dest="vulnerable_templates", default=False, action="store_true", help="List all vulnerable Certificate Templates") + + adcs_parser.add_argument("--get-note", dest="getnote", default=False, action="store_true", help="Optional, return the 'notes' attribute for whatever objects are returned") + adcs_parser.add_argument("-l", dest="label", action="store_true", default=False, help="Optional, apply labels to the columns returned") + adcs_parser.add_argument("-d", "--delim", dest="delimeter", default="-", required=False, help="Flag to specify output delimiter between attributes (default '-')") + + + # MARKOWNED function paramters markowned.add_argument("-f","--file",dest="filename",default="",required=False,help="Filename containing AD objects (must have FQDN attached)") markowned.add_argument("--add-note",dest="notes",default="",help="Notes to add to all marked objects (method of compromise)") @@ -1611,6 +1931,7 @@ def main(): dpat.add_argument("--own-cracked", dest="own_cracked", action="store_true", required=False, help="Mark all users with cracked passwords as owned") dpat.add_argument("--add-crack-note",dest="add_crack_note",action="store_true",required=False,help="Add a note to cracked users indicating they have been cracked") + args = parser.parse_args() @@ -1653,6 +1974,8 @@ def main(): dpat_func(args) elif args.command == "pet-max": pet_max() + elif args.command == "adcs": + run_adcs(args) # else: # print("Error: use a module or use -h/--help to see help")