Skip to content

Commit

Permalink
Merge pull request #54 from Mazars-Tech/rework-gpo
Browse files Browse the repository at this point in the history
feat: small rework for GPO
  • Loading branch information
k4amos committed Oct 25, 2023
2 parents ce52bb5 + eaf8ef4 commit 50e152d
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 258 deletions.
2 changes: 1 addition & 1 deletion ad_miner/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ def main() -> None:
domains = Domains(arguments, neo4j)
computers = Computers(arguments, neo4j, domains)
users = Users(arguments, neo4j, domains)
objects = Objects(arguments, neo4j)
objects = Objects(arguments, neo4j, domains, computers, users)

# Generate the main page
logger.print_success("Temporary vulnerabilities rating :")
Expand Down
8 changes: 4 additions & 4 deletions ad_miner/sources/modules/computers.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,7 @@ def genComputersAdminOfPages(self):
computers_admin_to_list = generic_computing.getListAdminTo(
self.list_computers_admin_computers, "source_computer", "target_computer"
)
computers_admin_data_grid = []
self.computers_admin_data_grid = []
for admin_computer, computers_list in computers_admin_to_list.items():
if admin_computer is not None and computers_list is not None:
num_path, nb_domains = self.domain.findAndCreatePathToDaFromComputersList(admin_computer, computers_list)
Expand All @@ -337,8 +337,8 @@ def genComputersAdminOfPages(self):
else:
tmp_line["Paths to domain admin"] = "-"

computers_admin_data_grid.append(tmp_line)
computers_admin_data_grid.sort(
self.computers_admin_data_grid.append(tmp_line)
self.computers_admin_data_grid.sort(
key=lambda x: x["Computers count"], reverse=True
)

Expand All @@ -351,7 +351,7 @@ def genComputersAdminOfPages(self):
)
grid = Grid("Computers admins of other computers")
grid.setheaders(["Computer Admin", "Computers count", "Paths to domain admin"])
grid.setData(json.dumps(computers_admin_data_grid))
grid.setData(json.dumps(self.computers_admin_data_grid))
page.addComponent(grid)
page.render()

Expand Down
2 changes: 1 addition & 1 deletion ad_miner/sources/modules/description.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@

"users_GPO_access": {
"title": "GPOs that can be edited by unprivileged users",
"description": "GPOs that can be edited by unprivileged users",
"description": "GPOs that can be edited by unprivileged users.<br/><i class='bi bi-star-fill' style='color: red'></i><i class='bi bi-star-fill' style='color: red'></i><i class='bi bi-star-fill' style='color: red'></i> : Full domain or at least one domain admin as target.<br /><i class='bi bi-star-fill' style='color: orange'></i><i class='bi bi-star-fill' style='color: orange'></i><i class='bi bi-star' style='color: orange'></i> : At least one object admin of a computer.<br/><i class='bi bi-star-fill' style='color: green'></i><i class='bi bi-star' style='color: green'></i><i class='bi bi-star' style='color: green'></i> : At least one object as target.<br/><i class='bi bi-star' style='color: green'></i><i class='bi bi-star' style='color: green'></i><i class='bi bi-star' style='color: green'></i> : No direct target.",
"risk": "If an AD object has rights over a GPO, it can potentially cause damage over all the objects affected by the GPO. GPOs can also be leveraged to gain privileges in the domain(s). If an attacker exploits one of these paths, they will be able to gain privileges in the domain(s) and cause some serious damage.",
"poa": "Review the paths, make sure they are not exploitable. If they are, cut the link between the Active Directory objects in order to reduce the attack surface."
},
Expand Down
250 changes: 0 additions & 250 deletions ad_miner/sources/modules/domains.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import json
import time

from urllib.parse import quote
from os.path import sep

from ad_miner.sources.modules import generic_formating
from ad_miner.sources.modules import logger
Expand Down Expand Up @@ -238,7 +236,6 @@ def __init__(self, arguments, neo4j):

self.generateDomainMapTrust()

self.get_unpriv_users_to_GPO()
self.get_domain_OUs()
self.genDAPage()
self.genInsufficientForestDomainsLevels()
Expand Down Expand Up @@ -1316,253 +1313,6 @@ def findAndCreatePathToDaFromUsersList(self, admin_user, computers):
)
return (len(path_to_generate), len(list_domain))

