diff --git a/ad_miner/scripts/analyse_cache.py b/ad_miner/scripts/analyse_cache.py index ab6cc29b..0532eada 100644 --- a/ad_miner/scripts/analyse_cache.py +++ b/ad_miner/scripts/analyse_cache.py @@ -1,19 +1,34 @@ -import pickle +import os import sys from pathlib import Path +from ad_miner.sources.modules.safe_pickle import safe_load + # Constants MODULES_DIRECTORY = Path(__file__).parent / 'sources/modules' def request_a(): module_name = sys.argv[1] - return retrieveCacheEntry(full_module_path=MODULES_DIRECTORY / module_name) + # Security: Validate that module_name is a simple filename without path components + if os.path.sep in module_name or '/' in module_name or module_name.startswith('.'): + raise ValueError(f"Invalid module name: path components not allowed") + + return retrieveCacheEntry(module_name=module_name) + + +def retrieveCacheEntry(module_name: str): + # Resolve the full path + full_path = (MODULES_DIRECTORY / module_name).resolve() + + # Security: Verify the path stays within the allowed directory + modules_dir_resolved = MODULES_DIRECTORY.resolve() + if not str(full_path).startswith(str(modules_dir_resolved)): + raise ValueError("Path traversal detected: access denied") -def retrieveCacheEntry(full_module_path: Path): - with open(full_module_path, "rb") as f: - return pickle.load(f) + with open(full_path, "rb") as f: + return safe_load(f) list_path = request_a() diff --git a/ad_miner/sources/js/graph.js b/ad_miner/sources/js/graph.js index 43fbed08..a95b3ba1 100644 --- a/ad_miner/sources/js/graph.js +++ b/ad_miner/sources/js/graph.js @@ -1,6 +1,13 @@ // This object is used by vis.js to retrieve node icon var icon_group_options = {}; +// HTML escape function to prevent XSS +function escapeHtmlGraph(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + var existing_attribute_strings = [] for (var i = 0; i < window.data_nodes.length; i++) { @@ -1755,6 +1762,8 @@ function bindRightClick() { No Action `; } else { + // Sanitize lastSelectedNode by using JSON.stringify to prevent injection + var safeNodeId = JSON.stringify(lastSelectedNode); if ( allNodescopy[lastSelectedNode].clusterType == 'complete' || allNodescopy[lastSelectedNode].clusterType == 'partial' || @@ -1762,22 +1771,14 @@ function bindRightClick() { ) { menu.innerHTML = `
- Open Cluster + Open Cluster
`; } else { menu.innerHTML = `
- Cluster - Cluster direct children only - Cluster direct forward children + Cluster + Cluster direct children only + Cluster direct forward children
`; } } @@ -1809,13 +1810,12 @@ function bindRightClick() { } function printRecurseWarning(nodeLabel) { + var escapedLabel = escapeHtmlGraph(nodeLabel); document.getElementById('hooker').innerHTML = `

Warning!

-

The configuration of the graph doesn't allow to cluster ` + - nodeLabel + - `

+

The configuration of the graph doesn't allow to cluster ${escapedLabel}

