Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added ADCS #45

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 225 additions & 15 deletions max.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this needs to be lowered, undo this

"columns" : ["ComputerName","OperatingSystem"]
},
"foreignprivs" : {
Expand Down Expand Up @@ -243,6 +243,214 @@ def get_info(args):
}
}


if args.adcs:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put all these in a new function/module similar to get info. Provide the ability for the user to select just one query rather than requiring all of them to be run

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it can literally be as simple as --esc1 --esc2 --esc3

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:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I said above, provide the ability to make this more granular. Provide an --all flag if you want to be able to pull everything at once

for query_name, query_info in queries.items():
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general the coding here is very inefficient. You add your ADCS queries to the full list, then just iterate through the full list and identify the ADCS related ones. Again, speaks to separating it out to a new module. The arg parser is a mutually exclusive (mutex) grouping so if you select ADCS then you can't get any other datapoints, so there's no reason to add to the full queries then parse them back out

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"
Expand Down Expand Up @@ -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",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like these additions, but please separate them out into a new PR if desired. Too much going on in this PR

'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"
},
{
Expand Down Expand Up @@ -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"
},
{
Expand All @@ -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"
}
]
Expand Down Expand Up @@ -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 = """
Expand Down Expand Up @@ -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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is instantiated but never used. Use it and stay in line with the current code structure. Use adcs as a new module and structure it the same as get-info rather than having them combined

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")
Expand Down Expand Up @@ -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")
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the comment above, separate get-info and adcs modules. Made the adcs module more granular with specific flags to be able to select one specific attack path required


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)")
Expand Down Expand Up @@ -1659,3 +1868,4 @@ def main():

if __name__ == "__main__":
main()