Skip to content

Commit

Permalink
feat: adding cross domain privileges control
Browse files Browse the repository at this point in the history
  • Loading branch information
VangelisHoareau committed Oct 27, 2023
1 parent dac9ef3 commit 1242dca
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 8 deletions.
3 changes: 2 additions & 1 deletion ad_miner/sources/modules/config.json
Expand Up @@ -96,6 +96,7 @@
"da_to_da": "true",
"users_password_not_required": "true",
"get_empty_groups": "true",
"has_sid_history": "true"
"has_sid_history": "true",
"cross_domain":"true"
}
}
6 changes: 6 additions & 0 deletions ad_miner/sources/modules/description.json
Expand Up @@ -494,5 +494,11 @@
"description": "SID History (Security Identifier History) is a feature that allows a user or group to retain access to resources that they had permissions for in a different domain. This feature is particularly useful in scenarios involving domain migrations, domain trust relationships, or domain reorganizations.",
"risk": " If an attacker gains control of an account with SID History entries, they may be able to use those historical SIDs to gain unauthorized access to resources in old domains. This is particularly concerning if the attacker can exploit vulnerabilities or weak security practices in the old domain.",
"poa": "Regularly review and clean up SID History entries for users and groups that no longer require access to resources in old domains."
},
"cross_domain_admin_privileges":{
"title":"List of users that have powerful cross-domain privileges.",
"description": "Users privileges are not limited to their domains. Sometimes some users may have direct or non-direct local admin or even domain admin privilege on a foreign domain.",
"risk": "Cross-domain privileges are quite dangerous and will help attackers to pivot to other domains if they manage to compromise a domain",
"poa": "Review these privileges, this list should be as little as possible."
}
}
199 changes: 199 additions & 0 deletions ad_miner/sources/modules/domains.py
@@ -1,4 +1,5 @@
import time
import copy

from urllib.parse import quote

Expand Down Expand Up @@ -143,6 +144,9 @@ def __init__(self, arguments, neo4j):
self.collected_domains = neo4j.all_requests["nb_domain_collected"]["result"]
self.crossDomain = 0

self.cross_domain_local_admins_paths = neo4j.all_requests["cross_domain_local_admins"]["result"]
self.cross_domain_domain_admins_paths = neo4j.all_requests["cross_domain_domain_admins"]["result"]

self.number_of_gpo = 0
self.number_of_OU = 0

Expand Down Expand Up @@ -247,6 +251,8 @@ def __init__(self, arguments, neo4j):
self.genEmptyGroups()
self.genEmptyOUs()

self.genPathsCrossDomainsAdminPrivileges()

logger.print_warning(timer_format(time.time() - start))

# All groups
Expand Down Expand Up @@ -1614,4 +1620,197 @@ def genEmptyOUs(self):

page.addComponent(grid)
page.render()

def genPathsCrossDomainsAdminPrivileges(self):
# get the result of the cypher request (a list of Path objects)
paths_local_admins = self.cross_domain_local_admins_paths

paths_domain_admins = self.cross_domain_domain_admins_paths
# create the page
page = Page(
self.arguments.cache_prefix,
"cross_domain_admin_privileges",
"Cross-Domain admin privileges",
"cross_domain_admin_privileges",
)
# create the grid
grid = Grid("Cross-Domain admin privileges")
# create the headers (domains)
headers = ["user","crossLocalAdminAsGraph","crossLocalAdminAsList","crossDomainAdminAsGraph","crossDomainAdminAsList"]

data_local_admins={}
for path in paths_local_admins:
user = path.nodes[0].name
target_domain = path.nodes[-1].domain
if user in data_local_admins.keys():
# data_local_admins[user].append(path)
if target_domain in data_local_admins[user]:
data_local_admins[user][target_domain].append(path)
else:
data_local_admins[user][target_domain]=[path]
else:
data_local_admins[user]={target_domain:[path]}

data={}

data_domain_admins={}
for path in paths_domain_admins:
user = path.nodes[0].name
target_domain = path.nodes[-1].domain
if user in data_domain_admins.keys():
# data_domain_admins[user].append(path)
if target_domain in data_domain_admins[user]:
data_domain_admins[user][target_domain].append(path)
else:
data_domain_admins[user][target_domain]=[path]
else:
data_domain_admins[user]={target_domain:[path]}

user_keys_raw = list(data_local_admins.keys())+list(data_domain_admins.keys())
unique_users_keys = set(user_keys_raw)


grid_data = []

self.cross_domain_total_admin_accounts=len(list(unique_users_keys))
self.cross_domain_local_admin_accounts=len(list(data_local_admins))
self.cross_domain_domain_admin_accounts=len(list(data_domain_admins))

for key in unique_users_keys:
user=key
tmp_data={}