def get_unpriv_users_to_GPO(self):
if self.arguments.gpo_low and self.unpriv_users_to_GPO is None:
return
if not self.arguments.gpo_low:
fail = []
if self.unpriv_users_to_GPO_init is None:
fail.append("unpriv_users_to_GPO_init")
elif self.unpriv_users_to_GPO_user_enforced is None:
fail.append("unpriv_users_to_GPO_user_enforced")
elif self.unpriv_users_to_GPO_user_not_enforced is None:
fail.append("unpriv_users_to_GPO_user_not_enforced")
elif self.unpriv_users_to_GPO_computer_enforced is None:
fail.append("unpriv_users_to_GPO_computer_enforced")
elif self.unpriv_users_to_GPO_computer_not_enforced is None:
fail.append("unpriv_users_to_GPO_computer_not_enforced")

if 0 < len(fail) < 5: # if only some of them are disabled
logger.print_error(
f" In order to use 'normal GPO mode', please activate the following in config.json : {', '.join(fail)}"
)

if len(fail) > 0:
return

def parseGPOData(listOfPaths, headers):
"""
Initial parsing of data from neo4j requests for GPO
"""
dictOfGPO = {}
for path in listOfPaths:
start = path.nodes[0]
end = path.nodes[-1]
if "GPO" in start.labels:
nameOfGPO = start.name
idOfGPO = start.id
sens = "right"
elif "GPO" in end.labels:
nameOfGPO = end.name
idOfGPO = end.id
sens = "left"
else:
continue
try:
if sens == "right":
dictOfGPO[nameOfGPO][headers[4]] += 1
dictOfGPO[nameOfGPO]["right_path"].append(path)
dictOfGPO[nameOfGPO]["end_list"].append(end.name)
elif sens == "left":
dictOfGPO[nameOfGPO][headers[1]] += 1
dictOfGPO[nameOfGPO]["left_path"].append(path)
dictOfGPO[nameOfGPO]["entry_list"].append(start.name)
else:
continue
except KeyError:
if sens == "right":
dictOfGPO[nameOfGPO] = {
headers[0]: nameOfGPO,
headers[1]: 0,
headers[4]: 1,
"left_path": [],
"right_path": [path],
"id": idOfGPO,
"entry_list": [],
"end_list": [end.name],
}
elif sens == "left":
dictOfGPO[nameOfGPO] = {
headers[0]: nameOfGPO,
headers[1]: 1,
headers[4]: 0,
"left_path": [path],
"right_path": [],
"id": idOfGPO,
"entry_list": [start.name],
"end_list": [],
}
else:
continue
return dictOfGPO

def formatGPOGrid(dictOfGPO, headers):
output = []
for _, dict in dictOfGPO.items():
# Get number of domains
domains = []
for path in dict["right_path"]:
#for relation in path.relationships:
for i in range(len(path.nodes)):
if path.nodes[i].labels == "Domain":
domains.append(path.nodes[i].name)
# if dict[headers[4]] > 0:
self.number_of_gpo += 1