`; } diff --git a/ad_miner/sources/js/search_bar.js b/ad_miner/sources/js/search_bar.js index 8784d818..380f6050 100644 --- a/ad_miner/sources/js/search_bar.js +++ b/ad_miner/sources/js/search_bar.js @@ -2,6 +2,18 @@ Controls the search bar on the main page. */ +// HTML escape function to prevent XSS +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Escape special regex characters +function escapeRegex(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + // Hide and display the search bar function toggleSearch() { const searchBarDiv = document.getElementById("search-bar-div"); @@ -47,11 +59,14 @@ function updateDropdown(filteredControls) { dropdownItem.classList.add("dropdown-item"); dropdownItem.href = control.link; - // Highlight search input in result + // Highlight search input in result (escape HTML first to prevent XSS) const searchTerm = searchBar.value.toLowerCase().trim(); - var regex = new RegExp(searchTerm, 'gi'); - var name = control.name.replace(regex, '$&'); - var title = control.title.replace(regex, '$&'); + var escapedSearchTerm = escapeRegex(searchTerm); + var regex = new RegExp(escapedSearchTerm, 'gi'); + var escapedName = escapeHtml(control.name); + var escapedTitle = escapeHtml(control.title); + var name = escapedName.replace(regex, '$&'); + var title = escapedTitle.replace(regex, '$&'); dropdownItem.innerHTML = `
diff --git a/ad_miner/sources/modules/cache_class.py b/ad_miner/sources/modules/cache_class.py index 037f34ae..eaada7dc 100755 --- a/ad_miner/sources/modules/cache_class.py +++ b/ad_miner/sources/modules/cache_class.py @@ -2,6 +2,8 @@ import pickle import csv +from ad_miner.sources.modules.safe_pickle import safe_load + class Cache: def __init__(self, arguments): @@ -25,7 +27,7 @@ def retrieveCacheEntry(self, filename): full_name = self.cache_prefix + "_" + filename if os.path.exists(full_name): with open(full_name, "rb") as f: - return pickle.load(f) + return safe_load(f) return False def createCsvFileFromRequest(self, filename, data, object_type): diff --git a/ad_miner/sources/modules/common_analysis.py b/ad_miner/sources/modules/common_analysis.py index ed77d733..8468bb07 100644 --- a/ad_miner/sources/modules/common_analysis.py +++ b/ad_miner/sources/modules/common_analysis.py @@ -1,5 +1,5 @@ from ad_miner.sources.modules import logger -from ad_miner.sources.modules.utils import CONFIG_MAP, days_format +from ad_miner.sources.modules.utils import CONFIG_MAP, days_format, escape_html from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.graph_class import Graph from ad_miner.sources.modules.path_neo4j import Path @@ -517,16 +517,16 @@ def genNumberOfDCPage(requests_results, arguments): for d in computers_nb_domain_controllers: temp_data = {} - temp_data["domain"] = ' ' + d["domain"] + temp_data["domain"] = ' ' + escape_html(d["domain"]) if d["ghost"]: temp_data["name"] = ( ' ' - + d["name"] + + escape_html(d["name"]) ) else: - temp_data["name"] = ' ' + d["name"] + temp_data["name"] = ' ' + escape_html(d["name"]) if "WINDOWS" in d["os"].upper(): - temp_data["os"] = ' ' + d["os"] + temp_data["os"] = ' ' + escape_html(d["os"]) temp_data["last logon"] = days_format(d["lastLogon"]) data.append(temp_data) grid.setData(data) @@ -561,15 +561,15 @@ def genUsersListPage(requests_results, arguments): data = [] for user in users: tmp_dict = {} - tmp_dict["domain"] = ' ' + user["domain"] + tmp_dict["domain"] = ' ' + escape_html(user["domain"]) # Add admin icon if user["name"] in admin_list: tmp_dict["name"] = ( ' ' - + user["name"] + + escape_html(user["name"]) ) else: - tmp_dict["name"] = ' ' + user["name"] + tmp_dict["name"] = ' ' + escape_html(user["name"]) # Add calendar icon logon = -1 if user.get("logon"): @@ -601,12 +601,12 @@ def genAllGroupsPage(requests_results, arguments): grid.setheaders(["domain", "name"]) group_extract = [ { - "domain": ' ' + groups[k]["domain"], + "domain": ' ' + escape_html(groups[k]["domain"]), "name": ( ' ' - + groups[k]["name"] + + escape_html(groups[k]["name"]) if groups[k].get("da") - else ' ' + groups[k]["name"] + else ' ' + escape_html(groups[k]["name"]) ), } for k in range(len(groups)) @@ -641,13 +641,13 @@ def generateComputersListPage(requests_results, arguments): if computer["ghost"]: name = ( ' ' - + computer["name"] + + escape_html(computer["name"]) ) else: - name = ' ' + computer["name"] + name = ' ' + escape_html(computer["name"]) # OS if computer["os"]: - os = computer["os"] + os = escape_html(computer["os"]) if "windows" in computer["os"].lower(): os = ' ' + os elif "mac" in computer["os"].lower(): @@ -657,7 +657,7 @@ def generateComputersListPage(requests_results, arguments): else: os = "Unknown" formated_computer = { - "domain": ' ' + computer["domain"], + "domain": ' ' + escape_html(computer["domain"]), "name": name, "operating system": os, } @@ -689,8 +689,8 @@ def generateADCSListPage(requests_results, arguments): grid = Grid("ADCS servers") grid.setheaders(["domain", "name"]) for adcs in computers_adcs: - adcs["domain"] = ' ' + adcs["domain"] - adcs["name"] = ' ' + adcs["name"] + adcs["domain"] = ' ' + escape_html(adcs["domain"]) + adcs["name"] = ' ' + escape_html(adcs["name"]) grid.setData(computers_adcs) page.addComponent(grid) page.render() @@ -734,8 +734,8 @@ def genAzureTenants(requests_results, arguments): data.append( { "Tenant ID": ' ' - + tenant["ID"], - "Tenant Name": ' ' + tenant["Name"], + + escape_html(tenant["ID"]), + "Tenant Name": ' ' + escape_html(tenant["Name"]), } ) @@ -774,15 +774,15 @@ def genAzureUsers(requests_results, arguments): tenant_name = tenant_id_name.get(tenant_id, tenant_id) data.append( { - "Tenant Name": ' ' + tenant_name, - "Name": ' ' + user["Name"], + "Tenant Name": ' ' + escape_html(tenant_name), + "Name": ' ' + escape_html(user["Name"]), "Synced on premise": ( '' if user["onpremisesynced"] == True else '' ), "On premise SID": ( - user["SID"] + escape_html(user["SID"]) if user["onpremisesynced"] == True and user["SID"] != None else "-" ), @@ -824,8 +824,8 @@ def genAzureAdmin(requests_results, arguments): tenant_name = tenant_id_name.get(tenant_id, tenant_id) data.append( { - "Tenant Name": ' ' + tenant_name, - "Name": ' ' + admin["Name"], + "Tenant Name": ' ' + escape_html(tenant_name), + "Name": ' ' + escape_html(admin["Name"]), } ) @@ -864,9 +864,9 @@ def genAzureGroups(requests_results, arguments): tenant_name = tenant_id_name.get(tenant_id, tenant_id) data.append( { - "Tenant Name": ' ' + tenant_name, - "Name": ' ' + group["Name"], - "Description": group["Description"], + "Tenant Name": ' ' + escape_html(tenant_name), + "Name": ' ' + escape_html(group["Name"]), + "Description": escape_html(group["Description"]), } ) @@ -905,13 +905,13 @@ def genAzureVM(requests_results, arguments): for dict in azure_vm: tenant_id = dict.get("Tenant ID") tenant_name = tenant_id_name.get(tenant_id, tenant_id) - tmp_data = {"Tenant Name": tenant_name} + tmp_data = {"Tenant Name": escape_html(tenant_name)} - tmp_data["Name"] = dict["Name"] + tmp_data["Name"] = escape_html(dict["Name"]) # os if dict.get("os"): - os = dict["os"] + os = escape_html(dict["os"]) if "windows" in dict["os"].lower(): os = ' ' + os elif "mac" in dict["os"].lower(): @@ -958,13 +958,13 @@ def genAzureDevices(requests_results, arguments): tenant_id = dict.get("Tenant ID") tenant_name = tenant_id_name.get(tenant_id, tenant_id) - tmp_data = {"Tenant Name": tenant_name} + tmp_data = {"Tenant Name": escape_html(tenant_name)} - tmp_data["Name"] = dict["Name"] + tmp_data["Name"] = escape_html(dict["Name"]) # os if dict.get("os"): - os = dict["os"] + os = escape_html(dict["os"]) if "windows" in dict["os"].lower(): os = ' ' + os elif ( @@ -1023,15 +1023,15 @@ def genAzureApps(requests_results, arguments): if tenant_id == "F8CDEF31-A31E-4B4A-93E4-5F571E91255A": data_microsft.append( { - "Tenant ID": ' ' + tenant_name, - "Name": ' ' + app["Name"], + "Tenant ID": ' ' + escape_html(tenant_name), + "Name": ' ' + escape_html(app["Name"]), } ) else: data.append( { - "Tenant ID": ' ' + tenant_name, - "Name": ' ' + app["Name"], + "Tenant ID": ' ' + escape_html(tenant_name), + "Name": ' ' + escape_html(app["Name"]), } ) data += data_microsft diff --git a/ad_miner/sources/modules/controls/anomaly_acl.py b/ad_miner/sources/modules/controls/anomaly_acl.py index 66b1334a..f74ad508 100644 --- a/ad_miner/sources/modules/controls/anomaly_acl.py +++ b/ad_miner/sources/modules/controls/anomaly_acl.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules import generic_formating, generic_computing -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import ( get_dico_admin_of_computer_id, createGraphPage, @@ -178,7 +178,7 @@ def run(self): else: icon = "bi-person-fill" tmp_dict["targets"] = ( - ' ' + name + ' ' + escape_html(name) ) if name in self.dico_is_user_admin_on_computer: count = len(self.users_admin_computer_list[name]) @@ -281,7 +281,7 @@ def run(self): self.requests_results, ) - tmp_dict["targets"] = f' {name}' + tmp_dict["targets"] = f' ' + escape_html(name) formated_data_details.append(tmp_dict) page = Page( diff --git a/ad_miner/sources/modules/controls/azure_aadconnect_users.py b/ad_miner/sources/modules/controls/azure_aadconnect_users.py index 37bc0c34..79ad3e52 100644 --- a/ad_miner/sources/modules/controls/azure_aadconnect_users.py +++ b/ad_miner/sources/modules/controls/azure_aadconnect_users.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -40,8 +41,8 @@ def run(self): data.append( { "Tenant ID": ' ' - + f'{user["Tenant ID"] if user["Tenant ID"] != None else "-"}', - "Name": ' ' + user["Name"], + + escape_html(f'{user["Tenant ID"] if user["Tenant ID"] != None else "-"}'), + "Name": ' ' + escape_html(user["Name"]), "Session": user["Session"] if user["Session"] != None else "-", } ) diff --git a/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py b/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py index 35d869c1..d9716adb 100644 --- a/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py +++ b/ad_miner/sources/modules/controls/azure_accounts_not_found_on_prem.py @@ -4,6 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -46,7 +47,7 @@ def run(self): for user in self.azure_accounts_not_found_on_prem: data.append( { - "Name": ' ' + user["Name"], + "Name": ' ' + escape_html(user["Name"]), "Synced to on premise": '', "Synced account": '', } diff --git a/ad_miner/sources/modules/controls/azure_admin_on_prem.py b/ad_miner/sources/modules/controls/azure_admin_on_prem.py index f5deaf2f..96e338f3 100644 --- a/ad_miner/sources/modules/controls/azure_admin_on_prem.py +++ b/ad_miner/sources/modules/controls/azure_admin_on_prem.py @@ -4,6 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -40,7 +41,7 @@ def run(self): data = [] for user in self.azure_admin_on_prem: - data.append({"Name": ' ' + user["Name"]}) + data.append({"Name": ' ' + escape_html(user["Name"])}) grid.setheaders(["Name"]) diff --git a/ad_miner/sources/modules/controls/azure_dormant_accounts.py b/ad_miner/sources/modules/controls/azure_dormant_accounts.py index 8cbda5a2..e612e36b 100644 --- a/ad_miner/sources/modules/controls/azure_dormant_accounts.py +++ b/ad_miner/sources/modules/controls/azure_dormant_accounts.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import presence_of @@ -47,7 +47,7 @@ def run(self): self.azure_dormant_accounts_90_days.append(user) data.append( { - "Name": ' ' + user["Name"], + "Name": ' ' + escape_html(user["Name"]), "Last logon": days_format(user["lastlogon"]), "Creation date": days_format(user["whencreated"]), } diff --git a/ad_miner/sources/modules/controls/azure_last_passwd_change.py b/ad_miner/sources/modules/controls/azure_last_passwd_change.py index e3de3737..afa44b23 100644 --- a/ad_miner/sources/modules/controls/azure_last_passwd_change.py +++ b/ad_miner/sources/modules/controls/azure_last_passwd_change.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import presence_of @@ -49,7 +49,7 @@ def run(self): self.azure_last_passwd_change_strange.append(user) data.append( { - "Name": ' ' + user["Name"], + "Name": ' ' + escape_html(user["Name"]), "Last password set on premise": days_format(onprem), "Last password set on Azure": days_format(onazure), "Difference": days_format(diff), diff --git a/ad_miner/sources/modules/controls/azure_reset_passwd.py b/ad_miner/sources/modules/controls/azure_reset_passwd.py index 0c6d923e..34d07725 100644 --- a/ad_miner/sources/modules/controls/azure_reset_passwd.py +++ b/ad_miner/sources/modules/controls/azure_reset_passwd.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from hashlib import md5 @@ -65,7 +65,7 @@ def run(self): data.append( { - "Privileged user": ' ' + user, + "Privileged user": ' ' + escape_html(user), "Passwords that can be reset": grid_data_stringify( { "link": f"passwords_reset_{hash}.html", diff --git a/ad_miner/sources/modules/controls/can_dcsync.py b/ad_miner/sources/modules/controls/can_dcsync.py index 4783fe08..d7c2f6e5 100644 --- a/ad_miner/sources/modules/controls/can_dcsync.py +++ b/ad_miner/sources/modules/controls/can_dcsync.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.graph_class import Graph -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from urllib.parse import quote @@ -117,9 +117,9 @@ def genFullDCSync(self): sortClass = str(len(paths_left)).zfill(6) data.append( { - "domain": ' ' + n.domain, - "type": type_icon + " " + n.labels, - "name": name_icon + " " + n.name, + "domain": ' ' + escape_html(n.domain), + "type": type_icon + " " + escape_html(n.labels), + "name": name_icon + " " + escape_html(n.name), "path to account": grid_data_stringify( { "link": "path_to_%s_with_dcsync.html" % quote(str(n.name)), diff --git a/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py b/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py index 4d4098c0..910bec63 100644 --- a/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py +++ b/ad_miner/sources/modules/controls/can_read_gmsapassword_of_adm.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.graph_class import Graph -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from urllib.parse import quote @@ -72,8 +72,8 @@ def run(self): for d in data.values(): sortClass = str(len(d["paths"])).zfill(6) tmp_grid_data = { - "domain": ' ' + d["domain"], - "name": ' ' + d["name"], + "domain": ' ' + escape_html(d["domain"]), + "name": ' ' + escape_html(d["name"]), "target": grid_data_stringify( { "value": f"{len(d['paths'])} paths to {len(d['target'])} target{'s' if len(d['target'])>1 else ''}", diff --git a/ad_miner/sources/modules/controls/can_read_laps.py b/ad_miner/sources/modules/controls/can_read_laps.py index 4f1b874c..c855657f 100644 --- a/ad_miner/sources/modules/controls/can_read_laps.py +++ b/ad_miner/sources/modules/controls/can_read_laps.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid +from ad_miner.sources.modules.utils import escape_html @register_control @@ -39,8 +40,8 @@ def run(self): grid.setheaders(["domain", "name"]) self.can_read_laps_parsed = [ { - "domain": ' ' + user["domain"], - "name": ' ' + user["name"], + "domain": ' ' + escape_html(user["domain"]), + "name": ' ' + escape_html(user["name"]), } for user in self.can_read_laps if user["domain"] is not None and user["name"] is not None diff --git a/ad_miner/sources/modules/controls/computers_admin_of_computers.py b/ad_miner/sources/modules/controls/computers_admin_of_computers.py index ebabc33e..9dbdd3c7 100644 --- a/ad_miner/sources/modules/controls/computers_admin_of_computers.py +++ b/ad_miner/sources/modules/controls/computers_admin_of_computers.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules import generic_formating, generic_computing from ad_miner.sources.modules.common_analysis import ( findAndCreatePathToDaFromComputersList, @@ -67,7 +67,7 @@ def run(self): tmp_line = { "Computer Admin": ' ' - + admin_computer, + + escape_html(admin_computer), "Computers count": grid_data_stringify( { "value": f"{computers_admin_to_count[admin_computer]} computers", diff --git a/ad_miner/sources/modules/controls/computers_last_connexion.py b/ad_miner/sources/modules/controls/computers_last_connexion.py index d1a8cb22..a4b20a7d 100644 --- a/ad_miner/sources/modules/controls/computers_last_connexion.py +++ b/ad_miner/sources/modules/controls/computers_last_connexion.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import percentage_superior @@ -49,7 +49,7 @@ def run(self): for c in self.computers_not_connected_since_60: data.append( { - "name": ' ' + c["name"], + "name": ' ' + escape_html(c["name"]), "Last logon": days_format(c["days"]), "Last password set": days_format(c["pwdlastset"]), "Enabled": str(c["enabled"]), diff --git a/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py b/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py index 6cca35fa..607e5a27 100644 --- a/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py +++ b/ad_miner/sources/modules/controls/computers_list_of_rdp_users.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import percentage_superior from urllib.parse import quote @@ -57,7 +57,7 @@ def run(self): for key in self.users_rdp_access_2: sortClass = str(len(self.users_rdp_access_2[key])).zfill(6) d = { - "Computers": ' ' + key, + "Computers": ' ' + escape_html(key), "Users": grid_data_stringify( { "value": f"{len(self.users_rdp_access_2[key])} Users