tmp_data["user"] = '<i class="bi bi-person-fill"></i> '+user
grid_list_local_admin_targets_data=[]
grid_list_domain_admin_targets_data=[]
# create the grid
grid_list_local_admin_targets = Grid("List of computers from a foreign domain where "+user+" happens to be a local admin")
grid_list_domain_admin_targets = Grid("List of foreign domains where "+user+" happens to be a domain admin")
if key in data_local_admins.keys():
local_targets=[]
local_distinct_ends=[]
for domain in data_local_admins[key]:
list_local_admin_targets_tmp_data={"domain":'<i class="bi bi-globe2"></i> '+domain}
numberofpaths = 0
for path in data_local_admins[key][domain]:
list_local_admin_targets_tmp_data_copy = copy.deepcopy(list_local_admin_targets_tmp_data)
last_node_name = path.nodes[-1].name
local_targets.append(path)
if last_node_name not in local_distinct_ends:
local_distinct_ends.append(last_node_name)
sortClass = last_node_name.zfill(6)
list_local_admin_targets_tmp_data_copy["target"]=grid_data_stringify({
"value": f"{last_node_name}",
"link": "%s_paths_cross_domain_local_admin.html" % user,
"before_link": f'<i class="bi bi-shuffle {sortClass}"></i>'
})

grid_list_local_admin_targets_data.append(list_local_admin_targets_tmp_data_copy)
nb_local_distinct_ends=len(local_distinct_ends)
sortClass = str(nb_local_distinct_ends).zfill(6)
tmp_data["crossLocalAdminAsGraph"]=grid_data_stringify({
"value": f"{nb_local_distinct_ends} computers impacted",
"link": "%s_paths_cross_domain_local_admin.html" % user,
"before_link": f'<i class="bi bi-shuffle {sortClass}"></i>'
})
self.createGraphPage(
self.arguments.cache_prefix,
user + "_paths_cross_domain_local_admin",
"Paths from "+ user +" to machines of privileged groups from other domains making them domainadmin",
"cross_domain_admin_privileges",
local_targets,
)



page_list_local_admin_targets = Page(
self.arguments.cache_prefix,
"cross_domain_local_admins_targets_from_"+user,
"List of computers from a foreign domain where "+user+" happens to be a local admin",
"cross_domain_admin_privileges",
)
# create the headers (domains)
local_admins_list_page_headers = ["domain","target"]
grid_list_local_admin_targets.setheaders(local_admins_list_page_headers )
grid_list_local_admin_targets.setData(grid_list_local_admin_targets_data)
page_list_local_admin_targets.addComponent(grid_list_local_admin_targets)
page_list_local_admin_targets.render()
tmp_data["crossLocalAdminAsList"]=grid_data_stringify({
"value": "<i class='bi bi-list-columns-reverse'></i></span>",
"link": "cross_domain_local_admins_targets_from_%s.html" % user
})

else:
tmp_data["crossLocalAdminAsGraph"]="-"
tmp_data["crossLocalAdminAsList"]="-"



if key in data_domain_admins.keys():
domain_targets=[]
domain_distinct_ends=[]
for domain in data_domain_admins[key]:
list_domain_admin_targets_tmp_data={"domain":'<i class="bi bi-globe2"></i> '+domain}

for path in data_domain_admins[key][domain]:
list_domain_admin_targets_tmp_data_copy = copy.deepcopy(list_domain_admin_targets_tmp_data)
last_node_name = path.nodes[-1].name
domain_targets.append(path)
if last_node_name not in domain_distinct_ends:

domain_distinct_ends.append(last_node_name)

sortClass = last_node_name.zfill(6)
list_domain_admin_targets_tmp_data_copy["target"]=grid_data_stringify({
"value": f"{last_node_name}",
"link": "%s_paths_cross_domain_domain_admin.html" % user,
"before_link": f'<i class="bi bi-shuffle {sortClass}"></i>'
})

grid_list_domain_admin_targets_data.append(list_domain_admin_targets_tmp_data_copy)

nb_domain_distinct_ends=len(domain_distinct_ends)
sortClass = str(len(list(data_domain_admins[key].keys()))).zfill(6)
tmp_data["crossDomainAdminAsGraph"]=grid_data_stringify({
"value": f"{len(list(data_domain_admins[key].keys()))} domains impacted",
"link": "%s_paths_cross_domain_domain_admin.html" % user,
"before_link": f'<i class="bi bi-shuffle {sortClass}"></i>'
})
self.createGraphPage(
self.arguments.cache_prefix,
user + "_paths_cross_domain_domain_admin",
"Paths from "+ user +" to privileged groups from other domains making him/her domain admin",
"cross_domain_admin_privileges",
domain_targets,
)



page_list_domain_admin_targets = Page(
self.arguments.cache_prefix,
"cross_domain_domain_admins_targets_from_"+user,
"List of other domains where "+user+" happens to be a domain admin",
"cross_domain_admin_privileges",
)
# create the headers (domains)
domain_admins_list_page_headers = ["domain","target"]
grid_list_domain_admin_targets.setheaders(domain_admins_list_page_headers )
grid_list_domain_admin_targets.setData(grid_list_domain_admin_targets_data)
page_list_domain_admin_targets.addComponent(grid_list_domain_admin_targets)
page_list_domain_admin_targets.render()
tmp_data["crossDomainAdminAsList"]=grid_data_stringify({
"value": "<i class='bi bi-list-columns-reverse'></i></span>",
"link": "cross_domain_domain_admins_targets_from_%s.html" % user
})