nbDomains = len(list(set(domains)))
sortClass = str(nbDomains).zfill(6)
icon = (
'<svg class="%s" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1008 1024" style="width:25px";><path d="M477.177 855.5c-53.61-2.647-114.503-13.494-162.25-28.904l-12.093-3.903 11.589-2.755c39.97-9.503 85.839-16.49 131.01-19.956 24.873-1.909 90.361-1.613 115.893.523 27.799 2.326 57.168 5.986 82.23 10.247 23.064 3.921 58.937 11.383 60.137 12.509.427.4-5.922 2.855-14.109 5.455-66.413 21.092-139.925 30.362-212.407 26.784zm-208.104-75.808c0-20.764.152-22.771 1.764-23.266 19.345-5.942 55.283-14.613 79.414-19.16 80.071-15.088 168.65-17.737 252.896-7.563 36.86 4.451 82.447 13.218 120.176 23.111l16.376 4.294v22.537c0 13.633-.38 22.537-.962 22.537-.529 0-10.166-2.284-21.415-5.076-50.372-12.5-98.281-20.276-152.974-24.828-25.99-2.163-97.243-2.464-121.436-.513-55.427 4.47-101.73 11.938-148.142 23.893-11.64 2.998-22.183 5.745-23.431 6.105l-2.267.654zm0-81.045v-29.175l13.353-3.655c55.753-15.261 103.839-23.547 163.006-28.088 21.019-1.613 88.971-1.597 109.846.027 61.272 4.765 117.088 14.018 169.808 28.152l14.109 3.782.267 28.958c.147 15.927.078 28.958-.154 28.958s-9.596-2.294-20.808-5.097c-49.769-12.444-101.749-20.848-156.672-25.331-21.773-1.777-101.382-1.766-122.947.017-51.904 4.292-96.105 11.5-142.914 23.303-12.852 3.241-24.161 6.214-25.13 6.608-1.655.672-1.764-1.073-1.764-28.46zm464.076-59.894c-56.218-15.378-125.089-26.108-187.444-29.203-13.303-.66-24.282-1.257-24.399-1.327-.535-.318 8.091-36.072 11.798-48.899 13.277-45.954 33.728-88.885 57.246-120.175 25.869-34.418 58.363-60.432 89.428-71.593 10.915-3.922 28.85-7.582 43.059-8.788 35.38-3.002 72.831 6.529 100.423 25.558 25.515 17.596 31.39 50.279 16.837 93.67-7.738 23.074-22.629 53.628-41.103 84.34-8.764 14.571-39.872 62.986-49.261 76.667l-2.476 3.608zM247.35 625.59c-27.073-36.143-46.369-66.745-60.371-95.745-13.53-28.021-20.849-50.067-25.294-76.179-2.821-16.576-3.068-26.178-.909-35.34 6.174-26.19 35.844-49.829 70.191-55.921 13.228-2.346 37.419-3.987 47.907-3.25 34.167 2.402 67.912 16.521 98.533 41.228 11.349 9.157 29.856 28.537 39.171 41.017 29.069 38.95 55.724 101.939 69.551 164.358l.579 2.612-20.89.658c-44.784 1.41-98.475 7.955-141.822 17.288-12.384 2.666-50.707 12.405-59.092 15.017l-4.672 1.455zm245.774-115.325c-19.161-38.24-30.164-71.107-38.741-115.725-7.802-40.588-2.473-75.701 14.355-94.581 8.899-9.985 21.478-14.468 37.958-13.528 6.759.385 9.976 1.101 13.909 3.094 13.13 6.653 23.272 23.335 26.794 44.069 1.986 11.689 1.669 54.908-.517 70.52-3.877 27.69-10.832 54.591-20.382 78.831-5.682 14.423-19.608 43.444-22.056 45.963-1.076 1.107-3.066-2.17-11.32-18.644zm-21.49 464.563c-30.312-2.251-65.173-9.427-99.29-20.438-35.041-11.31-53.622-18.505-73.361-28.406C157.301 854.915 60.799 701.642 49.405 529.581c-5.867-88.606 15.612-180.612 58.7-251.437 10.422-17.13 38.329-58.173 46.597-68.528 34.91-43.724 76.788-78.503 127.253-105.681 78.12-42.072 171.469-60.997 263.751-53.47 90.355 7.37 175.104 40.137 241.864 93.513 14.51 11.601 38.686 34.958 51.18 49.448 11.63 13.486 34.003 43.345 44.601 59.524 39.171 59.798 65.304 131.971 73.527 203.065 3.887 33.607 4.369 85.162 1.064 113.877-13.304 115.605-74.695 222.976-172.388 301.5-72.61 58.362-159.043 94.146-247.82 102.599-14.38 1.369-52.445 1.852-66.099.838zm85.66-91.052c67.122-5.18 131.259-20.249 190.368-44.725 7.428-3.076 14.406-6.394 15.508-7.374 4.804-4.275 4.747-3.228 4.747-88.344v-80.077l18.999-29.046c41.342-63.205 59.138-94.391 72.73-127.456 11.023-26.817 15.059-43.376 15.807-64.853.612-17.566-.444-26.018-4.944-39.589-6.056-18.263-16.853-32.204-33.585-43.364-58.996-39.35-144.595-39.047-204.085.722-19.28 12.889-38.028 29.508-52.345 46.401l-6.355 7.499.646-4.475c2.731-18.93 3.384-47.839 1.562-69.159-2.452-28.688-11.216-49.319-27.37-64.431-7.583-7.093-13.599-10.692-23.428-14.014l-7.054-2.384-.54-47.476h61.51v-28.217h-61.474v-49.38h-28.217v49.38h-60.466v28.217h60.466v47.896l-8.975 2.144c-17.712 4.231-29.651 12.594-39.794 27.877-18.155 27.353-23.774 70.795-14.297 110.535 2.678 11.228 2.336 11.276-7.226 1.018-40.961-43.944-96.357-70.461-147.386-70.55-21.091-.037-49.396 3.723-64.243 8.533-47.021 15.235-76.818 51.595-76.831 93.752-.003 10.092 3.538 32.824 7.515 48.241 13.407 51.969 37.989 97.82 88.363 164.817l13.954 18.559v80.587c0 94.101-1.195 84.997 11.874 90.474 67.849 28.437 148.271 45.742 230.998 49.705 12.443.596 58.977-.316 73.567-1.442z"/></svg>'
% sortClass
)

