From d3aa8f0dcf20a0595f6012df3004a900121456e1 Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Sun, 15 Sep 2024 17:57:36 -0400 Subject: [PATCH 1/7] added ADCS --- max.py | 240 +++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 225 insertions(+), 15 deletions(-) diff --git a/max.py b/max.py index 06bc60c..cf985ef 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 c.operatingsystem =~ '.*(2000|2003|2008|xp|vista| 7 |me).*' RETURN c.name,c.operatingsystem", "columns" : ["ComputerName","OperatingSystem"] }, "foreignprivs" : { @@ -243,6 +243,214 @@ def get_info(args): } } + + if args.adcs: + queries.update({ + "Find all Certificate Templates": { + "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' RETURN n.name AS TemplateName, n", + "columns": ["TemplateName", "Node"] + }, + "Find enabled Certificate Templates": { + "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' AND n.Enabled = true RETURN n.name AS TemplateName, n", + "columns": ["TemplateName", "Node"] + }, + "Find Certificate Authorities": { + "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name AS CAName, n", + "columns": ["CAName", "Node"] + }, + "Find Misconfigured Certificate Templates (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, n + """, + "columns": ["TemplateName", "Node"] + }, + "Find Misconfigured Certificate Templates (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, n + """, + "columns": ["TemplateName", "Node"] + }, + "Find Enrollment Agent Templates (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, n + """, + "columns": ["TemplateName", "Node"] + }, + "Find Certificate Authorities with User Specified SAN (ESC6)": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Enrollment Service' + AND n.`User Specified SAN` = 'Enabled' + RETURN n.name AS CAName, n + """, + "columns": ["CAName", "Node"] + }, + "Find Certificate Authorities with HTTP Web Enrollment (ESC8)": { + "query": """ + MATCH (n:GPO) + WHERE n.type = 'Enrollment Service' + AND n.`Web Enrollment` = 'Enabled' + RETURN n.name AS CAName, n + """, + "columns": ["CAName", "Node"] + }, + "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)": { + "query": """ + MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) + WHERE g <> n + AND n.type = 'Certificate Template' + AND n.`Enrollee Supplies Subject` = true + AND n.`Client Authentication` = true + AND n.Enabled = true + RETURN p + """, + "columns": ["Path"] + }, + "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC2)": { + "query": """ + MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) + WHERE g <> n + AND 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 p + """, + "columns": ["Path"] + }, + "Shortest Paths to Enrollment Agent Templates from Owned Principals (ESC3)": { + "query": """ + MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) + WHERE g <> n + AND 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 + OR 'Certificate Request Agent' IN n.`Extended Key Usage` + ) + RETURN p + """, + "columns": ["Path"] + }, + "Shortest Paths to Vulnerable Certificate Template Access Control (ESC4)": { + "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"] + }, + "Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)": { + "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"] + }, + "Shortest Paths to Vulnerable Certificate Authority Access Control (ESC7)": { + "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"] + }, + "Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)": { + "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"] + }, + "Shortest Paths to Unsecured Certificate Templates from Owned Principals (ESC9)": { + "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"] + }, + "Find Unsecured Certificate Templates (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, n + """, + "columns": ["TemplateName", "Node"] + }, + }) + + query = "" + cols = [] + data_format = "row" + + if args.adcs: + for query_name, query_info in queries.items(): + if query_name.startswith("Find") or query_name.startswith("Shortest Paths"): + query = query_info["query"] + cols = query_info["columns"] + 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(f"No results found for {query_name}") + else: + print(f"Results for {query_name}:") + if args.label: + print(" - ".join(cols)) + for entry in entry_list: + output = get_query_output(entry, args.delimeter, cols_len=len(cols)) + print(output) + print("\n") + + + query = "" cols = [] data_format = "row" @@ -900,31 +1108,31 @@ def dpat_func(args): "label" : "Enabled User Accounts Cracked" }, { - 'query' : "match p = (k:Group)<-[:MemberOf*1..]-(m) where k.highvalue = true WITH [ n in nodes(p) WHERE n:User] as ulist UNWIND (ulist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "MATCH p=(u:User {cracked:true})-[r:MemberOf*1..]->(g:Group {highvalue:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "High Value User Accounts Cracked" }, { - 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-512' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", + 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-512' MATCH (u:User)-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", 'label' : "Domain Admin Members" }, { - 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-512' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-512' MATCH (u:User {cracked:true})-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Domain Admin Members Cracked" }, { - 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-519' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", + 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-519' MATCH (u:User)-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", 'label' : "Enterprise Admin Members" }, { - 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-519' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-519' MATCH (u:User {cracked:true})-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Enterprise Admin Accounts Cracked" }, { - 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-544' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", + 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-544' MATCH (u:User)-[r:MemberOf]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", 'label' : "Administrator Group Members" }, { - 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-544' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-544' MATCH (u:User {cracked:true})-[r:MemberOf]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Administrator Group Member Accounts Cracked" }, { @@ -955,11 +1163,11 @@ def dpat_func(args): intense_queries = [ { - "query" : "match k = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid ENDS WITH '-516' AND NOT (n = m) with [c in nodes(k) WHERE c:Computer] as dcs match p = shortestPath((n)-[:HasSession|AdminTo|Contains|AZLogicAppContributor*1..]->(m {unconstraineddelegation: true})) where not (n = m) AND NOT ( m IN dcs ) with [ n IN nodes(p) WHERE n:User] as ulist UNWIND ulist as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + "query" : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-516' MATCH (c:Computer)-[MemberOf]->(g) WITH COLLECT(c) AS dcs MATCH (u:User {cracked:true}),(n {unconstraineddelegation:true}),p=shortestPath((u)-[r*1..]->(n)) WHERE NOT n IN dcs AND NONE (r IN relationships(p) WHERE type(r)= 'GetChanges') AND NONE (r in relationships(p) WHERE type(r)='GetChangesAll') AND NOT u=n RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash,n.name", "label" : "Accounts With Paths To Unconstrained Delegation Objects Cracked (Excluding DCs)" }, { - "query" : "match p = shortestPath((u)-[*1..]->(n)) where n.highvalue = true AND u <> n WITH [n in nodes(p) WHERE n:User] as ulist UNWIND(ulist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + "query" : "MATCH (u:User {cracked:true}),(n {highvalue:true}),p=shortestPath((u)-[r*1..]->(n)) WHERE NONE (r IN relationships(p) WHERE type(r)= 'GetChanges') AND NONE (r in relationships(p) WHERE type(r)='GetChangesAll') AND NOT u=n RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", "label" : "Accounts With Paths To High Value Targets Cracked" }, { @@ -975,7 +1183,7 @@ def dpat_func(args): "label" : "Accounts With Explicit Controlling Privileges Cracked" }, { - "query" : "MATCH p2=(n)-[r1:MemberOf*1..]->(g:Group)-[r2:AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|CanRDP|CanPSRemote|ExecuteDCOM|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory]->(n2) WITH [u in nodes(p2) WHERE u:User] AS ulist UNWIND(ulist) AS u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + "query" : "MATCH p2=(u:User {cracked:true})-[r1:MemberOf*1..]->(g:Group)-[r2:AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|CanRDP|CanPSRemote|ExecuteDCOM|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory]->(n2) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", "label" : "Accounts With Group Delegated Controlling Privileges Cracked" } ] @@ -1468,7 +1676,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 +1728,7 @@ 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 + adcs = switch.add_parser("adcs",help="adcs") 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,7 +1763,8 @@ 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("--adcs", dest="adcs", default=False, action="store_true", help="Perform AD CS ESC attack detection queries") + 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)") @@ -1659,3 +1868,4 @@ def main(): if __name__ == "__main__": main() + From e69b991a43e5aa2e888ee20f37b093c0b3dc5978 Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Mon, 16 Sep 2024 12:51:36 -0400 Subject: [PATCH 2/7] reverted changes & broke out adcs --- max.py | 425 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 212 insertions(+), 213 deletions(-) diff --git a/max.py b/max.py index cf985ef..dfe2b5b 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 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" : { @@ -243,214 +243,6 @@ def get_info(args): } } - - if args.adcs: - queries.update({ - "Find all Certificate Templates": { - "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' RETURN n.name AS TemplateName, n", - "columns": ["TemplateName", "Node"] - }, - "Find enabled Certificate Templates": { - "query": "MATCH (n:GPO) WHERE n.type = 'Certificate Template' AND n.Enabled = true RETURN n.name AS TemplateName, n", - "columns": ["TemplateName", "Node"] - }, - "Find Certificate Authorities": { - "query": "MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name AS CAName, n", - "columns": ["CAName", "Node"] - }, - "Find Misconfigured Certificate Templates (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, n - """, - "columns": ["TemplateName", "Node"] - }, - "Find Misconfigured Certificate Templates (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, n - """, - "columns": ["TemplateName", "Node"] - }, - "Find Enrollment Agent Templates (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, n - """, - "columns": ["TemplateName", "Node"] - }, - "Find Certificate Authorities with User Specified SAN (ESC6)": { - "query": """ - MATCH (n:GPO) - WHERE n.type = 'Enrollment Service' - AND n.`User Specified SAN` = 'Enabled' - RETURN n.name AS CAName, n - """, - "columns": ["CAName", "Node"] - }, - "Find Certificate Authorities with HTTP Web Enrollment (ESC8)": { - "query": """ - MATCH (n:GPO) - WHERE n.type = 'Enrollment Service' - AND n.`Web Enrollment` = 'Enabled' - RETURN n.name AS CAName, n - """, - "columns": ["CAName", "Node"] - }, - "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC1)": { - "query": """ - MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) - WHERE g <> n - AND n.type = 'Certificate Template' - AND n.`Enrollee Supplies Subject` = true - AND n.`Client Authentication` = true - AND n.Enabled = true - RETURN p - """, - "columns": ["Path"] - }, - "Shortest Paths to Misconfigured Certificate Templates from Owned Principals (ESC2)": { - "query": """ - MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) - WHERE g <> n - AND 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 p - """, - "columns": ["Path"] - }, - "Shortest Paths to Enrollment Agent Templates from Owned Principals (ESC3)": { - "query": """ - MATCH p = allShortestPaths((g {owned: true})-[*1..]->(n:GPO)) - WHERE g <> n - AND 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 - OR 'Certificate Request Agent' IN n.`Extended Key Usage` - ) - RETURN p - """, - "columns": ["Path"] - }, - "Shortest Paths to Vulnerable Certificate Template Access Control (ESC4)": { - "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"] - }, - "Shortest Paths to Vulnerable Certificate Template Access Control from Owned Principals (ESC4)": { - "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"] - }, - "Shortest Paths to Vulnerable Certificate Authority Access Control (ESC7)": { - "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"] - }, - "Shortest Paths to Vulnerable Certificate Authority Access Control from Owned Principals (ESC7)": { - "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"] - }, - "Shortest Paths to Unsecured Certificate Templates from Owned Principals (ESC9)": { - "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"] - }, - "Find Unsecured Certificate Templates (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, n - """, - "columns": ["TemplateName", "Node"] - }, - }) - - query = "" - cols = [] - data_format = "row" - - if args.adcs: - for query_name, query_info in queries.items(): - if query_name.startswith("Find") or query_name.startswith("Shortest Paths"): - query = query_info["query"] - cols = query_info["columns"] - 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(f"No results found for {query_name}") - else: - print(f"Results for {query_name}:") - if args.label: - print(" - ".join(cols)) - for entry in entry_list: - output = get_query_output(entry, args.delimeter, cols_len=len(cols)) - print(output) - print("\n") - - - query = "" cols = [] data_format = "row" @@ -590,6 +382,191 @@ 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" + } + } + + # 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"] + else: + print("No valid ESC 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): @@ -1116,7 +1093,7 @@ def dpat_func(args): 'label' : "Domain Admin Members" }, { - 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-512' MATCH (u:User {cracked:true})-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "match p = (k:Group)<-[:MemberOf*1..]-(m) where k.highvalue = true WITH [ n in nodes(p) WHERE n:User] as ulist UNWIND (ulist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Domain Admin Members Cracked" }, { @@ -1728,7 +1705,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)") - adcs = switch.add_parser("adcs",help="adcs") 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") @@ -1763,13 +1739,34 @@ 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("--adcs", dest="adcs", default=False, action="store_true", help="Perform AD CS ESC attack detection queries") + 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("--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_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)") @@ -1820,6 +1817,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() @@ -1862,10 +1860,11 @@ 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") if __name__ == "__main__": main() - From 72fe6fdc54594eec66c43ef83660962f2727ae24 Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Mon, 16 Sep 2024 13:56:07 -0400 Subject: [PATCH 3/7] reversed dpat --- max.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/max.py b/max.py index dfe2b5b..487e9ef 100644 --- a/max.py +++ b/max.py @@ -1085,31 +1085,31 @@ def dpat_func(args): "label" : "Enabled User Accounts Cracked" }, { - 'query' : "MATCH p=(u:User {cracked:true})-[r:MemberOf*1..]->(g:Group {highvalue:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "match p = (k:Group)<-[:MemberOf*1..]-(m) where k.highvalue = true WITH [ n in nodes(p) WHERE n:User] as ulist UNWIND (ulist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "High Value User Accounts Cracked" }, { - 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-512' MATCH (u:User)-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", + 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-512' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", 'label' : "Domain Admin Members" }, { - 'query' : "match p = (k:Group)<-[:MemberOf*1..]-(m) where k.highvalue = true WITH [ n in nodes(p) WHERE n:User] as ulist UNWIND (ulist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-512' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Domain Admin Members Cracked" }, { - 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-519' MATCH (u:User)-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", + 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-519' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", 'label' : "Enterprise Admin Members" }, { - 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-519' MATCH (u:User {cracked:true})-[r:MemberOf*1..]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-519' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Enterprise Admin Accounts Cracked" }, { - 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-544' MATCH (u:User)-[r:MemberOf]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", + 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-544' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u RETURN DISTINCT u.enabled,u.ntds_uname,u.nt_hash,u.password", 'label' : "Administrator Group Members" }, { - 'query' : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-544' MATCH (u:User {cracked:true})-[r:MemberOf]->(g) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + 'query' : "match p = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid =~ '(?i)S-1-5-.*-544' with [ n IN nodes(p) WHERE n:User] as dalist unwind (dalist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", 'label' : "Administrator Group Member Accounts Cracked" }, { @@ -1140,11 +1140,11 @@ def dpat_func(args): intense_queries = [ { - "query" : "MATCH (g:Group) WHERE g.objectid ENDS WITH '-516' MATCH (c:Computer)-[MemberOf]->(g) WITH COLLECT(c) AS dcs MATCH (u:User {cracked:true}),(n {unconstraineddelegation:true}),p=shortestPath((u)-[r*1..]->(n)) WHERE NOT n IN dcs AND NONE (r IN relationships(p) WHERE type(r)= 'GetChanges') AND NONE (r in relationships(p) WHERE type(r)='GetChangesAll') AND NOT u=n RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash,n.name", + "query" : "match k = (n:Group)<-[:MemberOf*1..]-(m) where n.objectid ENDS WITH '-516' AND NOT (n = m) with [c in nodes(k) WHERE c:Computer] as dcs match p = shortestPath((n)-[:HasSession|AdminTo|Contains|AZLogicAppContributor*1..]->(m {unconstraineddelegation: true})) where not (n = m) AND NOT ( m IN dcs ) with [ n IN nodes(p) WHERE n:User] as ulist UNWIND ulist as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", "label" : "Accounts With Paths To Unconstrained Delegation Objects Cracked (Excluding DCs)" }, { - "query" : "MATCH (u:User {cracked:true}),(n {highvalue:true}),p=shortestPath((u)-[r*1..]->(n)) WHERE NONE (r IN relationships(p) WHERE type(r)= 'GetChanges') AND NONE (r in relationships(p) WHERE type(r)='GetChangesAll') AND NOT u=n RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + "query" : "match p = shortestPath((u)-[*1..]->(n)) where n.highvalue = true AND u <> n WITH [n in nodes(p) WHERE n:User] as ulist UNWIND(ulist) as u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", "label" : "Accounts With Paths To High Value Targets Cracked" }, { @@ -1160,7 +1160,7 @@ def dpat_func(args): "label" : "Accounts With Explicit Controlling Privileges Cracked" }, { - "query" : "MATCH p2=(u:User {cracked:true})-[r1:MemberOf*1..]->(g:Group)-[r2:AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|CanRDP|CanPSRemote|ExecuteDCOM|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory]->(n2) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", + "query" : "MATCH p2=(n)-[r1:MemberOf*1..]->(g:Group)-[r2:AllExtendedRights|AddMember|ForceChangePassword|GenericAll|GenericWrite|Owns|WriteDacl|WriteOwner|ReadLAPSPassword|ReadGMSAPassword|CanRDP|CanPSRemote|ExecuteDCOM|AllowedToDelegate|AddAllowedToAct|AllowedToAct|SQLAdmin|HasSIDHistory]->(n2) WITH [u in nodes(p2) WHERE u:User] AS ulist UNWIND(ulist) AS u MATCH (u {cracked:true}) RETURN DISTINCT u.enabled,u.ntds_uname,u.password,u.nt_hash", "label" : "Accounts With Group Delegated Controlling Privileges Cracked" } ] @@ -1641,6 +1641,7 @@ def write_html_report(self, filebase, filename): print("") + def pet_max(): messages = [ From 760193d9bc31c323438ece8c5bad1db1dbdcad7b Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Mon, 16 Sep 2024 16:57:46 -0400 Subject: [PATCH 4/7] added esc1-owned-path, --templates, --cas, --enrollment_rights --- max.py | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/max.py b/max.py index 487e9ef..9cd0cd1 100644 --- a/max.py +++ b/max.py @@ -509,7 +509,46 @@ def run_adcs(args): """, "columns": ["Path"], "data_format": "graph" - } + }, + "cas": { + "query": """ + MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name AS CAName + """, + "columns": ["CAName"], + }, + "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"] + }, } # Determine which query to execute based on args @@ -537,8 +576,16 @@ def run_adcs(args): 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"] else: - print("No valid ESC option selected.") + print("No valid ADCS option selected.") return query = selected_query["query"] @@ -1751,6 +1798,7 @@ def main(): 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)") @@ -1761,6 +1809,9 @@ def main(): 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_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") From 4391a3093145cd1a716760475b6d48759f1c8591 Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Mon, 16 Sep 2024 17:05:02 -0400 Subject: [PATCH 5/7] added 'dns hostname' to --cas flag to make it easier to abuse esc8 --- max.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/max.py b/max.py index 9cd0cd1..f0c7a68 100644 --- a/max.py +++ b/max.py @@ -512,9 +512,9 @@ def run_adcs(args): }, "cas": { "query": """ - MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name AS CAName + MATCH (n:GPO) WHERE n.type = 'Enrollment Service' RETURN n.name AS CAName, n.`DNS Name` AS DNSHostname """, - "columns": ["CAName"], + "columns": ["CAName", "DNSHostname"], }, "templates": { "query": """ From a89af488e3b83b24f8e6285721a8988c99f0c9d1 Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Mon, 16 Sep 2024 17:11:03 -0400 Subject: [PATCH 6/7] added vulnerable_templates flag --- max.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/max.py b/max.py index f0c7a68..387bc25 100644 --- a/max.py +++ b/max.py @@ -549,6 +549,38 @@ def run_adcs(args): """, "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 @@ -584,6 +616,8 @@ def run_adcs(args): 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 @@ -1812,6 +1846,7 @@ def main(): 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") From 093e89ac4197b859f2135ddad246cfd9bd5dff31 Mon Sep 17 00:00:00 2001 From: AJ Hammond Date: Thu, 19 Sep 2024 23:52:47 -0400 Subject: [PATCH 7/7] syntax --- max.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/max.py b/max.py index 387bc25..060e5f1 100644 --- a/max.py +++ b/max.py @@ -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" @@ -1821,7 +1848,7 @@ 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") @@ -1846,7 +1873,7 @@ def main(): 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_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")