else:
tmp_data["crossDomainAdminAsGraph"]="-"
tmp_data["crossDomainAdminAsList"]="-"
grid_data.append(tmp_data)


grid.setheaders(headers)
grid.setData(grid_data)
page.addComponent(grid)
page.render()

13 changes: 8 additions & 5 deletions ad_miner/sources/modules/main_page.py
Expand Up @@ -238,7 +238,8 @@ def create_dico_data(
"group_anomaly_acl": users.number_group_ACL_anomaly,
"empty_groups": len(domains.empty_groups),
"empty_ous": len(domains.empty_ous),
"has_sid_history": len(users.has_sid_history)
"has_sid_history": len(users.has_sid_history),
"cross_domain_admin_privileges":domains.cross_domain_total_admin_accounts
}

dico_data["color_category"] = dico_rating_color
Expand Down Expand Up @@ -367,7 +368,8 @@ def render(
"group_anomaly_acl": f"{users.number_group_ACL_anomaly} groups with potential ACL anomalies",
"empty_groups": f"{len(domains.empty_groups)} groups without any member",
"empty_ous": f"{len(domains.empty_ous)} OUs without any member",
"has_sid_history": f"{len(users.has_sid_history)} objects can exploit SID History"
"has_sid_history": f"{len(users.has_sid_history)} objects can exploit SID History",
"cross_domain_admin_privileges": f"{dico_data['value']['cross_domain_admin_privileges']} accounts have cross-domain admin privileges"
}

descriptions = DESCRIPTION_MAP
Expand Down Expand Up @@ -566,7 +568,7 @@ def render(
],
"permission": [
[19, 65],
[10, 30],
[8, 28],
[25, 20],
[28, 60],
[5, 50],
Expand All @@ -579,12 +581,13 @@ def render(
[5, 38],
[7, 58],
[27, 36],
[20, 30],
[22, 29],
[13, 53],
[26, 48],
[30, 25],
[30, 68],
[16, 72]
[16, 72],
[15,33]
],
}

Expand Down
12 changes: 11 additions & 1 deletion ad_miner/sources/modules/rating.py
Expand Up @@ -168,6 +168,8 @@ def rating(users, domains, computers, objects, arguments):
d[2 if len(domains.empty_ous)/len(domains.groups) > 0.40 else 3 if len(domains.empty_ous)/len(domains.groups) > 0.20 else 5].append("empty_ous")

d[presence_of(users.has_sid_history, 2)].append("has_sid_history")
d[rate_cross_domain_privileges(domains.cross_domain_local_admin_accounts,domains.cross_domain_domain_admin_accounts)].append("cross_domain_admin_privileges")


return d

Expand Down Expand Up @@ -347,4 +349,12 @@ def rate_vuln_functional_level(req):
if req != None:
return min([ret["Level maturity"] for ret in req])
else:
return -1
return -1

def rate_cross_domain_privileges(nb_local_priv,nb_da_priv):
if nb_da_priv > 0:
return 1
elif nb_local_priv>0:
return 2
else:
return 5
10 changes: 10 additions & 0 deletions ad_miner/sources/modules/requests.json
Expand Up @@ -657,5 +657,15 @@
"output_type": "Graph",
"scope_query": "MATCH (g:GPO) RETURN COUNT(g)",
"_comment": "this is the normal version of the GPO request"
},
"cross_domain_local_admins":{
"name": "Users that are local admins cross-domain",
"request": "MATCH p1=(u{enabled:true})-[r:MemberOf*1..4]->(g:Group{is_admin:true})-[rr:AdminTo]->(c:Computer) WHERE c.ghost_computer IS NULL AND u.domain <> c.domain AND NOT c.domain CONTAINS u.domain WITH collect(distinct p1) as paths OPTIONAL MATCH q=(u{enabled:true})-[r:AdminTo]->(c:Computer) WHERE c.ghost_computer IS NULL AND u.domain <> c.domain AND NOT c.domain CONTAINS u.domain WITH paths + collect(distinct q) as allPaths UNWIND allPaths as p RETURN DISTINCT p",
"output_type": "Graph"
},
"cross_domain_domain_admins":{
"name": "Users that are domain admins cross-domain",
"request": "MATCH p=(u{enabled:true})-[r:MemberOf*1..4]->(g:Group{is_da:true}) WHERE u.domain <> g.domain AND NOT g.domain CONTAINS u.domain return p",
"output_type": "Graph"
}
}
3 changes: 2 additions & 1 deletion ad_miner/sources/modules/smolcard_class.py
Expand Up @@ -53,7 +53,8 @@
"da_to_da",
"dangerous_paths",
"group_anomaly_acl",
"has_sid_history"
"has_sid_history",
"cross_domain_admin_privileges"
],
}

Expand Down

0 comments on commit 1242dca

Please sign in to comment.