output.append(
{
headers[0]: dict[headers[0]],
headers[1]: dict[headers[1]],
headers[2]: {
"link": "users_GPO_access-%s-left-graph.html"
% (quote(str(dict[headers[0]]).replace(sep, '_'))),
"value": "<i class='bi bi-diagram-3-fill' aria-hidden='true'></i>",
},
headers[3]: {
"link": "users_GPO_access-%s-left-grid.html"
% (quote(str(dict[headers[0]]).replace(sep, '_'))),
"value": "<i class='bi bi-list-columns-reverse' aria-hidden='true'></i>",
},
headers[4]: len(list(set(dict["end_list"]))),
headers[5]: "%s %d" % (icon, nbDomains),
headers[6]: {
"link": "users_GPO_access-%s-right-graph.html"
% (quote(str(dict[headers[0]]).replace(sep, '_'))),
"value": "<i class='bi bi-diagram-3-fill' aria-hidden='true'></i>",
},
headers[7]: {
"link": "users_GPO_access-%s-right-grid.html"
% (quote(str(dict[headers[0]]).replace(sep, '_'))),
"value": "<i class='bi bi-list-columns-reverse' aria-hidden='true'></i>",
},
}
)
return output

def formatSmallGrid(list, gpo_name):
output = []
for n in list:
output.append({gpo_name: n})
return output

headers = [
"GPO name",
"Paths to GPO",
"Inbound graph",
"Inbound list",
"Objects impacted",
"Domains impacted",
"Outbound graph",
"Outbound list",
]
if not self.arguments.gpo_low:
data = (
self.unpriv_users_to_GPO_init
+ self.unpriv_users_to_GPO_user_enforced
+ self.unpriv_users_to_GPO_computer_enforced
+ self.unpriv_users_to_GPO_user_not_enforced
+ self.unpriv_users_to_GPO_computer_not_enforced
)
self.unpriv_users_to_GPO_parsed = parseGPOData(data, headers)
grid = Grid("Users with GPO access")
else:
self.unpriv_users_to_GPO_parsed = parseGPOData(
self.unpriv_users_to_GPO, headers
)
grid = Grid("Users with GPO access")

formated_data = sorted(
formatGPOGrid(self.unpriv_users_to_GPO_parsed, headers),
key=lambda x: x[headers[1]],
reverse=True,
)
page = Page(
self.arguments.cache_prefix,
"users_GPO_access",
"Exploitation through GPO",
"users_GPO_access",
)

grid.setheaders(headers)
grid.setData(json.dumps(formated_data))
page.addComponent(grid)
page.render()

for _, GPO in self.unpriv_users_to_GPO_parsed.items():
url_left_graph = "users_GPO_access-%s-left-graph" % GPO[headers[0]]
url_right_graph = "users_GPO_access-%s-right-graph" % GPO[headers[0]]
page_left_graph = Page(
self.arguments.cache_prefix,
url_left_graph,
"Users with write access on GPO",
"graph_GPO_access",
)
page_right_graph = Page(
self.arguments.cache_prefix,
url_right_graph,
"Objects impacted by GPO",
"graph_GPO_access",
)

url_left_grid = "users_GPO_access-%s-left-grid" % GPO[headers[0]]
url_right_grid = "users_GPO_access-%s-right-grid" % GPO[headers[0]]
page_left_grid = Page(
self.arguments.cache_prefix,
url_left_grid,
"List of users able to compromise %s" % GPO[headers[0]],
"grid_GPO_access",
)
page_right_grid = Page(
self.arguments.cache_prefix,
url_right_grid,
"List of users impacted by %s" % GPO[headers[0]],
"grid_GPO_access",
)

# if GPO[headers[4]] > 0:
graph_left = Graph()
graph_left.setPaths(GPO["left_path"])
page_left_graph.addComponent(graph_left)

graph_right = Graph()
graph_right.setPaths(GPO["right_path"])
page_right_graph.addComponent(graph_right)

if not self.arguments.gpo_low:
entry_grid = Grid("List of users able to compromise %s" % GPO[headers[0]])
else:
entry_grid = Grid("List of users able to compromise %s" % GPO[headers[0]])
entry_grid.setheaders([GPO[headers[0]]])
entry_grid.setData(
json.dumps(
formatSmallGrid(
list(set(GPO["entry_list"])), GPO[headers[0]])
)
)
page_left_grid.addComponent(entry_grid)

if not self.arguments.gpo_low:
end_grid = Grid("List of users impacted by %s" % GPO[headers[0]])
else:
end_grid = Grid("List of users impacted by %s" % GPO[headers[0]])
end_grid.setheaders([GPO[headers[0]]])
end_grid.setData(
json.dumps(formatSmallGrid(
list(set(GPO["end_list"])), GPO[headers[0]]))
)
page_right_grid.addComponent(end_grid)

page_left_graph.render()
page_right_graph.render()
page_left_grid.render()
page_right_grid.render()

@staticmethod
def generatePathToOUHandlers(self):
Expand Down
Loading

0 comments on commit 50e152d

Please sign in to comment.