{self.users_rdp_access_2[key]}

", diff --git a/ad_miner/sources/modules/controls/computers_members_high_privilege.py b/ad_miner/sources/modules/controls/computers_members_high_privilege.py index 403896eb..d6aa6611 100644 --- a/ad_miner/sources/modules/controls/computers_members_high_privilege.py +++ b/ad_miner/sources/modules/controls/computers_members_high_privilege.py @@ -4,6 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -45,9 +46,9 @@ def run(self): ) grid = Grid("List of computer admins") for d in self.computers_members_high_privilege: - d["domain"] = ' ' + d["domain"] - d["computer"] = ' ' + d["computer"] - d["group"] = ' ' + d["group"] + d["domain"] = ' ' + escape_html(d["domain"]) + d["computer"] = ' ' + escape_html(d["computer"]) + d["group"] = ' ' + escape_html(d["group"]) grid.setheaders(["domain", "computer", "group"]) grid.setData(self.computers_members_high_privilege) page.addComponent(grid) diff --git a/ad_miner/sources/modules/controls/computers_os_obsolete.py b/ad_miner/sources/modules/controls/computers_os_obsolete.py index 31083034..864bd6bb 100644 --- a/ad_miner/sources/modules/controls/computers_os_obsolete.py +++ b/ad_miner/sources/modules/controls/computers_os_obsolete.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import manageComputersOs, presence_of @@ -41,7 +41,7 @@ def run(self): for computer in self.list_computers_os_obsolete: if computer["Last logon in days"] < 90: # remove ghost computers computer["Domain"] = ( - ' ' + computer["Domain"] + ' ' + escape_html(computer["Domain"]) ) computer["Last logon"] = days_format(computer["Last logon in days"]) if ( @@ -50,10 +50,10 @@ def run(self): or "2012" in computer["Operating system"] ): # Add icons whether it's a computer or a server computer["Operating system"] = ( - ' ' + computer["Operating system"] + ' ' + escape_html(computer["Operating system"]) ) computer["name"] = ( - ' ' + computer["name"] + ' ' + escape_html(computer["name"]) ) if ( "2000" in computer["Operating system"] @@ -62,10 +62,10 @@ def run(self): ): computer["Operating system"] = ( ' ' - + computer["Operating system"] + + escape_html(computer["Operating system"]) ) computer["name"] = ( - ' ' + computer["name"] + ' ' + escape_html(computer["name"]) ) cleaned_data.append(computer) diff --git a/ad_miner/sources/modules/controls/computers_without_laps.py b/ad_miner/sources/modules/controls/computers_without_laps.py index 9f59bb98..bb50c112 100644 --- a/ad_miner/sources/modules/controls/computers_without_laps.py +++ b/ad_miner/sources/modules/controls/computers_without_laps.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html @register_control @@ -66,11 +66,11 @@ def run(self): # Exclude ghost computers (last logon > 90 days) if computer["lastLogon"] < 90: tmp_dict["domain"] = ( - ' ' + computer["domain"] + ' ' + escape_html(computer["domain"]) ) tmp_dict["Last logon"] = days_format(computer["lastLogon"]) tmp_dict["name"] = ( - ' ' + computer["name"] + ' ' + escape_html(computer["name"]) ) if computer["LAPS"] == "false": tmp_dict["LAPS"] = ( diff --git a/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py b/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py index 42d71e55..0ec71e82 100644 --- a/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py +++ b/ad_miner/sources/modules/controls/cross_domain_admin_privileges.py @@ -2,7 +2,7 @@ from ad_miner.sources.modules.controls import register_control from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import createGraphPage import copy @@ -95,7 +95,7 @@ def run(self): user = key tmp_data = {} - tmp_data["user"] = ' ' + user + tmp_data["user"] = ' ' + escape_html(user) grid_list_local_admin_targets_data = [] grid_list_domain_admin_targets_data = [] # create the grid @@ -114,7 +114,7 @@ def run(self): local_distinct_ends = [] for domain in data_local_admins[key]: list_local_admin_targets_tmp_data = { - "domain": ' ' + domain + "domain": ' ' + escape_html(domain) } numberofpaths = 0 for path in data_local_admins[key][domain]: @@ -194,7 +194,7 @@ def run(self): domain_distinct_ends = [] for domain in data_domain_admins[key]: list_domain_admin_targets_tmp_data = { - "domain": ' ' + domain + "domain": ' ' + escape_html(domain) } for path in data_domain_admins[key][domain]: diff --git a/ad_miner/sources/modules/controls/da_to_da.py b/ad_miner/sources/modules/controls/da_to_da.py index 4666df24..f234fe2e 100644 --- a/ad_miner/sources/modules/controls/da_to_da.py +++ b/ad_miner/sources/modules/controls/da_to_da.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.graph_class import Graph -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from urllib.parse import quote @@ -57,7 +57,7 @@ def run(self): headers.append(domain) graphDatas[domain] = {} pathLengthss.append( - {"FROM / TO": ' ' + domain, domain: "-"} + {"FROM / TO": ' ' + escape_html(domain), domain: "-"} ) for path in paths: # headers and pathLengths share the same index and it is cheaper to use headers here @@ -70,7 +70,7 @@ def run(self): graphDatas[unknown_domain] = {} pathLengthss.append( { - "FROM / TO": ' ' + unknown_domain, + "FROM / TO": ' ' + escape_html(unknown_domain), unknown_domain: "-", } ) diff --git a/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py b/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py index 6dcc10bf..de0ab539 100644 --- a/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py +++ b/ad_miner/sources/modules/controls/dom_admin_on_non_dc.py @@ -2,7 +2,7 @@ from ad_miner.sources.modules.controls import register_control from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import createGraphPage, presence_of @@ -57,8 +57,8 @@ def run(self): data = [] for da in dico.keys(): tmp = {} - tmp["Domain"] = ' ' + dico[da]["domain"] - tmp["Domain Admin"] = ' ' + da + tmp["Domain"] = ' ' + escape_html(dico[da]["domain"]) + tmp["Domain Admin"] = ' ' + escape_html(da) nb_computers = len(dico[da]["computers"]) tmp["Computers"] = grid_data_stringify( { @@ -103,7 +103,7 @@ def run(self): ) computer_list_grid.setheaders(["Computer"]) computer_list_data = [ - {"Computer": ' ' + c} + {"Computer": ' ' + escape_html(c)} for c in dico[da]["computers"] ] computer_list_grid.setData(computer_list_data) @@ -122,7 +122,7 @@ def run(self): ) domain_list_grid.setheaders(["Domain"]) domain_list_data = [ - {"Domain": ' ' + c} for c in domain_list + {"Domain": ' ' + escape_html(c)} for c in domain_list ] domain_list_grid.setData(domain_list_data) domain_list_page.addComponent(domain_list_grid) diff --git a/ad_miner/sources/modules/controls/dormants_accounts.py b/ad_miner/sources/modules/controls/dormants_accounts.py index 1ca607d9..82872ef0 100644 --- a/ad_miner/sources/modules/controls/dormants_accounts.py +++ b/ad_miner/sources/modules/controls/dormants_accounts.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import percentage_superior @@ -50,15 +50,15 @@ def run(self): data = [] for dict in self.users_dormant_accounts: - tmp_data = {"domain": ' ' + dict["domain"]} + tmp_data = {"domain": ' ' + escape_html(dict["domain"])} tmp_data["name"] = ( ( ' ' - + dict["name"] + + escape_html(dict["name"]) ) if dict["name"] in self.admin_list - else ' ' + dict["name"] + else ' ' + escape_html(dict["name"]) ) tmp_data["last logon"] = days_format(dict["days"]) diff --git a/ad_miner/sources/modules/controls/empty_groups.py b/ad_miner/sources/modules/controls/empty_groups.py index d796719d..476963de 100644 --- a/ad_miner/sources/modules/controls/empty_groups.py +++ b/ad_miner/sources/modules/controls/empty_groups.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid +from ad_miner.sources.modules.utils import escape_html @register_control @@ -35,7 +36,7 @@ def run(self): headers = ["Empty group", "Full Reference"] for d in self.empty_groups: - d["Empty group"] = ' ' + d["Empty group"] + d["Empty group"] = ' ' + escape_html(d["Empty group"]) grid.setheaders(headers) grid.setData(self.empty_groups) diff --git a/ad_miner/sources/modules/controls/empty_ous.py b/ad_miner/sources/modules/controls/empty_ous.py index a1c6af6b..6c379721 100644 --- a/ad_miner/sources/modules/controls/empty_ous.py +++ b/ad_miner/sources/modules/controls/empty_ous.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid +from ad_miner.sources.modules.utils import escape_html @register_control @@ -36,7 +37,7 @@ def run(self): for d in self.empty_ous: d["Empty Organizational Unit"] = ( - ' ' + d["Empty Organizational Unit"] + ' ' + escape_html(d["Empty Organizational Unit"]) ) grid.setheaders(headers) diff --git a/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py b/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py index 66b48e0a..6cdffdce 100644 --- a/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py +++ b/ad_miner/sources/modules/controls/graph_list_objects_rbcd.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules import logger from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of, createGraphPage from urllib.parse import quote @@ -161,9 +161,9 @@ def run(self): tmp_data = {} tmp_data["Domain"] = ( ' ' - + self.rbcd_graphs[object_name]["domain"] + + escape_html(self.rbcd_graphs[object_name]["domain"]) ) - tmp_data["Name"] = ' ' + object_name + tmp_data["Name"] = ' ' + escape_html(object_name) sortClass1 = str(len(self.rbcd_graphs[object_name]["paths"])).zfill(6) tmp_data["Paths to targets"] = grid_data_stringify( { diff --git a/ad_miner/sources/modules/controls/graph_path_objects_to_da.py b/ad_miner/sources/modules/controls/graph_path_objects_to_da.py index e6aeece0..b7489a66 100644 --- a/ad_miner/sources/modules/controls/graph_path_objects_to_da.py +++ b/ad_miner/sources/modules/controls/graph_path_objects_to_da.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules import logger from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of, createGraphPage from urllib.parse import quote @@ -167,7 +167,7 @@ def count_object_from_path(list_of_paths): domain = domain[0] tmp_data = {} - tmp_data[headers[0]] = ' ' + domain + tmp_data[headers[0]] = ' ' + escape_html(domain) count = count_object_from_path(self.users_to_domain[domain]) sortClass = str(count).zfill( diff --git a/ad_miner/sources/modules/controls/guest_accounts.py b/ad_miner/sources/modules/controls/guest_accounts.py index 6636303c..81c72813 100644 --- a/ad_miner/sources/modules/controls/guest_accounts.py +++ b/ad_miner/sources/modules/controls/guest_accounts.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -39,8 +40,8 @@ def run(self): data = [] for account_name, domain, is_enabled in guest_list: - tmp_data = {"domain": ' ' + domain} - tmp_data["name"] = ' ' + account_name + tmp_data = {"domain": ' ' + escape_html(domain)} + tmp_data["name"] = ' ' + escape_html(account_name) tmp_data["enabled"] = ( ' Enabled' if is_enabled diff --git a/ad_miner/sources/modules/controls/kerberoastables.py b/ad_miner/sources/modules/controls/kerberoastables.py index c0d196e9..e55c5733 100644 --- a/ad_miner/sources/modules/controls/kerberoastables.py +++ b/ad_miner/sources/modules/controls/kerberoastables.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify, days_format +from ad_miner.sources.modules.utils import grid_data_stringify, days_format, escape_html from ad_miner.sources.modules.common_analysis import containsDAs from urllib.parse import quote @@ -88,17 +88,17 @@ def run(self): if self.users_kerberoastable_users[elem]["is_Domain_Admin"] == True: self.users_kerberoastable_users[elem]["name"] = ( ' ' - + self.users_kerberoastable_users[elem]["name"] + + escape_html(self.users_kerberoastable_users[elem]["name"]) ) else: self.users_kerberoastable_users[elem]["name"] = ( ' ' - + self.users_kerberoastable_users[elem]["name"] + + escape_html(self.users_kerberoastable_users[elem]["name"]) ) data = [] for dict in self.users_kerberoastable_users: - tmp_data = {"domain": ' ' + dict["domain"]} + tmp_data = {"domain": ' ' + escape_html(dict["domain"])} tmp_data["name"] = dict["name"] tmp_data["Last password change"] = days_format(dict["pass_last_change"]) tmp_data["Account Creation Date"] = days_format(dict["accountCreationDate"]) diff --git a/ad_miner/sources/modules/controls/krb_last_change.py b/ad_miner/sources/modules/controls/krb_last_change.py index 26bf026b..5738f136 100644 --- a/ad_miner/sources/modules/controls/krb_last_change.py +++ b/ad_miner/sources/modules/controls/krb_last_change.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import time_since @@ -41,9 +41,9 @@ def run(self): data = [] for dict in self.users_krb_pwd_last_set: - tmp_data = {"domain": ' ' + dict["domain"]} + tmp_data = {"domain": ' ' + escape_html(dict["domain"])} tmp_data["name"] = ( - ' ' + dict["name"] + ' ' + escape_html(dict["name"]) ) tmp_data["Last password change"] = days_format(dict["pass_last_change"]) tmp_data["Account Creation Date"] = days_format(dict["accountCreationDate"]) diff --git a/ad_miner/sources/modules/controls/nb_domain_admins.py b/ad_miner/sources/modules/controls/nb_domain_admins.py index bbeb2174..2d0a24b2 100644 --- a/ad_miner/sources/modules/controls/nb_domain_admins.py +++ b/ad_miner/sources/modules/controls/nb_domain_admins.py @@ -5,6 +5,7 @@ from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -48,8 +49,8 @@ def run(self): for da in self.users_nb_domain_admins: tmp_data = {} - tmp_data["domain"] = ' ' + da["domain"] - tmp_data["name"] = ' ' + da["name"] + tmp_data["domain"] = ' ' + escape_html(da["domain"]) + tmp_data["name"] = ' ' + escape_html(da["name"]) tmp_data["domain admin"] = ( 'True' if "Domain Admin" in da["admin type"] diff --git a/ad_miner/sources/modules/controls/never_expires.py b/ad_miner/sources/modules/controls/never_expires.py index 1671808a..83af610f 100644 --- a/ad_miner/sources/modules/controls/never_expires.py +++ b/ad_miner/sources/modules/controls/never_expires.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import percentage_superior @@ -42,10 +42,10 @@ def run(self): if user["name"] in self.admin_list: user["name"] = ( ' ' - + user["name"] + + escape_html(user["name"]) ) else: - user["name"] = ' ' + user["name"] + user["name"] = ' ' + escape_html(user["name"]) page = Page( self.arguments.cache_prefix, "never_expires", @@ -66,7 +66,7 @@ def run(self): data = [] for dict in self.users_password_never_expires: tmp_data = { - "domain": ' ' + dict["domain"], + "domain": ' ' + escape_html(dict["domain"]), "name": dict["name"], } tmp_data["Last login"] = days_format(dict["LastLogin"]) diff --git a/ad_miner/sources/modules/controls/objects_to_adcs.py b/ad_miner/sources/modules/controls/objects_to_adcs.py index ceadab46..dd23d7e6 100644 --- a/ad_miner/sources/modules/controls/objects_to_adcs.py +++ b/ad_miner/sources/modules/controls/objects_to_adcs.py @@ -5,7 +5,7 @@ from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.graph_class import Graph -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from urllib.parse import quote @@ -71,9 +71,9 @@ def run(self): for key, paths in self.ADCS_path_sorted.items(): tmp_data = {} tmp_data["Domain"] = ( - ' ' + paths[0].nodes[-1].domain + ' ' + escape_html(paths[0].nodes[-1].domain) ) - tmp_data["Name"] = ' ' + key + tmp_data["Name"] = ' ' + escape_html(key) nb_path_to_adcs = len(paths) self.total_paths += nb_path_to_adcs sortClass = str(nb_path_to_adcs).zfill(6) diff --git a/ad_miner/sources/modules/controls/objects_to_operators_member.py b/ad_miner/sources/modules/controls/objects_to_operators_member.py index 62c27188..6c44dcb7 100644 --- a/ad_miner/sources/modules/controls/objects_to_operators_member.py +++ b/ad_miner/sources/modules/controls/objects_to_operators_member.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.graph_class import Graph -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from urllib.parse import quote @@ -49,8 +49,8 @@ def run(self): data[path.nodes[0].name]["target"].append(path.nodes[-1].name) except KeyError: data[path.nodes[0].name] = { - "domain": ' ' + path.nodes[-1].domain, - "name": ' ' + path.nodes[0].name, + "domain": ' ' + escape_html(path.nodes[-1].domain), + "name": ' ' + escape_html(path.nodes[0].name), "link": quote(str(path.nodes[0].name)), "target": [path.nodes[-1].name], "paths": [path], @@ -63,8 +63,8 @@ def run(self): KeyError ): # Really **should not** happen, but to prevent crash in case of corrupted cache/db data[path.nodes[-1].name] = { - "domain": ' ' + path.nodes[-1].domain, - "name": ' ' + path.nodes[-1].name, + "domain": ' ' + escape_html(path.nodes[-1].domain), + "name": ' ' + escape_html(path.nodes[-1].name), "link": quote(str(path.nodes[-1].name)), "target": [""], "paths": [path], diff --git a/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py b/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py index c9f43c00..fc2e4e95 100644 --- a/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py +++ b/ad_miner/sources/modules/controls/pre_windows_2000_compatible_access_group.py @@ -4,6 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules import generic_formating +from ad_miner.sources.modules.utils import escape_html @register_control @@ -56,12 +57,12 @@ def run(self): data = [] for domain, account_name, objectid, type_list in sorted_list: - tmp_data = {"Domain": ' ' + domain} + tmp_data = {"Domain": ' ' + escape_html(domain)} type_clean = generic_formating.clean_label(type_list) tmp_data["Name"] = ( - f"{generic_formating.get_label_icon(type_clean)} {account_name}" + f"{generic_formating.get_label_icon(type_clean)} {escape_html(account_name)}" ) tmp_data["Rating"] = ( diff --git a/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py b/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py index 7dbf7abd..10d040e8 100644 --- a/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py +++ b/ad_miner/sources/modules/controls/primaryGroupID_lower_than_1000.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import MODULES_DIRECTORY +from ad_miner.sources.modules.utils import MODULES_DIRECTORY, escape_html import json @@ -54,21 +54,21 @@ def run(self): tmp_data = {} if str(rid) not in known_RIDs: - tmp_data["domain"] = ' ' + domain + tmp_data["domain"] = ' ' + escape_html(domain) tmp_data["RID"] = str(rid) tmp_data["name"] = ( - ' ' + name if is_da else name + ' ' + escape_html(name) if is_da else escape_html(name) ) tmp_data["reason"] = "Unknown RID" data.append(tmp_data) elif name_without_domain not in known_RIDs[str(rid)]: - tmp_data["domain"] = ' ' + domain + tmp_data["domain"] = ' ' + escape_html(domain) tmp_data["RID"] = str(rid) tmp_data["name"] = ( - ' ' + name if is_da else name + ' ' + escape_html(name) if is_da else escape_html(name) ) tmp_data["reason"] = ( - "Unexpected name, expected : " + known_RIDs[str(rid)][0] + "Unexpected name, expected : " + escape_html(known_RIDs[str(rid)][0]) ) data.append(tmp_data) diff --git a/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py b/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py index 4e2dbcec..8f1a1f5d 100644 --- a/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py +++ b/ad_miner/sources/modules/controls/privileged_accounts_outside_Protected_Users.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.common_analysis import presence_of +from ad_miner.sources.modules.utils import escape_html @register_control @@ -55,8 +56,8 @@ def run(self): for dic in self.users_nb_domain_admins: if "Protected Users" in dic["admin type"]: continue - tmp_data = {"domain": ' ' + dic["domain"]} - tmp_data["name"] = ' ' + dic["name"] + tmp_data = {"domain": ' ' + escape_html(dic["domain"])} + tmp_data["name"] = ' ' + escape_html(dic["name"]) tmp_data["domain admin"] = ( 'True' if "Domain Admin" in dic["admin type"] diff --git a/ad_miner/sources/modules/controls/up_to_date_admincount.py b/ad_miner/sources/modules/controls/up_to_date_admincount.py index 3f6558ff..1963ed00 100644 --- a/ad_miner/sources/modules/controls/up_to_date_admincount.py +++ b/ad_miner/sources/modules/controls/up_to_date_admincount.py @@ -2,6 +2,7 @@ from ad_miner.sources.modules.controls import register_control from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid +from ad_miner.sources.modules.utils import escape_html @register_control @@ -56,8 +57,8 @@ def run(self): for dic in self.users_nb_domain_admins: if dic["admincount"]: continue - tmp_data = {"domain": ' ' + dic["domain"]} - tmp_data["name"] = ' ' + dic["name"] + tmp_data = {"domain": ' ' + escape_html(dic["domain"])} + tmp_data["name"] = ' ' + escape_html(dic["name"]) tmp_data["domain admin"] = ( 'True' if "Domain Admin" in dic["admin type"] @@ -94,8 +95,8 @@ def run(self): data.append(tmp_data) for name, domain, da_type in self.unpriviledged_users_with_admincount: - tmp_data = {"domain": ' ' + domain} - tmp_data["name"] = ' ' + name + tmp_data = {"domain": ' ' + escape_html(domain)} + tmp_data["name"] = ' ' + escape_html(name) tmp_data["domain admin"] = '' tmp_data["schema admin"] = '' tmp_data["enterprise admin"] = '' diff --git a/ad_miner/sources/modules/controls/users_admin_of_computers.py b/ad_miner/sources/modules/controls/users_admin_of_computers.py index ca6e7c0f..28b34124 100644 --- a/ad_miner/sources/modules/controls/users_admin_of_computers.py +++ b/ad_miner/sources/modules/controls/users_admin_of_computers.py @@ -8,7 +8,7 @@ from ad_miner.sources.modules.path_neo4j import Path from ad_miner.sources.modules import generic_computing -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import ( findAndCreatePathToDaFromUsersList, hasPathToDA, @@ -306,7 +306,7 @@ def get_last_pass_change(account): headers[ 0 ]: ' ' - + dict[headers[0]], + + escape_html(dict[headers[0]]), headers[1]: dict[headers[1]], headers[2]: dict[headers[2]], headers[3]: data_header_computer, @@ -318,7 +318,7 @@ def get_last_pass_change(account): formated_data.append( { headers[0]: ' ' - + dict[headers[0]], + + escape_html(dict[headers[0]]), headers[1]: dict[headers[1]], headers[2]: dict[headers[2]], headers[3]: data_header_computer, diff --git a/ad_miner/sources/modules/controls/users_password_not_required.py b/ad_miner/sources/modules/controls/users_password_not_required.py index faaac8bc..eff90236 100644 --- a/ad_miner/sources/modules/controls/users_password_not_required.py +++ b/ad_miner/sources/modules/controls/users_password_not_required.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import presence_of @@ -41,8 +41,8 @@ def run(self): grid_data = [] for dic in self.users_password_not_required: tmp_data = {} - tmp_data["Domain"] = ' ' + dic["domain"] - tmp_data["User"] = ' ' + dic["user"] + tmp_data["Domain"] = ' ' + escape_html(dic["domain"]) + tmp_data["User"] = ' ' + escape_html(dic["user"]) tmp_data["Password last change"] = days_format(dic["pwdlastset"]) tmp_data["Last logon"] = days_format(dic["lastlogon"]) grid_data.append(tmp_data) diff --git a/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py b/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py index e949231b..6adaa259 100644 --- a/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py +++ b/ad_miner/sources/modules/controls/users_pwd_not_changed_since.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import days_format +from ad_miner.sources.modules.utils import days_format, escape_html from ad_miner.sources.modules.common_analysis import percentage_superior @@ -69,11 +69,11 @@ def run(self): if dict["user"] in self.admin_list: tmp_data["user"] = ( ' ' - + tmp_data["user"] + + escape_html(tmp_data["user"]) ) else: tmp_data["user"] = ( - ' ' + tmp_data["user"] + ' ' + escape_html(tmp_data["user"]) ) tmp_data["Last password change"] = days_format(dict["days"]) tmp_data["Account Creation Date"] = days_format(dict["accountCreationDate"]) diff --git a/ad_miner/sources/modules/controls/users_rdp_access.py b/ad_miner/sources/modules/controls/users_rdp_access.py index 7e46aa18..de05e38c 100644 --- a/ad_miner/sources/modules/controls/users_rdp_access.py +++ b/ad_miner/sources/modules/controls/users_rdp_access.py @@ -3,7 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import percentage_superior from urllib.parse import quote @@ -48,7 +48,7 @@ def run(self): for key in self.users_rdp_access_1: sortClass = str(len(self.users_rdp_access_1[key])).zfill(6) d = { - "Users": ' ' + key, + "Users": ' ' + escape_html(key), "Computers": grid_data_stringify( { "value": f"{len(self.users_rdp_access_1[key])} Computers

{self.users_rdp_access_1[key]}

", diff --git a/ad_miner/sources/modules/controls/users_shadow_credentials.py b/ad_miner/sources/modules/controls/users_shadow_credentials.py index cfa79824..e1c9db78 100644 --- a/ad_miner/sources/modules/controls/users_shadow_credentials.py +++ b/ad_miner/sources/modules/controls/users_shadow_credentials.py @@ -5,7 +5,7 @@ from ad_miner.sources.modules.grid_class import Grid from ad_miner.sources.modules.graph_class import Graph -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of from urllib.parse import quote @@ -59,8 +59,8 @@ def run(self): for d in data.values(): sortClass = str(len(d["paths"])).zfill(6) tmp_grid_data = { - "domain": ' ' + d["domain"], - "name": ' ' + d["name"], + "domain": ' ' + escape_html(d["domain"]), + "name": ' ' + escape_html(d["name"]), "target": grid_data_stringify( { "value": f"{len(d['paths'])} paths to {len(d['target'])} target{'s' if len(d['target'])>1 else ''}", diff --git a/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py b/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py index a51bae38..9de0e0b2 100644 --- a/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py +++ b/ad_miner/sources/modules/controls/users_shadow_credentials_to_non_admins.py @@ -4,7 +4,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.graph_class import Graph from ad_miner.sources.modules.grid_class import Grid -from ad_miner.sources.modules.utils import grid_data_stringify +from ad_miner.sources.modules.utils import grid_data_stringify, escape_html from ad_miner.sources.modules.common_analysis import presence_of @@ -53,9 +53,9 @@ def run(self): sortClass = str(nb_paths).zfill(6) grid_data.append( { - "domain": ' ' + data[target]["domain"], + "domain": ' ' + escape_html(data[target]["domain"]), "target": ' ' - + data[target]["target"], + + escape_html(data[target]["target"]), "paths": grid_data_stringify( { "value": f"{nb_paths} paths to target", diff --git a/ad_miner/sources/modules/controls/vuln_functional_level.py b/ad_miner/sources/modules/controls/vuln_functional_level.py index e01ac81e..d7800d97 100644 --- a/ad_miner/sources/modules/controls/vuln_functional_level.py +++ b/ad_miner/sources/modules/controls/vuln_functional_level.py @@ -3,6 +3,7 @@ from ad_miner.sources.modules.page_class import Page from ad_miner.sources.modules.grid_class import Grid +from ad_miner.sources.modules.utils import escape_html @register_control diff --git a/ad_miner/sources/modules/main_page.py b/ad_miner/sources/modules/main_page.py index ec12526d..5d48934d 100644 --- a/ad_miner/sources/modules/main_page.py +++ b/ad_miner/sources/modules/main_page.py @@ -8,7 +8,7 @@ from ad_miner.sources.modules import common_analysis from ad_miner.sources.modules.smolcard_class import SmolCard -from ad_miner.sources.modules.utils import TEMPLATES_DIRECTORY +from ad_miner.sources.modules.utils import TEMPLATES_DIRECTORY, escape_html def americanStyle(n: int) -> str: @@ -562,14 +562,16 @@ def render( ] = "" red_status = f""" {risk_control.replace("_", " ").capitalize()}""" for issue in data[category_repartition][f"{risk_control}_list"]: - custom_title = dico_name_description[issue].replace("$", "") + custom_title = escape_html(dico_name_description[issue].replace("$", "")) + card_title = escape_html(dico_name_title[issue]) + escaped_red_status = escape_html(red_status) data[category_repartition][ global_risk_controls[risk_control]["panel_key"] ] += f""" -
+
-
{dico_name_title[issue]}
+
{card_title}
diff --git a/ad_miner/sources/modules/safe_pickle.py b/ad_miner/sources/modules/safe_pickle.py new file mode 100644 index 00000000..6c83ebf5 --- /dev/null +++ b/ad_miner/sources/modules/safe_pickle.py @@ -0,0 +1,53 @@ +""" +Secure pickle deserialization module. + +This module provides a RestrictedUnpickler that only allows deserialization +of safe, whitelisted classes to prevent arbitrary code execution attacks. +""" + +import pickle + +# Whitelist of allowed modules and classes for deserialization +SAFE_MODULES = { + 'builtins': {'dict', 'list', 'set', 'frozenset', 'tuple', 'str', 'int', 'float', 'bool', 'bytes', 'type', 'NoneType'}, + 'datetime': {'datetime', 'date', 'time', 'timedelta', 'timezone'}, + 'neo4j.time': {'DateTime', 'Date', 'Time', 'Duration'}, + 'neo4j.graph': {'Node', 'Relationship', 'Path'}, + 'collections': {'OrderedDict', 'defaultdict'}, + # AD_Miner internal data classes (safe - no dangerous methods) + 'ad_miner.sources.modules.path_neo4j': {'Path'}, + 'ad_miner.sources.modules.node_neo4j': {'Node'}, +} + + +class RestrictedUnpickler(pickle.Unpickler): + """ + A restricted unpickler that only allows deserialization of whitelisted classes. + + This prevents arbitrary code execution through malicious pickle files by + raising an error when an unauthorized class is encountered. + """ + + def find_class(self, module, name): + if module in SAFE_MODULES and name in SAFE_MODULES[module]: + return super().find_class(module, name) + raise pickle.UnpicklingError( + f"Unauthorized class: {module}.{name}. " + "Only whitelisted classes can be deserialized for security reasons." + ) + + +def safe_load(file): + """ + Safely load a pickle file using the RestrictedUnpickler. + + Args: + file: A file-like object opened in binary mode. + + Returns: + The deserialized Python object. + + Raises: + pickle.UnpicklingError: If an unauthorized class is encountered. + """ + return RestrictedUnpickler(file).load() diff --git a/ad_miner/sources/modules/table_class.py b/ad_miner/sources/modules/table_class.py index d269d080..4f59b8a9 100755 --- a/ad_miner/sources/modules/table_class.py +++ b/ad_miner/sources/modules/table_class.py @@ -1,7 +1,7 @@ import random import string -from ad_miner.sources.modules.utils import HTML_DIRECTORY +from ad_miner.sources.modules.utils import HTML_DIRECTORY, escape_html class Table: def __init__( @@ -41,7 +41,7 @@ def render(self, page_f): with open( self.template_base_path / (self.template + "_header.html"), "r" ) as header_f: - page_f.write(header_f.read() % (self.id, self.id, self.title, self.id)) + page_f.write(header_f.read() % (self.id, self.id, escape_html(self.title), self.id)) page_f.write('\n\n') @@ -51,7 +51,7 @@ def render(self, page_f): line = row_header_f.read() for header in self.headers: - page_f.write(line % ("th", 'scope="col"', header, "th")) + page_f.write(line % ("th", 'scope="col"', escape_html(header), "th")) page_f.write("\n\n\n") @@ -60,10 +60,11 @@ def render(self, page_f): page_f.write("\n") for index, col in enumerate(row): + escaped_col = escape_html(col) if index == 0: - page_f.write(line % ("th", 'scope="row"', col, "th")) + page_f.write(line % ("th", 'scope="row"', escaped_col, "th")) else: - page_f.write(line % ("td", "", col, "td")) + page_f.write(line % ("td", "", escaped_col, "td")) page_f.write("\n") page_f.write("\n") diff --git a/ad_miner/sources/modules/utils.py b/ad_miner/sources/modules/utils.py index 3468b7bb..882dc16b 100755 --- a/ad_miner/sources/modules/utils.py +++ b/ad_miner/sources/modules/utils.py @@ -1,4 +1,5 @@ import argparse +import html from pathlib import Path import multiprocessing as mp import json @@ -6,6 +7,21 @@ from datetime import date from os.path import sep + +def escape_html(text): + """ + Escape HTML special characters to prevent XSS attacks. + + Args: + text: The text to escape. Can be None or any type. + + Returns: + A string with HTML special characters escaped. + """ + if text is None: + return "" + return html.escape(str(text), quote=True).replace("'", "'") + today = date.today() current_date = today.strftime("%Y%m%d") @@ -171,11 +187,13 @@ def grid_data_stringify(raw_data: dict) -> str: "before_link" } """ - link = raw_data['link'].replace(sep, '_').replace('/', '_') + link = escape_html(raw_data['link'].replace(sep, '_').replace('/', '_')) + escaped_value = escape_html(raw_data['value']) try: - return f"{raw_data['before_link']}
{raw_data['value']} " + escaped_before = escape_html(raw_data['before_link']) + return f"{escaped_before} {escaped_value} " except KeyError: - return f"{raw_data['value']} " + return f"{escaped_value} " def cache_check(template:str,cache:bool) -> dict: res={'nb_cache':0}