├── .gitignore ├── README.adoc ├── configs ├── hijack.dll ├── namenotfound.pmc └── priv-esc-hunt.pmc ├── docs ├── acl_analysis.png ├── low_fruit_report.png └── procmon_report.png ├── requirements.txt ├── search.py └── src ├── __init__.py ├── analyze.py ├── low_fruit.py ├── permissions.py ├── reporting.py └── windows_objects.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | test 50 | out 51 | .vscode 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Windows Hunt 2 | 3 | v1.0.0 4 | :toc: 5 | 6 | == Concept 7 | This tool/package is designed to help Analyze/Hunt Windows Systems for misconfigurations, exposed credentials, privilege escalation paths, and much more. 8 | 9 | This is *not* a Post-Exploitation tool considering Python is not native to Windows Systems unless you want to be load as hell to install Python and subsequent packages. This was designed be used more as a Hunt Tool for your Home / Company system builds and environments. 10 | 11 | === Capabilities 12 | 13 | . Procmon.exe output Analysis to hunt for weak ACLs on all loaded / access objects. 14 | . Recursive ACL analysis given a path (i.e., C:\Program Files) to report on weak permissions. 15 | . Low Hanging Fruit Analysis: 16 | .. _Analyze Windows Services_ - Unquoted Service Paths, Can user edit `binpath`, and ACL analysis. 17 | .. _Analyze Windows Scheduled Tasks_ - ACL analysis on Task Command (i.e., can we edit it?) 18 | .. _Analyze Known Registry Keys_ - Looks for registry keys known to harbor credentials / juicy data. 19 | .. _Analyze Message Event Dlls_ - Check ACL permissions on all Message Event Dlls within the Registry. 20 | .. _Full System Scan for Credentials_ - Recursively enumerates several file types (ini, txt, conf, etc.) and looks for passwords. 21 | . Report Generation in either JSON or EXCEL 22 | 23 | 24 | == Usage 25 | 26 | [code, raw] 27 | ---- 28 | usage: search.py [-h] [-p | -f | -F] [-r] [-t] -o 29 | 30 | optional arguments: 31 | -h, --help show this help message and exit 32 | -p, --procmon Path to the Procmon Output File (CSV) 33 | -f, --files Analyze all files (Recursive) given a path 34 | -F, --fruit Run a full analysis (Low Hanging Fruit) 35 | -r, --report Output Format of Final Report [Excel, JSON] 36 | -t, --threads Defined number of threads (Max 100). Default=10 37 | -o, --out Output location for results. 38 | ---- 39 | 40 | === Resources 41 | 42 | Within the directory there are three (3) different files: 43 | 44 | . *namenotfound.pmg* - Procmon.exe configruation to hunt for Name Not Found corresponding to DLLs. 45 | . *priv-esc-hunt* - Review all SYSTEM and HIGH intgreity processes, enumerate HKLM keys, DLLs, and EXEs accessed/loaded. 46 | . *hijack.dll* - 32-bit DLL developed in C++ that presents a MessageBox that displays _Hijacked_ if the DLL is loaded. Useful for a quick PoC. 47 | 48 | === Hunt Topics 49 | 50 | * *Analyze a Process Chain* - A user can utilize `Procmon.exe` to analyze a massive set of processes or narrow very granularity on much smaller scope (i.e., one process). Once the user has determined enough data has been captured, the final Procmon capture can be dumped to a CSV file and loaded into `Search.py` for a full ACL analysis. 51 | ** *Command:* _python3 search.py -p procmon_output.csv -o _ 52 | ** *Purpose:* Analyze processes and loaded objects (i.e., HKLM, DLL's, EXE's, etc.), review them to determine misconfigurations and weak ACL permissions. This is incredibly useful to determine boot vulnerabilities as well as individual software ACL vulnerabilities. 53 | ** *Report:* Excel Document 54 | +++ 55 |
56 |

57 | Procmon Analysis 58 |

59 |
60 | +++ 61 | 62 | * *Recursive ACL Analysis* - Given a file path, enumerate all directories and files, pull their ACLs, review for weak permissions. 63 | ** *Command:* _python search.py -f "C:\Program Files (x86)" -o _ 64 | ** *Purpose:* Look for directories and files that have weak permissions. This feature is designed to hunt for possible privilege escallation paths such as the link:https://remoteawesomethoughts.blogspot.com/2019/05/windows-10-task-schedulerservice.html[Task Manager DLL Hijack]. 65 | ** *Report:* Excel Document 66 | +++ 67 |
68 |

69 | ACL Analysis 70 |

71 |
72 | +++ 73 | 74 | * *Low Hanging Fruit* - Conducts a number of checks that most attackers enumerate during post-exploitation. This is the easily overlooked / misconfiguration checks. 75 | ** *Command:* _python search.py -F -o -r [JSON,EXCEL]_ 76 | ** *Purpose:* Look for all the simple yet, highly probable, misconfigurations, weak permissions, exposed credentials within System Services, Tasks, Registry Keys, Dlls and known configuration file types. 77 | ** *Report:* JSON or EXCEL 78 | +++ 79 |
80 |

81 | Low Hanging Fruit 82 |

83 |
84 | +++ 85 | -------------------------------------------------------------------------------- /configs/hijack.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/configs/hijack.dll -------------------------------------------------------------------------------- /configs/namenotfound.pmc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/configs/namenotfound.pmc -------------------------------------------------------------------------------- /configs/priv-esc-hunt.pmc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/configs/priv-esc-hunt.pmc -------------------------------------------------------------------------------- /docs/acl_analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/docs/acl_analysis.png -------------------------------------------------------------------------------- /docs/low_fruit_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/docs/low_fruit_report.png -------------------------------------------------------------------------------- /docs/procmon_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/docs/procmon_report.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | astroid==2.3.3 2 | beautifulsoup4==4.8.2 3 | lxml==4.6.3 4 | colorama==0.4.3 5 | et-xmlfile==1.0.1 6 | isort==4.3.21 7 | jdcal==1.4.1 8 | lazy-object-proxy==1.4.3 9 | mccabe==0.6.1 10 | numpy==1.18.1 11 | openpyxl==3.0.3 12 | pandas==1.0.2 13 | pylint==2.4.4 14 | python-dateutil==2.8.1 15 | pytz==2019.3 16 | pywin32==227 17 | six==1.14.0 18 | tqdm==4.43.0 19 | wrapt==1.11.2 20 | tabulate==0.8.7 21 | -------------------------------------------------------------------------------- /search.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import csv 4 | import argparse 5 | import linecache 6 | import pandas as pd 7 | from tabulate import tabulate 8 | from src import analyze, low_fruit, reporting 9 | from colorama import Fore, init 10 | init() 11 | 12 | 13 | # ==========================================================# 14 | # Windows Hunt: This script / set of scripts is designed # 15 | # to take the CSV output of a Procmon.exe # 16 | # sessions and analyze HKLM Registry keys, # 17 | # File, and Filepaths that were accessed by a # 18 | # High/System integrity context. The analysis # 19 | # is conducted by pulling the objects current # 20 | # DACL via the win32api. # 21 | # # 22 | # Author: @Jfaust0 # 23 | # Site: SevroSecurity.com # 24 | # ==========================================================# 25 | 26 | 27 | def print_exception(): 28 | exc_type, exc_obj, tb = sys.exc_info() 29 | tmp_file = tb.tb_frame 30 | lineno = tb.tb_lineno 31 | filename = tmp_file.f_code.co_filename 32 | linecache.checkcache(filename) 33 | line = linecache.getline(filename, lineno, tmp_file.f_globals) 34 | print( 35 | f"{Fore.RED}EXCEPTION IN: {Fore.GREEN}{filename}\n\t[i] LINE: {lineno}, {line.strip()}\n\t[i] OBJECT: {exc_obj}{Fore.RESET}" 36 | ) 37 | 38 | 39 | def testing(): 40 | pass 41 | 42 | 43 | 44 | # =======================================# 45 | # MAIN # 46 | # =======================================# 47 | if __name__ == "__main__": 48 | try: 49 | 50 | parser = argparse.ArgumentParser() 51 | me = parser.add_mutually_exclusive_group() 52 | me.add_argument( 53 | "-p", 54 | "--procmon", 55 | dest="p", 56 | default=None, 57 | metavar="", 58 | required=False, 59 | help="Path to the Procmon Output File (CSV)", 60 | ) 61 | me.add_argument( 62 | "-f", 63 | "--files", 64 | dest="analysis_path", 65 | default=None, 66 | metavar='', 67 | required=False, 68 | help="Analyze all files (Recursive) given a path" 69 | ) 70 | me.add_argument( 71 | "-F", 72 | "--fruit", 73 | action="store_true", 74 | dest="fruit", 75 | required=False, 76 | help="Run a full analysis (Low Hanging Fruit)" 77 | ) 78 | parser.add_argument( 79 | "-r", 80 | "--report", 81 | default="excel", 82 | metavar="", 83 | dest="report", 84 | required=False, 85 | help="Output Format of Final Report [Excel, JSON]" 86 | ) 87 | parser.add_argument( 88 | "-t", 89 | "--threads", 90 | metavar="", 91 | dest="t", 92 | type=int, 93 | default=10, 94 | required=False, 95 | help="Defined number of threads (Max 100). Default=10", 96 | ) 97 | parser.add_argument( 98 | "-o", 99 | "--out", 100 | dest="o", 101 | metavar="", 102 | required=True, 103 | help="Output location for results.", 104 | ) 105 | args = parser.parse_args() 106 | 107 | # Check to make sure output path is valid: 108 | if not os.path.exists(args.o): 109 | print(f"[!] {args.o} does not exist") 110 | exit(1) 111 | 112 | # Check report type: 113 | good_types = ["excel","json"] 114 | if (str(args.report).lower() not in good_types): 115 | print(f"[!] Error: output report type {args.report} is not supported. Use json or excel.") 116 | exit(1) 117 | 118 | 119 | if (args.p != None): 120 | 121 | # Check to make sure Procmon File is CSV: 122 | with open(args.p, "r") as f: 123 | if not csv.Sniffer().has_header(f.read(2014)): 124 | print(f"[!] {str(args.p).strip()} is not a CSV file.") 125 | exit(1) 126 | 127 | # Start the Enumeration. 128 | a = analyze.analyze(args.o) # Analysis Class Object 129 | parsed_df = a.parse_procmon_csv(args.p) # parse the Procmon CSV (Return: Pandas DataFrame) 130 | total_analyzed = a.build_command_list_procmon(args.t, parsed_df) # Pull all ACLs for paths (threaded) 131 | interesting_items = a.analyze_acls_procmon() # Analyze all the enumerate ACLS. 132 | 133 | print("-" * 125) 134 | print(f"\n[i] A total of {total_analyzed} objects Were Analyzed.") 135 | print(f"[i] {interesting_items} Were found to have Write or Fullcontol Permissions.") 136 | print(f"[i] {a.get_error_count()} ERRORS occurred during the analysis.") 137 | print("[i] Output Files:") 138 | print(f"\t+ {args.o}evil.xlsx:\t\tKeys denoted as improperly configured/interesting") 139 | print(f"\t+ {args.o}errors.txt:\t\tDetails of all errors observed") 140 | 141 | 142 | if (args.analysis_path != None): 143 | a = analyze.analyze(args.o) # Analysis Class Object 144 | total_analyzed = a.build_command_list_path(args.t, args.analysis_path) 145 | interesting_items = a.analyze_acls_path() 146 | 147 | print("-" * 125) 148 | print(f"\n[i] A total of {total_analyzed} objects Were Analyzed.") 149 | print(f"[i] {interesting_items} Were found to have Write or FullContol Permissions.") 150 | print(f"[i] {a.get_error_count()} ERRORS occurred during the analysis.") 151 | print("[i] Output Files:") 152 | print(f"\t+ {args.o}evil.xlsx:\t\tKeys denoted as improperly configured/interesting") 153 | print(f"\t+ {args.o}errors.txt:\t\tDetails of all errors observed") 154 | 155 | 156 | if (args.fruit): 157 | low = low_fruit.low_haning_fruit(args.o) 158 | rep = reporting.report(args.report, args.o) 159 | 160 | # Analyze System Services 161 | service_analysis = low.analyze_all_services() 162 | # Analyze Scheduled Tasks 163 | tasks_analysis = low.analyze_scheduled_tasks() 164 | # Analyze Registry Keys: 165 | registry_analysis = low.registry_analysis() 166 | # Analyze Event Message Logging DLLs: 167 | message_analysis = low.message_event_analysis() 168 | # Analyze all files for credentials 169 | credential_analysis = low.look_for_credentials() 170 | 171 | report_table = rep.generate_fruit_report(service_analysis, tasks_analysis, registry_analysis, message_analysis, credential_analysis) 172 | print("\n\n") 173 | print(tabulate(report_table, headers='keys', tablefmt="psql")) 174 | 175 | exit(0) 176 | 177 | except Exception as e: 178 | print_exception() 179 | exit(1) 180 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshfaust/Windows_Hunt/baf2908f8c7f5a8a6b38c846d4db90eca61383ca/src/__init__.py -------------------------------------------------------------------------------- /src/analyze.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import io 4 | import sys 5 | import time 6 | import getpass 7 | import hashlib 8 | import threading 9 | import linecache 10 | import pandas as pd 11 | import concurrent.futures 12 | from . import permissions, windows_objects 13 | from tqdm import tqdm 14 | from colorama import Fore, init 15 | 16 | init() 17 | 18 | # --------------------------------------------------# 19 | # Name: Analysis Class # 20 | # Purpose: Conduct the overall DACL analysis # 21 | # Author: @cribdragg3r # 22 | # Website: sevrosecurity.com # 23 | # --------------------------------------------------# 24 | 25 | 26 | class analyze: 27 | 28 | # o_dir = output directory 29 | # initialize = do you want to initialize all write objects 30 | def __init__(self, o_dir): 31 | self.name = "Analysis" 32 | self.__procmon_analysis = [] # A list of dictionaries 33 | self.__path_analysis = [] 34 | self.__output_dir = o_dir 35 | self.__final_report = f"{self.__output_dir}/evil.xlsx" 36 | self.__permission_enum = permissions.permissions(self.__output_dir) 37 | self.__username = str(getpass.getuser()).lower() 38 | 39 | # ==========================================================# 40 | # Purpose: loads raw procmon csv output into a Pandas # 41 | # dataframe, removed duplicates, and outputs all # 42 | # cleaned / de-duplicated objects to # 43 | # cleaned_paths.txt # 44 | # Return: Pandas Dataframe # 45 | # ==========================================================# 46 | def parse_procmon_csv(self, p_file): 47 | try: 48 | # Names we do not want to enumerate 49 | bad_process_names = [ 50 | "conhost.exe", 51 | "dem.exe", 52 | "svchost.exe", 53 | "procmon64.exe" 54 | ] 55 | # Operations we don't care about 56 | bad_operation_names = [ 57 | "regclosekey", 58 | "regqueryvalue", 59 | "regenumvalue", 60 | "regquerykeysecurity", 61 | "regquerykey", 62 | ] 63 | 64 | # Dataframe to hold the parsed procmon data: 65 | output_dataframe = pd.DataFrame(columns=["process_name", "orig_path", "clean_path", "operation", "integrity"]) 66 | 67 | input_dataframe = pd.read_csv(p_file) # Read Procmon.CSV data into dataframe 68 | dataframe_length = input_dataframe.shape[0] # Size of procmon dataframe 69 | deduplication = (set()) # holds hashes of previously added paths to avoid duplication 70 | path = "" # placeholder for original paths (not cleaned) 71 | base_path = "" # placeholder for base path without exe/dll, etc 72 | clean_hash = "" # placeholder for clean hash value 73 | base_hash = "" # placeholder for base hand value 74 | pbar = tqdm(total=dataframe_length) # Progress Bar 75 | pbar.set_description("Analyzing Procmon Data") 76 | 77 | for i in range(0, dataframe_length): 78 | 79 | # Pull in dataframe content we're interested in. 80 | orig_path = str(input_dataframe["Path"][i]).lower() 81 | proc_name = str(input_dataframe["Process Name"][i]).lower() 82 | operation = str(input_dataframe["Operation"][i]).lower() 83 | integrity = str(input_dataframe["Integrity"][i]).lower() 84 | 85 | if (proc_name not in bad_process_names 86 | and operation not in bad_operation_names): 87 | 88 | # If path is a registry key: 89 | if (".exe" not in orig_path and ".dll" not in orig_path and "c:" not in orig_path): 90 | # Cleanup Registry Key Path 91 | dir_count = len(re.findall(r"\\", orig_path)) 92 | path = orig_path.split("\\") 93 | clean_path = "" 94 | 95 | for j in range(0, dir_count): 96 | if j == (dir_count - 1): 97 | clean_path += path[j] 98 | elif j == 0: 99 | clean_path += path[j] + ":\\" 100 | else: 101 | clean_path += path[j] + "\\" 102 | 103 | # If path is an executable or library: 104 | else: 105 | clean_path = orig_path 106 | 107 | # Avoid issues with rundll32.exe 108 | if ("rundll32.exe c:" in path): 109 | clean_path = clean_path.split("rundll32.exe ")[1] 110 | 111 | # Avoid Issues with CLI arguments: 112 | if (".exe -" in clean_path): 113 | clean_path = clean_path.split(" -")[0] 114 | 115 | base_path = os.path.dirname(clean_path) 116 | 117 | clean_hash = hashlib.md5(clean_path.encode("utf-8")).hexdigest() # MD5 of the Cleaned Path 118 | base_hash = hashlib.md5(base_path.encode("utf-8")).hexdigest() # MD5 of the Base Path 119 | 120 | # Make sure this is not a duplicate key before saving 121 | if clean_hash not in deduplication and len(clean_path) > 4: 122 | # Save the Cleaned path (Full Path) to the dataframe 123 | final_data = { 124 | "process_name": proc_name, 125 | "orig_path": orig_path, 126 | "clean_path": clean_path, 127 | "operation": operation, 128 | "integrity": integrity 129 | } 130 | output_dataframe = output_dataframe.append(final_data, ignore_index=True) 131 | deduplication.add(clean_hash) 132 | 133 | # Save the Base Path (no file included) to the dataframe 134 | if ((".exe" in clean_path.lower() or ".dll" in clean_path.lower()) and base_hash not in deduplication and len(clean_path) > 4): 135 | final_data = { 136 | "process_name": proc_name, 137 | "orig_path": orig_path, 138 | "clean_path": base_hash, 139 | "operation": operation, 140 | "integrity": integrity 141 | } 142 | output_dataframe = output_dataframe.append(final_data, ignore_index=True) 143 | deduplication.add(base_hash) 144 | 145 | pbar.update(1) 146 | 147 | else: 148 | pbar.update(1) 149 | 150 | pbar.close() 151 | return output_dataframe 152 | 153 | except Exception as e: 154 | self.__print_exception() 155 | 156 | # ==========================================================# 157 | # Purpose: Thread the win32api DACL lookups # 158 | # Return: None # 159 | # ==========================================================# 160 | ## build_command_list --> __thread_commands --> __get_acl_list --> __write_acl 161 | def build_command_list_procmon(self, total_threads, df): 162 | try: 163 | # DataFrame Objects 164 | total_number_of_paths = df.shape[0] 165 | 166 | commands = [None] * total_threads 167 | commands_index = 0 168 | total_commands_sent = 0 169 | 170 | pbar = tqdm(total=total_number_of_paths) 171 | pbar.set_description("Analyzing ACL's") 172 | 173 | for i in range(0, total_number_of_paths): 174 | 175 | #cleaned_data_file.write("Process Name,Original Path,Clean Path,Operation,Integrity") 176 | proc_name = str(df["process_name"][i]).lower() 177 | orig_cmd = str(df["orig_path"][i]).lower() 178 | clean_cmd = str(df["clean_path"][i]).lower() 179 | operation = str(df["operation"][i]).lower() 180 | integrity = str(df["integrity"][i]).lower() 181 | 182 | # Generate a Dictionary that will be stored in a list (I know, it's messy...) 183 | cmd = { 184 | "proc_name" : proc_name, 185 | "orig_cmd" : orig_cmd, 186 | "clean_cmd" : clean_cmd, 187 | "operation" : operation, 188 | "integrity" : integrity 189 | } 190 | 191 | # if the index is less than the requested threads, keep adding commands to the list 192 | if commands_index < total_threads: 193 | commands[commands_index] = cmd 194 | commands_index += 1 195 | total_commands_sent += 1 196 | 197 | # If commands list is full, send it off for analysis and reset 198 | if commands_index == total_threads: 199 | self.__thread_commands(commands, "procmon") # Send the data 200 | commands = [None] * total_threads # Reset list and counter 201 | commands_index = 0 202 | commands[commands_index] = cmd # Add current path to [0] place in list 203 | commands_index += 1 204 | total_commands_sent += 1 205 | 206 | pbar.update(1) 207 | 208 | # Send the last set of commands: 209 | self.__thread_commands(commands, "procmon") 210 | pbar.close() 211 | return total_number_of_paths 212 | 213 | except Exception as e: 214 | self.__print_exception() 215 | 216 | 217 | 218 | # ==========================================================# 219 | # Purpose: Thread the win32api DACL lookups # 220 | # Return: None # 221 | # ==========================================================# 222 | ## build_command_list --> __thread_commands --> __get_acl_list --> __write_acl 223 | def build_command_list_path(self, total_threads, path): 224 | try: 225 | 226 | file_paths = [] 227 | 228 | # We need to disable the file system redirects before enumerating any 229 | # privileged paths such as C:\Windows\System32. 230 | with windows_objects.disable_file_system_redirection(): 231 | for root, dirs, files in os.walk(path): 232 | for file in files: 233 | full_path = os.path.join(root, file) 234 | file_paths.append(full_path) 235 | 236 | total_number_of_paths = len(file_paths) 237 | commands = [None] * total_threads 238 | commands_index = 0 239 | total_commands_sent = 0 240 | 241 | pbar = tqdm(total=total_number_of_paths) 242 | pbar.set_description("Analyzing ACL's") 243 | 244 | for i, f_path in enumerate(file_paths): 245 | 246 | cmd = f_path.strip() 247 | 248 | # if the index is less than the requested threads, keep adding commands to the list 249 | if commands_index < total_threads: 250 | commands[commands_index] = cmd 251 | commands_index += 1 252 | total_commands_sent += 1 253 | 254 | # If commands list is full, send it off for analysis and reset 255 | if commands_index == total_threads: 256 | self.__thread_commands(commands, "path") # Send the data 257 | commands = [None] * total_threads # Reset list and counter 258 | commands_index = 0 259 | commands[commands_index] = cmd # Add current path to [0] place in list 260 | commands_index += 1 261 | total_commands_sent += 1 262 | 263 | pbar.update(1) 264 | 265 | # Send the last set of commands: 266 | self.__thread_commands(commands, "path") 267 | pbar.close() 268 | return total_number_of_paths 269 | 270 | except Exception as e: 271 | self.__print_exception() 272 | 273 | # ==============================================# 274 | # Purpose: Thread the win32api DACL lookups # 275 | # Return: Adds dictionaries to class list # 276 | # ==============================================# 277 | def __thread_commands(self, commands, analysis_type): 278 | try: 279 | threads = [] 280 | tot_commands = len(commands) 281 | pool = tot_commands 282 | 283 | if (analysis_type == "procmon"): 284 | with concurrent.futures.ThreadPoolExecutor(max_workers=tot_commands) as executor: 285 | for i in range(tot_commands): 286 | 287 | # Analyze registry keys 288 | if "hklm:" in str(commands[i]).lower() or "hkcu:" in str(commands[i]).lower(): 289 | t = executor.submit(self.__permission_enum.get_registry_key_acl_procmon, commands[i]) 290 | self.__procmon_analysis.append(t.result()) 291 | 292 | # Disregard NONE type objects 293 | elif commands[i] == None: 294 | pass 295 | 296 | # Analyze File Paths 297 | else: 298 | t = executor.submit(self.__permission_enum.get_file_path_acl_procmon, commands[i]) 299 | self.__procmon_analysis.append(t.result()) 300 | 301 | if (analysis_type == "path"): 302 | with concurrent.futures.ThreadPoolExecutor(max_workers=tot_commands) as executor: 303 | for i in range(tot_commands): 304 | 305 | # Analyse File Paths 306 | if (commands[i] != None): 307 | t = executor.submit(self.__permission_enum.get_file_path_acl, commands[i]) 308 | self.__path_analysis.append(t.result()) 309 | 310 | except Exception as e: 311 | self.__print_exception() 312 | 313 | # ==============================================# 314 | # Purpose:Check for suspect permission sets # 315 | # Return: Boolean # 316 | # - True: Found suspect Permission # 317 | # - False: benign # 318 | # ==============================================# 319 | def __check_permission(self, line): 320 | try: 321 | line = line.lower() 322 | tmp = False 323 | users = [self.__username, "users", "everyone", "interactive", "authenticated"] 324 | permissions = [ 325 | "fullcontrol", 326 | "write", 327 | "write_dac", 328 | "generic_write", 329 | "key_write", 330 | "write_owner", 331 | "service_change_config" 332 | "changepermissions", 333 | "takeownership", 334 | "traverse", 335 | "key_all_access", 336 | "file_all_access" 337 | "all_access", 338 | "file_generic_write", 339 | "generic_all" 340 | ] 341 | 342 | for user in users: 343 | for permission in permissions: 344 | if user in line.lower() and permission in line.lower(): 345 | tmp = True 346 | break 347 | 348 | if tmp: 349 | break 350 | 351 | return tmp 352 | 353 | except Exception as e: 354 | self.__print_exception() 355 | 356 | # ==========================================================# 357 | # Purpose: Reviews all the dictionary objects stored in # 358 | # __procmon_analysis. This function is dedicated # 359 | # to reviewing only the procmon data to determine # 360 | # if weak/bad permissions are resident on objects # 361 | # that may be use to elevate privileges. After # 362 | # the full analysis, a final report is written to # 363 | # disk detailed all objects that a user has # 364 | # control # 365 | # over. # 366 | # Return: int - number of weak permissions found # 367 | # ==========================================================# 368 | def analyze_acls_procmon(self): 369 | try: 370 | 371 | # __procmon_analysis Dictionary Format: 372 | ''' 373 | acl_dict = { 374 | "process_name": path_dict["proc_name"], 375 | "integrity": path_dict["integrity"], 376 | "operation": path_dict["operation"], 377 | "original_cmd": path_dict["orig_cmd"], 378 | "path": r_path, 379 | "acls": acls 380 | } 381 | ''' 382 | df = pd.DataFrame(columns=["Process_Name", "Integrity", "Operation", "Accessed Object", "ACLs"]) 383 | pbar = tqdm(total=len(self.__procmon_analysis)) 384 | pbar.set_description("Looking for Evil") 385 | add_index = 0 386 | 387 | for obj in self.__procmon_analysis: 388 | add = False 389 | acl_dict = dict(obj) 390 | 391 | if (len(acl_dict) > 3): # Error Dicts and < 3, skip those 392 | proc_name = acl_dict["process_name"] # Placeholder for process name 393 | integrity = acl_dict["integrity"] # Placeholder for process integrity 394 | operation = acl_dict["operation"] # Placeholder for operation type 395 | orig_cmd = acl_dict["original_cmd"] # Placeholder for original command/path 396 | clean_cmd = acl_dict["path"] # Placeholder for cleaned command/path 397 | access = acl_dict["acls"] # Placeholder for access control list 398 | 399 | parsed_access = access.split("\n") # Split each individual ACL via newline 400 | for line in parsed_access: 401 | if (user_full_control := self.__check_permission(line)): 402 | add = user_full_control 403 | break; 404 | 405 | if add: 406 | final_data = { 407 | "Process_Name": proc_name, 408 | "Integrity": integrity, 409 | "Operation": operation, 410 | "Accessed Object": clean_cmd, 411 | "ACLs": access 412 | } 413 | df = df.append(final_data, ignore_index=True) 414 | add_index += 1 415 | 416 | add = False 417 | access = "" 418 | pbar.update(1) 419 | 420 | pbar.close() 421 | df.to_excel(self.__final_report) 422 | return add_index 423 | 424 | except Exception as e: 425 | print(f"\n\n{obj}") 426 | self.__print_exception() 427 | 428 | 429 | # ==========================================================# 430 | # Purpose: Used during the (-f, --files) flagwhich, # 431 | # analyzes all files given a starting path. ACLs # 432 | # are pulled for each path/file and saved to the # 433 | # class variable __path_analysis. This function # 434 | # uses the __path_analysis variable to enumerate # 435 | # weak/bad permissions on a file-by-file basis and# 436 | # saves the output to an excel document # 437 | # # 438 | # Return: Integer - count of all weak permissions found # 439 | # ==========================================================# 440 | def analyze_acls_path(self): 441 | try: 442 | output_df = pd.DataFrame(columns=["Path", "Permissions"]) 443 | add_index = 0 444 | 445 | for obj in self.__path_analysis: # For each dictionary in __path_analysis 446 | acls = "" # Placeholder for ACL's string 447 | add = False # Dictate if bad permission we found 448 | acl_dict = dict(obj) # Typecast into a dict() object 449 | 450 | if (acl_dict["error"] == None): # If the ACL enumeration contained no errors 451 | parsed_access = acl_dict["acls"] 452 | 453 | for line in parsed_access: # Check each ACL line for weak permissions 454 | if (user_full_control := self.__check_permission(line)): 455 | add = user_full_control 456 | break; 457 | 458 | if add: 459 | for line in parsed_access: 460 | acls += line + "\n" 461 | 462 | final_data = { 463 | "Path": acl_dict["file_path"], 464 | "Permissions": acls 465 | } 466 | output_df = output_df.append(final_data, ignore_index=True) 467 | add_index += 1 468 | 469 | output_df.to_excel(f"{self.__output_dir}/weak_path_permissions.xlsx") 470 | return add_index 471 | 472 | except Exception as e: 473 | self.__print_exception() 474 | 475 | 476 | # ==========================================================# 477 | # Purpose: Given a list of ACL's, enumerate each ACL to # 478 | # check for weak/bad permission on the file in # 479 | # question. If the ACL's are weak, return a bool # 480 | # # 481 | # Return: Boolean # 482 | # ==========================================================# 483 | def analyze_acls_from_list(self, acl_list): 484 | try: 485 | add = False # Placeholder to determine if user has full permissions 486 | 487 | for line in acl_list: 488 | line = line.lower() 489 | 490 | if (user_full_control := self.__check_permission(line)): 491 | add = user_full_control 492 | break; 493 | 494 | return add 495 | 496 | except Exception as e: 497 | self.__print_exception() 498 | 499 | 500 | # ===============================================# 501 | # Purpose: Clean Exception Printing # 502 | # Return: None # 503 | # ===============================================# 504 | def __print_exception(self): 505 | exc_type, exc_obj, tb = sys.exc_info() 506 | tmp_file = tb.tb_frame 507 | lineno = tb.tb_lineno 508 | filename = tmp_file.f_code.co_filename 509 | linecache.checkcache(filename) 510 | line = linecache.getline(filename, lineno, tmp_file.f_globals) 511 | print(f"{Fore.RED}EXCEPTION IN: {Fore.GREEN}{filename}\n\t[i] LINE: {lineno}, {line.strip()}\n\t[i] OBJECT: {exc_obj}{Fore.RESET}") 512 | 513 | 514 | # ===============================================# 515 | # Purpose: return Total amount of errors seen # 516 | # Return: int # 517 | # ===============================================# 518 | def get_error_count(self): 519 | try: 520 | file_errors = self.__permission_enum.error_index 521 | return file_errors 522 | except: 523 | self.__print_exception() 524 | -------------------------------------------------------------------------------- /src/low_fruit.py: -------------------------------------------------------------------------------- 1 | import re 2 | import os 3 | import sys 4 | import getpass 5 | import datetime 6 | import linecache 7 | import win32service 8 | import win32com.client 9 | import pandas as pd 10 | from winreg import * 11 | from tqdm import tqdm 12 | from bs4 import BeautifulSoup 13 | from colorama import Fore, init 14 | from . import windows_objects, permissions, analyze 15 | init() 16 | 17 | # --------------------------------------------------#s 18 | # Name: Low Hanging Fruit Class # 19 | # Purpose: Conduct the overall DACL analysis # 20 | # Author: @cribdragg3r # 21 | # Website: sevrosecurity.com # 22 | # --------------------------------------------------# 23 | 24 | 25 | class low_haning_fruit: 26 | 27 | def __init__(self, o_dir): 28 | self.name = "Low Hangning Fruit" 29 | self.__output_dir = o_dir 30 | self.__windows_objects = windows_objects.windows_services_and_tasks() 31 | self.__perms = permissions.permissions(self.__output_dir) 32 | self.__analysis = analyze.analyze(self.__output_dir) 33 | self.__windows_file_path = re.compile(r"^([A-Za-z]):\\((?:[A-Za-z\d][A-Za-z\d\- \x27_\(\)~]{0,61}\\?)*[A-Za-z\d][A-Za-z\d\- \x27_\(\)]{0,61})(\.[A-Za-z\d]{1,6})?") 34 | self.__password_regex = re.compile(r"(?i)(adminpassword|password|pass|login|creds)( |)(=|:).*") 35 | self.__username = str(getpass.getuser()).lower() 36 | self.__reg_handle = None 37 | self.__key_handle = None 38 | self.__sub_key_handle = None 39 | 40 | 41 | # ==========================================================# 42 | # Purpose: Analyzes the SYSTEM Path to enumerate trusted # 43 | # locations on the file system. It then looks for # 44 | # the ability to write to these trusted paths # 45 | # with hopes of obtaining privileged write access # 46 | # and possibly search order hijacking. # 47 | # Return: dictionary # 48 | # ==========================================================# 49 | def path_analysis(self): 50 | try: 51 | reg_path = r'SYSTEM\CurrentControlSet\Control\Session Manager\Environment' 52 | reg_key = OpenKey(HKEY_LOCAL_MACHINE, reg_path) 53 | system_environment_variables = QueryValueEx(reg_key, 'Path')[0] 54 | system_environment_variables = system_environment_variables.split(";") 55 | 56 | for path in system_environment_variables: 57 | acl_dict = self.__perms.get_file_path_acl(path) 58 | acl_list = acl_dict["acls"] 59 | bad_perms = self.__analysis.analyze_acls_from_list(acl_list) 60 | if (bad_perms): 61 | print(path, bad_perms) 62 | 63 | exit(0) 64 | 65 | except Exception as e: 66 | print(e) 67 | self.__print_exception() 68 | 69 | 70 | 71 | # ======================================================# 72 | # Purpose: Reviews registry keys that have been known # 73 | # to hold credentials and or misconfigurations that can # 74 | # be utalized to elevate privileges or laterally move # 75 | # # 76 | # Return: dict # 77 | # ======================================================# 78 | def registry_analysis(self): 79 | try: 80 | df = pd.DataFrame(columns=["Root_Key_Name", "Root_Key_Values", "Sub_Keys", "Sub_Key_Values", "ACLS"]) 81 | 82 | registry_keys = [ 83 | "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\Currentversion\Winlogon", 84 | "HKCU\\Software\\ORL\\WinVNC3\\Password", 85 | "HKCU\\Software\\SimonTatham\\PuTTY\\Sessions", 86 | "HKLM\\SYSTEM\\Current\\ControlSet\\Services\\SNMP", 87 | "HKLM\\Software\\RealVNC\\WinVNC4", 88 | "HKLM\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer\\AlwaysInstallElevated", 89 | "HKCU\\SOFTWARE\\Policies\\Microsoft\\Windows\\Installer\\AlwaysInstallElevated" 90 | ] 91 | wanted_key_names = [ 92 | "defaultusername", 93 | "defaultdomainname", 94 | "defaultpassword", 95 | "password", 96 | "pass", 97 | "credentials", 98 | "user", 99 | "username", 100 | "privatekeyfile", 101 | "hostname", 102 | "shell" 103 | ] 104 | 105 | pbar = tqdm(total=len(registry_keys)) 106 | pbar.set_description("Analyzing Registry Keys") 107 | for root_key in registry_keys: 108 | 109 | sub_key_names = [] 110 | tmp_value_holder = [] # Hold all the interesting keys in a single dict 111 | root_key_values = {} 112 | sub_key_values = {} 113 | key_exists = False 114 | 115 | # Open a handle to the correct registry path 116 | if ("hkcu" in root_key.lower()): 117 | key = root_key.split("HKCU\\")[1] 118 | self.__reg_handle = ConnectRegistry(None, HKEY_CURRENT_USER) 119 | 120 | elif ("hklm" in root_key.lower()): 121 | key = root_key.split("HKLM\\")[1] 122 | self.__reg_handle = ConnectRegistry(None, HKEY_LOCAL_MACHINE) 123 | 124 | try: 125 | # Open a key: 126 | self.__key_handle = OpenKey(self.__reg_handle, key) 127 | key_exists = True 128 | except: 129 | pbar.update(1) 130 | continue 131 | 132 | # Obtain information about the root key: 133 | total_sub_keys, total_key_values, last_modified_date = QueryInfoKey(self.__key_handle) 134 | last_modified_date = self.__ldap_to_datetime(last_modified_date) 135 | 136 | # Enumerate root key values: 137 | if (total_key_values != 0): 138 | for i in range(total_key_values): 139 | kv = EnumValue(self.__key_handle, i) 140 | value_name = list(kv)[0] 141 | 142 | if (value_name.lower() in wanted_key_names): # Only add keys that match our wanted_key_names[] 143 | root_key_values[value_name] = kv 144 | 145 | # Enumerate sub_key names: 146 | if (total_sub_keys != 0): 147 | for i in range(total_sub_keys): 148 | sub_key_name = EnumKey(self.__key_handle, i) 149 | sub_key_names.append(sub_key_name) 150 | 151 | # Enumerate sub_key_values: 152 | if (len(sub_key_names) != 0): 153 | # For each sub_key, open a handle: 154 | for _sub_key_name in sub_key_names: 155 | self.__sub_key_handle = OpenKey(self.__key_handle, _sub_key_name) 156 | _total_sub_keys, _total_key_values, _last_modified_date = QueryInfoKey(self.__sub_key_handle) 157 | 158 | if (_total_key_values != 0): 159 | 160 | for j in range(_total_key_values): 161 | _kv = EnumValue(self.__sub_key_handle, j) # Key Value 162 | _value_name = list(_kv)[0] # Value Name 163 | 164 | if (_value_name.lower() in wanted_key_names): 165 | tmp_value_holder.append(_kv) 166 | 167 | sub_key_values[_sub_key_name] = tmp_value_holder # add a dict object to another dict object (dict inception?) 168 | tmp_value_holder = [] # Clear out the value holder array for the next interation. 169 | CloseKey(self.__sub_key_handle) # Close the sub_key handle 170 | 171 | if (key_exists): 172 | key_acl_dict = self.__perms.get_registry_key_acl(root_key) 173 | acls = key_acl_dict["acls"] 174 | final_data = { 175 | "Root_Key_Name": root_key.strip(), 176 | "Root_Key_Values": root_key_values, 177 | "Sub_Keys": sub_key_names, 178 | "Sub_Key_Values": sub_key_values, 179 | "ACLS": acls 180 | } 181 | 182 | df = df.append(final_data, ignore_index=True) 183 | 184 | # Cleanup 185 | CloseKey(self.__key_handle) 186 | CloseKey(self.__reg_handle) 187 | pbar.update(1) 188 | 189 | CloseKey(self.__sub_key_handle) 190 | 191 | return { 192 | "name": "Registry Key Analysis", 193 | "description": "Analyze common misconfigured registry keys", 194 | "dataframe": df 195 | } 196 | 197 | except Exception as e: 198 | self.__print_exception() 199 | 200 | 201 | # ======================================================# 202 | # Purpose: Looks through the filesystem examining known # 203 | # files that contain passwords/credentials as well as # 204 | # dynamically assessing if a file is worth # 205 | # investigating further based on filename/type # 206 | # # 207 | # Return: dict - contains number of issues found # 208 | # ======================================================# 209 | def look_for_credentials(self): 210 | try: 211 | # Output File 212 | df = pd.DataFrame(columns=["File_Path","Possible_Credentials"]) 213 | 214 | # Function Variables 215 | found_cred_files = 0 # counter of files that have credentials/passwords within. (dictated by regex) 216 | found_creds = "" 217 | interesting_file_paths = [] # Holds all interesting files the deem further analysis 218 | 219 | interesting_file_types = [ 220 | ".ini", 221 | ".xml", 222 | ".conf", 223 | ".config", 224 | ".inf", 225 | ".txt" 226 | ] 227 | exclusion_list = [ 228 | "credential_enumeration.txt", 229 | "test.xlsx" 230 | ] 231 | 232 | # We need to disable the file system redirects before enumerating any 233 | # privileged paths such as C:\Windows\System32. 234 | print("\n[i] Enumerating all system files. Please Wait.") 235 | with windows_objects.disable_file_system_redirection(): 236 | for root, dirs, files in os.walk("C:\\"): 237 | for file in files: 238 | if (file.endswith(tuple(interesting_file_types)) 239 | and file.lower() not in exclusion_list): 240 | 241 | full_path = os.path.join(root, file) 242 | interesting_file_paths.append(full_path) 243 | 244 | 245 | # Analyze files that met the interesting file type criteria 246 | pbar = tqdm(total=len(interesting_file_paths)) 247 | pbar.set_description("Analyzing Possible Credential Files") 248 | 249 | for file_path in interesting_file_paths: 250 | add = False 251 | try: 252 | # For each line in a singular file, check it against the regex 253 | # to determine if passwords/credentials present. 254 | for i, line in enumerate(open(file_path, encoding="utf-8")): 255 | creds = re.search(self.__password_regex, line) 256 | if (creds != None): 257 | 258 | found_creds += (f"{str(creds.group())[0:100]}\n") 259 | add = True 260 | 261 | # Add the data to the dataframe. 262 | if (add): 263 | interim_data = {"File_Path": file_path, "Possible_Credentials": found_creds} 264 | df = df.append(interim_data, ignore_index=True) 265 | found_cred_files +=1 266 | 267 | # Clean the creds for the next interation. 268 | found_creds = "" 269 | 270 | except Exception as e: 271 | pass 272 | 273 | pbar.update(1) 274 | pbar.close() 275 | 276 | return { 277 | "name":"File Credential Analysis", 278 | "description": "Analyze all files from C:\\ and search for credentials", 279 | "dataframe": df, 280 | "total_cred_files": found_cred_files 281 | } 282 | 283 | except Exception as e: 284 | self.__print_exception() 285 | 286 | # ======================================================# 287 | # Purpose: Enumerates all scheduled tasks and looks # 288 | # for open / liberal ACL permissions to the command. # 289 | # # 290 | # Return: dict - contains number of issues found # 291 | # ======================================================# 292 | def analyze_scheduled_tasks(self): 293 | 294 | """ 295 | # Enumeration of the COM Object via powershell: 296 | --> $o = [activator]::CreateInstance([type]::GetTypeFromProgID('Schedule.Service')) 297 | --> $o.Connect() | Get-Member 298 | --> $f = $o.GetFolder("") 299 | --> $t = $f.GetTasks(0) 300 | --> $t | Get-Member 301 | 302 | Name MemberType Definition 303 | ---- ---------- ---------- 304 | GetInstances Method IRunningTaskCollection GetInstances (int) 305 | GetSecurityDescriptor Method string GetSecurityDescriptor (int) 306 | Run Method IRunningTask Run (Variant) 307 | RunEx Method IRunningTask RunEx (Variant, int, int, string) 308 | SetSecurityDescriptor Method void SetSecurityDescriptor (string, int) 309 | Stop Method void Stop (int) 310 | Definition Property ITaskDefinition Definition () {get} 311 | Enabled Property bool Enabled () {get} {set} 312 | LastRunTime Property Date LastRunTime () {get} 313 | LastTaskResult Property int LastTaskResult () {get} 314 | Name Property string Name () {get} 315 | NextRunTime Property Date NextRunTime () {get} 316 | NumberOfMissedRuns Property int NumberOfMissedRuns () {get} 317 | Path Property string Path () {get} 318 | State Property _TASK_STATE State () {get} 319 | Xml Property string Xml () {get} 320 | """ 321 | 322 | try: 323 | # Function Variables / Objects 324 | df = pd.DataFrame(columns=["Task Name","Task Command", "Command Args", "Task Path", "Task State", "Last Run Date", "Next Run Date", "ACLS", "Vulnerable Permissions"]) 325 | total_tasks = 0 326 | vuln_perms = 0 327 | acl_list = [] 328 | vuln_tasks = [] 329 | task_path = "" 330 | task_name = "" 331 | task_state = "" 332 | last_run = "" 333 | last_result = "" 334 | next_run = "" 335 | 336 | # Windows Schedule.Service COM Object initialization 337 | scheduler = win32com.client.Dispatch("Schedule.Service") 338 | scheduler.Connect() 339 | folders = [scheduler.GetFolder("\\")] 340 | 341 | # Get the total number of tasks to enumerate: 342 | while folders: 343 | folder = folders.pop(0) 344 | folders += list(folder.GetFolders(0)) 345 | for t in folder.GetTasks(0): 346 | total_tasks += 1 347 | 348 | pbar = tqdm(total=total_tasks) 349 | pbar.set_description("Analyzing Scheduled Tasks") 350 | 351 | # Enumerate each task individually 352 | folders = [scheduler.GetFolder("\\")] 353 | while folders: 354 | 355 | folder = folders.pop(0) 356 | folders += list(folder.GetFolders(0)) 357 | 358 | for task in folder.GetTasks(0): 359 | acls = "" 360 | task_path = task.Path 361 | task_name = task.Name 362 | task_state = self.__windows_objects.TASK_STATE[task.State] 363 | last_run = task.LastRunTime 364 | last_result = task.LastTaskResult 365 | next_run = task.NextRunTime 366 | 367 | # We have to pull the XML object to obtain the command/executable 368 | # that is scheduled to run. This has issues where at times, there 369 | # is not command object. 370 | try: 371 | xml_data = BeautifulSoup(task.Xml, "xml") 372 | raw_task_command = xml_data.find("Command").text 373 | task_command = str(raw_task_command).replace('"', "").strip() 374 | 375 | # Handle ENV VAR paths 376 | if "%" in task_command: 377 | env_key = re.search(r"\%(\w+)\%", task_command) 378 | raw_key = str(env_key.group(1)).lower() 379 | replace_key = env_key.group(0) 380 | replacement = os.environ.get(raw_key) 381 | task_command = task_command.replace(replace_key, replacement) 382 | 383 | except Exception as e: 384 | task_command = "Unknown" 385 | 386 | # Within the XML, there is an Arguments parameter. This is something 387 | # we parse for further analysis. (i.e., look for other exe/dll objects) 388 | try: 389 | task_args = xml_data.find("Arguments").text 390 | except: 391 | task_args = "None" 392 | 393 | # If the Command is not Unknown, obtain its ACL's for further analysis. 394 | if task_command != "Unknown": 395 | acl_dict = self.__perms.get_file_path_acl(task_command) 396 | acl_list = acl_dict["acls"] 397 | 398 | for i, acl in enumerate(acl_list): 399 | 400 | if i == (len(acl_list) - 1): 401 | acls += acl 402 | else: 403 | acls += f"{acl}\n" 404 | 405 | # Analyze the Commands ACL values for access rights issues / vulns. 406 | suspect_task = self.__analysis.analyze_acls_from_list(acl_list) 407 | if suspect_task: 408 | vuln_perms += 1 409 | if (task_name not in vuln_tasks): 410 | vuln_tasks.append(task_name) 411 | 412 | last_run = last_run.strftime('%d/%m/%y %I:%M %S %p') 413 | try: 414 | next_run = next_run.strftime('%d/%m/%y %I:%M %S %p') 415 | except: 416 | next_run = None 417 | 418 | 419 | data = { 420 | "Task Name":task_name, 421 | "Task Command":task_command, 422 | "Command Args":task_args, 423 | "Task Path":task_path, 424 | "Task State":task_state, 425 | "Last Run Date":last_run, 426 | "Next Run Date":next_run, 427 | "ACLS":acls, 428 | "Vulnerable Permissions":suspect_task 429 | } 430 | df = df.append(data, ignore_index=True) 431 | pbar.update(1) 432 | 433 | return { 434 | "name": "Scheduled Task Analysis", 435 | "description": "Analyze all Scheduled tasks for weak binpath ACLs", 436 | "dataframe": df, 437 | "total_tasks": total_tasks, 438 | "vuln_tasks": vuln_tasks, 439 | "vuln_perms": vuln_perms 440 | } 441 | 442 | except Exception as e: 443 | self.__print_exception() 444 | exit(0) 445 | 446 | 447 | # ====================================================# 448 | # Purpose: Enumerates all services. Specifically we # 449 | # check for vulnerable ACL's on the binpath and we # 450 | # check to see if we can open the service handle with # 451 | # EDIT/CHANGE permissions in order to change the # 452 | # binpath. These two checks are stored as boolean # 453 | # values in the final report. # 454 | # # 455 | # Return: dict - contains number of vulns found # 456 | # ====================================================# 457 | def analyze_all_services(self): 458 | try: 459 | 460 | df = pd.DataFrame(columns=["Short Name","Long Name", "Service Type", "Start Type", "Dependencies", "Full Command", "Bin Path", "ACLS", "Vulnerable Permissions", "Can Edit Binpath", "Is Unquoted Path"]) 461 | total_services = 0 # Total Number of services 462 | vuln_perms = 0 # Total number of services that have suspect/vulnerable permissions (ACLS) 463 | vuln_conf = 0 # Total number of services where we can change the binpath as a standard user. 464 | vuln_unquote = 0 # Total number of services with unquoted service paths 465 | vuln_services = [] # List of all services tht can potentially be exploited 466 | 467 | service_config_manager = win32service.OpenSCManager( 468 | "", 469 | None, 470 | win32service.SC_MANAGER_CONNECT 471 | | win32service.SC_MANAGER_ENUMERATE_SERVICE 472 | | win32service.SC_MANAGER_QUERY_LOCK_STATUS 473 | | win32service.SERVICE_QUERY_CONFIG, 474 | ) 475 | service_manager = win32service.OpenSCManager( 476 | None, None, win32service.SC_MANAGER_ENUMERATE_SERVICE 477 | ) 478 | 479 | # Hacky way to get the total number of services 480 | for service in win32service.EnumServicesStatus( 481 | service_manager, 482 | win32service.SERVICE_WIN32, 483 | win32service.SERVICE_STATE_ALL, 484 | ): 485 | total_services += 1 486 | 487 | pbar = tqdm(total=total_services) 488 | pbar.set_description("Analyzing Services") 489 | # For each service, enumerate its values and check ACL's / binpath edits. 490 | for service in win32service.EnumServicesStatus( 491 | service_manager, 492 | win32service.SERVICE_WIN32, 493 | win32service.SERVICE_STATE_ALL, 494 | ): 495 | 496 | acls = "" # Holds all ACL's obtained from filepaths.py 497 | conf_check = False # Can we edit the current services configuration? 498 | unquote_check = False # Check for unquoted service paths 499 | 500 | access = win32service.OpenService( 501 | service_config_manager, 502 | service[0], 503 | win32service.SERVICE_QUERY_CONFIG, 504 | ) 505 | config = win32service.QueryServiceConfig(access) 506 | 507 | service_short_name = str(service[0]).replace('"', "").strip() 508 | service_long_name = str(service[1]).replace('"', "").strip() 509 | service_type = self.__access_from_int(config[0]) 510 | service_start_type = self.__windows_objects.START_TYPE[config[2]] 511 | service_dependencies = str(config[6]).replace('"', "").strip() 512 | raw_bin_path = str(config[3]) 513 | cleaned_bin_path = str(config[3]).replace('"', "").strip() 514 | 515 | # find and cleanup the bin path due to CLI argument being present. 516 | service_bin_path = re.findall( 517 | self.__windows_file_path, 518 | cleaned_bin_path)[0] 519 | 520 | service_bin_path = os.path.join( 521 | service_bin_path[0] + ":\\", 522 | service_bin_path[1] + service_bin_path[2]) 523 | 524 | # Analyze ACL's for the Bin Path: 525 | #acl_list = fp.get_acl_list_return(service_bin_path) 526 | acl_dict = self.__perms.get_file_path_acl(service_bin_path) 527 | acl_list = acl_dict["acls"] 528 | for i, acl in enumerate(acl_list): 529 | 530 | if i == (len(acl_list) - 1): 531 | acls += acl 532 | else: 533 | acls += f"{acl}\n" 534 | 535 | # Check for bad ACL permissions: 536 | suspect_service = self.__analysis.analyze_acls_from_list(acl_list) 537 | if suspect_service: 538 | vuln_perms += 1 539 | if service_short_name not in vuln_services: 540 | vuln_services.append(service_short_name) 541 | 542 | # Check if we can change the config: 543 | try: 544 | test = win32service.OpenService( 545 | service_config_manager, 546 | service[0], 547 | win32service.SERVICE_CHANGE_CONFIG, 548 | ) 549 | conf_check = True 550 | vuln_conf += 1 551 | if service_short_name not in vuln_services: 552 | vuln_services.append(service_short_name) 553 | except: 554 | pass 555 | 556 | # Check for unquoted service paths: 557 | if ("program files" in raw_bin_path.lower() 558 | and '"' not in raw_bin_path.lower()): 559 | unquote_check = True 560 | vuln_unquote += 1 561 | if service_short_name not in vuln_services: 562 | vuln_services.append(service_short_name) 563 | 564 | # Write the final data to a file. 565 | 566 | data = { 567 | "Short Name":service_short_name, 568 | "Long Name":service_long_name, 569 | "Service Type":f"{config[0]} {service_type}", 570 | "Start Type":service_start_type, 571 | "Dependencies":service_dependencies, 572 | "Full Command":cleaned_bin_path, 573 | "Bin Path":service_bin_path, 574 | "ACLS":acls, 575 | "Vulnerable Permissions":suspect_service, 576 | "Can Edit Binpath":conf_check, 577 | "Is Unquoted Path":unquote_check 578 | } 579 | 580 | df = df.append(data, ignore_index=True) 581 | pbar.update(1) 582 | 583 | return { 584 | "name": "Service Analysis", 585 | "description": "Analyze all services for weak ACLs, ability to change bin-path, and unquoted paths", 586 | "dataframe": df, 587 | "total_services": total_services, 588 | "vuln_perms": vuln_perms, 589 | "vuln_conf": vuln_conf, 590 | "vuln_unquote": vuln_unquote, 591 | "vuln_services": vuln_services 592 | } 593 | 594 | except Exception as e: 595 | self.__print_exception() 596 | exit(1) 597 | 598 | 599 | 600 | # ==========================================================# 601 | # Purpose: Event Message Files (DLL's) are used by various # 602 | # programs (many running as SYSTEM) to write logs # 603 | # that can be viewed in Event Viewer. This # 604 | # function enumerates the DLL's associated to # 605 | # programs who have registered a DLL to log events# 606 | # If a user has access to change such a DLL, an # 607 | # escalation of privileges is highly likely. # 608 | # Return: Dictionary of several objects # 609 | # ==========================================================# 610 | def message_event_analysis(self): 611 | try: 612 | interesting_keys = ["CategoryMessageFile", "EventMessageFile"] # These are keys that hold DLL paths 613 | key = "SYSTEM\\CurrentControlSet\\Services\\EventLog\\Application" # Reg Key Path for MessageEvent DLL's/EXE's 614 | reg_handle = ConnectRegistry(None, HKEY_LOCAL_MACHINE) # Handle to the registry 615 | df = pd.DataFrame(columns=[ 616 | "reg_key", 617 | "sub_key_name", 618 | "sub_key_value", 619 | "key_value_acls"] 620 | ) # DataFrame object to return 621 | sub_key_names = [] # Hold all the sub_key names associated with key 622 | acls = '' # Placeholder for string ACL's 623 | vuln_perms = 0 # Placeholder weak permissions counter 624 | checked = 0 # Placeholder for number of keys analyzed 625 | system_root = os.getenv("SystemRoot") # Swap out the %SystemRoot% with this lookup 626 | 627 | try: 628 | # Open a key: 629 | root_key_handle = OpenKey(reg_handle, key) 630 | except Exception as e: 631 | pass 632 | 633 | # Obtain information about the root key: 634 | total_sub_keys, total_key_values, last_modified_date = QueryInfoKey(root_key_handle) 635 | 636 | # Enumerate sub_key names: 637 | if (total_sub_keys != 0): 638 | for i in range(total_sub_keys): 639 | sub_key_name = EnumKey(root_key_handle, i) 640 | sub_key_names.append(sub_key_name) 641 | 642 | pbar = tqdm(total=len(sub_key_names)) 643 | pbar.set_description("Analyzing Event Message DLLs") 644 | # For each sub_key, open a handle: 645 | for _sub_key_name in sub_key_names: 646 | sub_key_handle = OpenKey(root_key_handle, _sub_key_name) 647 | _total_sub_keys, _total_key_values, _last_modified_date = QueryInfoKey(sub_key_handle) 648 | key_name_final = f"HKLM\\{key}\\{_sub_key_name}" 649 | 650 | # If the sub key has key values, we enumerate them 651 | if (_total_key_values != 0): 652 | 653 | # for each key value, obtain the dll names 654 | for j in range(_total_key_values): 655 | 656 | _kv = EnumValue(sub_key_handle, j) # sub key arrays 657 | _value_name = list(_kv)[0] # Value Name 658 | _key_value = list(_kv)[1] 659 | checked += 1 660 | 661 | if (_value_name in interesting_keys): # if the current key name is in interesting keys 662 | 663 | if (';' in _key_value): 664 | _key_value = _key_value.split(';')[0] 665 | 666 | if ("%SystemRoot%" in _key_value ): 667 | _key_value = _key_value.replace("%SystemRoot%", system_root) # replace %SystemRoot% 668 | 669 | acl_dict = self.__perms.get_file_path_acl(_key_value) # Get the ACL's for the DLL/EXE 670 | acl_list = acl_dict["acls"] # from dict to list 671 | acls = '\n'.join(acl_list) # ACL list to string with \n endings 672 | 673 | # Check to see if there are weak permissions on the dll/exe object 674 | if (self.__analysis.analyze_acls_from_list(acl_list)): 675 | data = { 676 | "reg_key": key_name_final, 677 | "sub_key_name": _value_name, 678 | "sub_key_value": _key_value, 679 | "key_value_acls": acls 680 | } 681 | df = df.append(data, ignore_index=True) 682 | vuln_perms += 1 683 | pbar.update(1) 684 | 685 | return { 686 | "name": "Message Event Analysis", 687 | "description": "Analyze Message Event DLL Permissions to check for search order hijacking", 688 | "dataframe": df, 689 | "vuln_perms": vuln_perms 690 | } 691 | 692 | except Exception as e: 693 | self.__print_exception() 694 | exit(1) 695 | 696 | 697 | # ==========================================================# 698 | # Purpose: Clean Exception Printing # 699 | # Return: None # 700 | # ==========================================================# 701 | def __print_exception(self): 702 | exc_type, exc_obj, tb = sys.exc_info() 703 | tmp_file = tb.tb_frame 704 | lineno = tb.tb_lineno 705 | filename = tmp_file.f_code.co_filename 706 | linecache.checkcache(filename) 707 | line = linecache.getline(filename, lineno, tmp_file.f_globals) 708 | print( 709 | f"{Fore.RED}EXCEPTION IN: {Fore.GREEN}{filename}\n\t[i] LINE: {lineno}, {line.strip()}\n\t[i] OBJECT: {exc_obj}{Fore.RESET}" 710 | ) 711 | 712 | # ==============================================# 713 | # Purpose: Given a int permission bitmask, # 714 | # translate it into an ASCII SERVICE_TYPE # 715 | # # 716 | # Return: String - Contains SERVICE_TYPE string # 717 | # ==============================================# 718 | def __access_from_int(self, num): 719 | 720 | rights = "" 721 | 722 | for spec in self.__windows_objects.SERVICE_TYPE.items(): 723 | if num & spec[0]: 724 | rights += spec[1] + " " 725 | 726 | return rights 727 | 728 | # ==============================================# 729 | # Purpose: Given a LDAP timestamp, convert it # 730 | # to datetime object. # 731 | # # 732 | # Return: datetime object # 733 | # ==============================================# 734 | def __ldap_to_datetime(self,ts: float): 735 | fixed = datetime.datetime(1601, 1, 1) + datetime.timedelta(seconds=ts/10000000) 736 | return fixed 737 | -------------------------------------------------------------------------------- /src/permissions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import io 4 | import sys 5 | import time 6 | import numpy 7 | import ctypes 8 | import getpass 9 | import win32api 10 | import threading 11 | import linecache 12 | import win32security 13 | import win32con as con 14 | from colorama import Fore, init 15 | from enum import Enum 16 | from . import windows_objects 17 | init() 18 | 19 | 20 | class permissions: 21 | 22 | def __init__(self, o_dir): 23 | self.name = "Permission Analysis" 24 | self.error_index = 0 25 | self.re_valid_string = re.compile(r"^[ADO][ADLU]?\:\(.*\)$") 26 | self.re_perms = re.compile(r"\(([^\(\)]+)\)") 27 | self.re_type = re.compile(r"^[DOGS]") 28 | self.re_owner = re.compile(r"^O:[^:()]+(?=[DGS]:)") 29 | self.re_group = re.compile(r"G:[^:()]+(?=[DOS]:)") 30 | self.re_acl = re.compile(r"[DS]:.+$") 31 | self.re_const = re.compile(r"(\w\w)") 32 | self.re_non_acl = re.compile(r"[^:()]+$") 33 | self.SDDL_OBJECT = windows_objects.SDDL() 34 | self.ACCESS = self.SDDL_OBJECT.ACCESS 35 | self.SDDL_TYPE = self.SDDL_OBJECT.SDDL_TYPE 36 | self.TRUSTEE = self.SDDL_OBJECT.TRUSTEE 37 | self.ACCESS_HEX = self.SDDL_OBJECT.ACCESS_HEX 38 | self.__username = str(getpass.getuser()).lower() 39 | self.__output_dir = o_dir 40 | self.__mutex = threading.Lock() 41 | self.__error_out_file = open(f"{self.__output_dir}/errors.txt", "w+") 42 | self.__CONVENTIONAL_ACES = { 43 | win32security.ACCESS_ALLOWED_ACE_TYPE: "ALLOW", 44 | win32security.ACCESS_DENIED_ACE_TYPE: "DENY", 45 | } 46 | 47 | 48 | # ==========================================================# 49 | # Purpose: Obtains ACL values for a single Registry path / # 50 | # key. This function is exclusive to the procmon # 51 | # Return: dictionary # 52 | # ==========================================================# 53 | def get_registry_key_acl_procmon(self, path_dict): 54 | try: 55 | ''' 56 | cmd = { 57 | "proc_name" : proc_name, 58 | "orig_cmd" : orig_cmd, 59 | "clean_cmd" : clean_cmd, 60 | "operation" : operation, 61 | "integrity" : integrity 62 | } 63 | ''' 64 | registry_dict = dict(path_dict) # A dictionary object containing all information regarding a single key 65 | r_path = registry_dict["clean_cmd"] # The cleaned registry key path - ready for DACL enumeration 66 | 67 | # HKLM support 68 | if "hklm" in r_path: 69 | try: 70 | path = r_path.split(":\\")[1] 71 | except: 72 | path = r_path.split("hklm\\")[1] 73 | key = win32api.RegOpenKey( 74 | con.HKEY_LOCAL_MACHINE, 75 | path, 76 | 0, 77 | con.KEY_ENUMERATE_SUB_KEYS 78 | | con.KEY_QUERY_VALUE 79 | | con.KEY_READ, 80 | ) 81 | 82 | # HKCU Support 83 | if "hkcu" in r_path: 84 | try: 85 | path = r_path.split(":\\")[1] 86 | except: 87 | path = r_path.split("hkcu\\")[1] 88 | key = win32api.RegOpenKey( 89 | con.HKEY_CURRENT_USER, 90 | path, 91 | 0, 92 | con.KEY_ENUMERATE_SUB_KEYS 93 | | con.KEY_QUERY_VALUE 94 | | con.KEY_READ, 95 | ) 96 | 97 | # HKCR Support 98 | if "hkcr" in r_path: 99 | try: 100 | path = r_path.split(":\\")[1] 101 | except: 102 | path = r_path.split("hkcr\\")[1] 103 | key = win32api.RegOpenKey( 104 | con.HKEY_CLASSES_ROOT, 105 | path, 106 | 0, 107 | con.KEY_ENUMERATE_SUB_KEYS 108 | | con.KEY_QUERY_VALUE 109 | | con.KEY_READ, 110 | ) 111 | 112 | 113 | sd = win32api.RegGetKeySecurity( # Obtain a Registry Security Object 114 | key, 115 | win32security.DACL_SECURITY_INFORMATION 116 | | win32security.OWNER_SECURITY_INFORMATION) 117 | dacl = sd.GetSecurityDescriptorDacl() # Registry Security DACL 118 | owner_sid = sd.GetSecurityDescriptorOwner() # Registry Security Owner 119 | sddl_string = win32security.ConvertSecurityDescriptorToStringSecurityDescriptor( 120 | sd, 121 | win32security.SDDL_REVISION_1, 122 | win32security.DACL_SECURITY_INFORMATION) # Gives us an SDDL String we can now parse. 123 | 124 | all_permissions = dict(self.__sddl_dacl_parse(sddl_string)) 125 | keys = all_permissions.keys() 126 | acls = "" 127 | 128 | # Enumerate all the keys (registry paths) and ACLS 129 | for key in keys: 130 | acls += f"{key}: {all_permissions[key]}\n" 131 | 132 | acl_dict = { 133 | "process_name": path_dict["proc_name"], 134 | "integrity": path_dict["integrity"], 135 | "operation": path_dict["operation"], 136 | "original_cmd": path_dict["orig_cmd"], 137 | "path": r_path, 138 | "acls": acls 139 | } 140 | return acl_dict 141 | 142 | except Exception as e: 143 | error = str(e).lower() 144 | if ( 145 | "find the path specified" in error 146 | or "find the file specified" in error 147 | or "access is denied" in error 148 | or "ace type 9" in error 149 | ): 150 | final_dict = { 151 | "key_path":r_path, 152 | "acls": "ERROR", 153 | "error": error 154 | } 155 | return final_dict 156 | 157 | elif "no mapping" in error: 158 | note = """Possibly VULNERABLE: No mapping between account names and SID's 159 | Account used to set GPO may have been removed 160 | Account name may be typed incorrectly 161 | INFO: https://www.rebeladmin.com/2016/01/how-to-fix-error-no-mapping-between-account-names-and-security-ids-in-active-directory/""" 162 | final_dict = { 163 | "key_path":r_path, 164 | "acls": note, 165 | "error": error 166 | } 167 | return final_dict 168 | 169 | else: 170 | self.__write_error(r_path + "\n" + all_permissions) 171 | self.__print_exception() 172 | exit(0) 173 | 174 | 175 | # ==========================================================# 176 | # Purpose: Obtains ACL values for a single Registry path # 177 | # / key # 178 | # Return: Dictionary # 179 | # ==========================================================# 180 | def get_registry_key_acl(self, root_key): 181 | try: 182 | 183 | r_path = root_key.lower() 184 | all_permissions = "" 185 | 186 | # HKLM support 187 | if "hklm" in r_path: 188 | try: 189 | path = r_path.split(":\\")[1] 190 | except: 191 | path = r_path.split("hklm\\")[1] 192 | key = win32api.RegOpenKey( 193 | con.HKEY_LOCAL_MACHINE, 194 | path, 195 | 0, 196 | con.KEY_ENUMERATE_SUB_KEYS 197 | | con.KEY_QUERY_VALUE 198 | | con.KEY_READ, 199 | ) 200 | 201 | # HKCU Support 202 | if "hkcu" in r_path: 203 | try: 204 | path = r_path.split(":\\")[1] 205 | except: 206 | path = r_path.split("hkcu\\")[1] 207 | key = win32api.RegOpenKey( 208 | con.HKEY_CURRENT_USER, 209 | path, 210 | 0, 211 | con.KEY_ENUMERATE_SUB_KEYS 212 | | con.KEY_QUERY_VALUE 213 | | con.KEY_READ, 214 | ) 215 | 216 | # HKCR Support 217 | if "hkcr" in r_path: 218 | try: 219 | path = r_path.split(":\\")[1] 220 | except: 221 | path = r_path.split("hkcr\\")[1] 222 | key = win32api.RegOpenKey( 223 | con.HKEY_CLASSES_ROOT, 224 | path, 225 | 0, 226 | con.KEY_ENUMERATE_SUB_KEYS 227 | | con.KEY_QUERY_VALUE 228 | | con.KEY_READ, 229 | ) 230 | 231 | sd = win32api.RegGetKeySecurity( 232 | key, 233 | win32security.DACL_SECURITY_INFORMATION 234 | | win32security.OWNER_SECURITY_INFORMATION, 235 | ) 236 | dacl = sd.GetSecurityDescriptorDacl() 237 | owner_sid = sd.GetSecurityDescriptorOwner() 238 | sddl_string = win32security.ConvertSecurityDescriptorToStringSecurityDescriptor( 239 | sd, 240 | win32security.SDDL_REVISION_1, 241 | win32security.DACL_SECURITY_INFORMATION, 242 | ) 243 | 244 | all_permissions = dict(self.__sddl_dacl_parse(sddl_string)) 245 | keys = all_permissions.keys() 246 | acls = "" 247 | 248 | for key in keys: 249 | acls += f"{key}: {all_permissions[key]}\n" 250 | 251 | final_dict = { 252 | "key_path":r_path, 253 | "acls": acls, 254 | "error": None 255 | } 256 | 257 | return final_dict 258 | 259 | except Exception as e: 260 | error = str(e).lower() 261 | if ( 262 | "find the path specified" in error 263 | or "find the file specified" in error 264 | or "access is denied" in error 265 | or "ace type 9" in error 266 | ): 267 | 268 | final_dict = { 269 | "key_path":r_path, 270 | "acls": None, 271 | "error": error 272 | } 273 | return final_dict 274 | 275 | elif "no mapping" in error: 276 | note = """Possibly VULNERABLE: No mapping between account names and SID's 277 | Account used to set GPO may have been removed 278 | Account name may be typed incorrectly 279 | INFO: https://www.rebeladmin.com/2016/01/how-to-fix-error-no-mapping-between-account-names-and-security-ids-in-active-directory/""" 280 | final_dict = { 281 | "key_path":r_path, 282 | "acls": note, 283 | "error": error 284 | } 285 | return final_dict 286 | 287 | else: 288 | self.__write_error(r_path + "\n" + all_permissions) 289 | self.__print_exception() 290 | exit(0) 291 | 292 | 293 | 294 | # ==========================================================# 295 | # Purpose: obtains ACL values for a single file / filepath # 296 | # given a dict object that contains Procmon.exe # 297 | # attributes/data # 298 | # Return: Dictionary # 299 | # ==========================================================# 300 | def get_file_path_acl_procmon(self, path_dict): 301 | try: 302 | ''' 303 | path_dict = { 304 | "proc_name" : proc_name, 305 | "orig_cmd" : orig_cmd, 306 | "clean_cmd" : clean_cmd, 307 | "operation" : operation, 308 | "integrity" : integrity 309 | } 310 | ''' 311 | path_dict = dict(path_dict) 312 | f_path = path_dict["clean_cmd"] 313 | 314 | # Working with weird WIndows/Procmon Output... 315 | if "|" in f_path: 316 | f_path = f_path.split("|")[0] 317 | 318 | if ("hklm" in f_path.lower() and "c:" in f_path.lower()): 319 | f_path = "C:/" + f_path.lower().split("c:")[1] 320 | 321 | acls = "" 322 | gfso = win32security.GetFileSecurity( 323 | f_path, win32security.DACL_SECURITY_INFORMATION 324 | ) 325 | dacl = gfso.GetSecurityDescriptorDacl() 326 | 327 | for n_ace in range(dacl.GetAceCount()): 328 | ace = dacl.GetAce(n_ace) 329 | (ace_type, ace_flags) = ace[0] 330 | 331 | mask = 0 # Reset the bitmask for each interation 332 | domain = "" # Reset the domain for each interation 333 | name = "" # Reset the name for each interation 334 | ascii_mask = "" # Reset the ascii permission value for each interation 335 | 336 | if ace_type in self.__CONVENTIONAL_ACES: 337 | mask, sid = ace[1:] 338 | else: 339 | mask, object_type, inherited_object_type, sid = ace[1:] 340 | 341 | name, domain, type = win32security.LookupAccountSid(None, sid) 342 | 343 | # Enumerate windows_security_enums 344 | for enum_obj in windows_objects.windows_security_enums: 345 | if ctypes.c_uint32(mask).value == enum_obj.value.value: 346 | access = self.__CONVENTIONAL_ACES.get(ace_type, "OTHER") 347 | ascii_mask = enum_obj.name 348 | acls += f"{domain}\\{name} {access} {ascii_mask}\n" 349 | 350 | # Enumerate nt_security_permissions 351 | for enum_obj in windows_objects.nt_security_enums: 352 | if ctypes.c_uint32(mask).value == enum_obj.value.value: 353 | access = self.__CONVENTIONAL_ACES.get(ace_type, "OTHER") 354 | ascii_mask = enum_obj.name 355 | acls += f"{domain}\\{name} {access} {ascii_mask}\n" 356 | 357 | acl_dict = { 358 | "process_name": path_dict["proc_name"], 359 | "integrity": path_dict["integrity"], 360 | "operation": path_dict["operation"], 361 | "original_cmd": path_dict["orig_cmd"], 362 | "path": f_path, 363 | "acls": acls} 364 | 365 | f_path = "" 366 | return acl_dict 367 | 368 | except Exception as e: 369 | error = str(e).lower() 370 | if ( 371 | "find the path specified" in error 372 | or "find the file specified" in error 373 | or "access is denied" in error 374 | or "ace type 9" in error 375 | or "nonetype" in error 376 | ): 377 | final_dict = { 378 | "key_path":f_path, 379 | "acls": "ERROR", 380 | "error": error 381 | } 382 | return final_dict 383 | 384 | elif "no mapping" in error: 385 | note = """Possibly VULNERABLE: No mapping between account names and SID's 386 | Account used to set GPO may have been removed 387 | Account name may be typed incorrectly 388 | INFO: https://www.rebeladmin.com/2016/01/how-to-fix-error-no-mapping-between-account-names-and-security-ids-in-active-directory/""" 389 | 390 | final_dict = { 391 | "key_path":f_path, 392 | "acls": "ERROR", 393 | "error": error 394 | } 395 | return final_dict 396 | 397 | else: 398 | self.__write_error(f_path) 399 | self.__print_exception() 400 | exit(0) 401 | 402 | 403 | # ==========================================================# 404 | # Purpose: Given a single path (directory or file), # 405 | # enumerate its corresponding ACL's. # 406 | # Return: Dictionary # 407 | # ==========================================================# 408 | def get_file_path_acl(self, f_path): 409 | 410 | try: 411 | with windows_objects.disable_file_system_redirection(): 412 | acls = [] 413 | gfso = win32security.GetFileSecurity( 414 | f_path, win32security.DACL_SECURITY_INFORMATION 415 | ) 416 | dacl = gfso.GetSecurityDescriptorDacl() 417 | 418 | for n_ace in range(dacl.GetAceCount()): 419 | 420 | ace = dacl.GetAce(n_ace) 421 | (ace_type, ace_flags) = ace[0] 422 | 423 | mask = 0 # Reset the bitmask for each interation 424 | domain = "" # Reset the domain for each interation 425 | name = "" # Reset the name for each interation 426 | ascii_mask = "" # Reset the ascii permission value for each interation 427 | 428 | if ace_type in self.__CONVENTIONAL_ACES: 429 | mask, sid = ace[1:] 430 | else: 431 | mask, object_type, inherited_object_type, sid = ace[1:] 432 | 433 | name, domain, type = win32security.LookupAccountSid(None, sid) 434 | 435 | # Enumerate windows_security_enums 436 | for enum_obj in windows_objects.windows_security_enums: 437 | if ctypes.c_uint32(mask).value == enum_obj.value.value: 438 | access = self.__CONVENTIONAL_ACES.get(ace_type, "OTHER") 439 | ascii_mask = enum_obj.name 440 | acls.append(f"{domain}\\{name} {access} {ascii_mask}") 441 | 442 | # Enumerate nt_security_permissions 443 | for enum_obj in windows_objects.nt_security_enums: 444 | if ctypes.c_uint32(mask).value == enum_obj.value.value: 445 | access = self.__CONVENTIONAL_ACES.get(ace_type, "OTHER") 446 | ascii_mask = enum_obj.name 447 | acls.append(f"{domain}\\{name} {access} {ascii_mask}") 448 | 449 | final_acls = { 450 | "file_path": f_path, 451 | "acls": acls, 452 | "error": None 453 | } 454 | return final_acls 455 | 456 | except Exception as e: 457 | error = str(e).lower() 458 | if ("find the path specified" in error 459 | or "find the file specified" in error 460 | or "access is denied" in error 461 | or "ace type 9" in error 462 | or "nonetype" in error 463 | or "trust relationship" in error): 464 | 465 | error_dict = { 466 | "path_name": f_path, 467 | "acls": acls, 468 | "error": error 469 | } 470 | return error_dict 471 | 472 | elif "no mapping" in error: 473 | note = """Possibly VULNERABLE: No mapping between account names and SID's 474 | Account used to set GPO may have been removed 475 | Account name may be typed incorrectly 476 | INFO: https://www.rebeladmin.com/2016/01/how-to-fix-error-no-mapping-between-account-names-and-security-ids-in-active-directory/""" 477 | 478 | error_dict = { 479 | "path_name": f_path, 480 | "acls": note, 481 | "error": error 482 | } 483 | return error_dict 484 | 485 | else: 486 | self.__write_error(f_path) 487 | self.__print_exception() 488 | exit(0) 489 | 490 | 491 | # ==========================================================# 492 | # Purpose: Given an SDDL DACL String, parse it and analyze # 493 | # the overall permission set for each user and # 494 | # group # 495 | # # 496 | # Return: Dict of all permissions in SDDL # 497 | # ==========================================================# 498 | def __sddl_dacl_parse(self, sddl_string): 499 | try: 500 | 501 | # We already know we have obtained the DACL SDDL string via win32security. Therefore 502 | # we do not have to enumerate the SDDL type even though we prove such data in the 503 | # windows_objects.SDDL() class. 504 | 505 | all_permissions = {} 506 | sddl_permissions = re.findall(self.re_perms, sddl_string) 507 | 508 | for dacl in sddl_permissions: 509 | 510 | # There are some odd dacl windows-isms here that I need to account for: 511 | if "WIN://" not in dacl: 512 | 513 | raw_ace_type = dacl.split(";")[0] 514 | raw_ace_flags = dacl.split(";")[1] 515 | raw_perms = dacl.split(";")[2] 516 | raw_trustee = dacl.split(";")[5] 517 | 518 | # Obtain the Plaintext ACE Type: 519 | access_type = self.ACCESS[raw_ace_type] 520 | 521 | # Obtain the plaintext ACE Flags: Don't Need These 522 | """ 523 | flags = "" 524 | flags_index = 0 525 | if (len(raw_ace_flags) > 2): 526 | flag_split = [raw_ace_flags[i:i+2] for i in range(0, len(raw_ace_flags), 2)] 527 | for flag in flag_split: 528 | if (flags_index == 0): 529 | flags += f"{self.ACCESS[flag]}" 530 | flags_index += 1 531 | else: 532 | flags += f", {self.ACCESS[flag]}" 533 | else: 534 | flags += f"{self.ACCESS[raw_ace_flags]}" 535 | """ 536 | 537 | # Obtain the plaintext permissions: 538 | acls = "" 539 | acl_index = 0 540 | # Check if we have HEX permissions first: 541 | if "0x" in raw_perms: 542 | raw_perms = self.__access_from_hex(raw_perms) 543 | # Plaintext Permission Set: 544 | if len(raw_perms) > 2: 545 | perm_split = [ 546 | raw_perms[i : i + 2] for i in range(0, len(raw_perms), 2) 547 | ] 548 | for acl in perm_split: 549 | if acl_index == 0: 550 | acls += f"{self.ACCESS[acl]}" 551 | acl_index += 1 552 | else: 553 | acls += f", {self.ACCESS[acl]}" 554 | else: 555 | acls += f"{self.ACCESS[raw_perms]}" 556 | 557 | # Obtain the Account/User (Trustee) 558 | try: # sometimes fails due to undocumented trustees such as services. 559 | if len(raw_trustee) <= 2: 560 | trustee = self.TRUSTEE[ 561 | raw_trustee 562 | ] # Get the trustee from the windows_objects class 563 | else: 564 | try: # if the object is a SID, attempt to translate it and obtain the ASCII name 565 | trustee = win32security.LookupAccountSid( 566 | None, win32security.GetBinarySid(raw_trustee) 567 | ) 568 | trustee = f"{trustee[1]}/{trustee[0]}" 569 | except: 570 | trustee = None 571 | except: 572 | trustee = None 573 | 574 | # Add all the content to the dict object 575 | if trustee not in all_permissions.keys() and trustee != None: 576 | all_permissions[trustee] = acls 577 | elif trustee != None: 578 | current = f"{str(all_permissions[trustee])} {acls}" 579 | all_permissions[trustee] = current 580 | current = "" 581 | 582 | return all_permissions 583 | 584 | except Exception as e: 585 | self.__write_error(sddl_string + "\n" + dacl) 586 | self.__print_exception() 587 | pass 588 | 589 | 590 | # ==========================================================# 591 | # Purpose:Check for suspect permission sets # 592 | # Return: Boolean # 593 | # - True: Found suspect Permission # 594 | # - False: benign # 595 | # ==========================================================# 596 | def __check_permission(self, line): 597 | try: 598 | line = line.lower() 599 | tmp = False 600 | users = [self.__username, "users", "everyone", "interactive", "authenticated"] 601 | permissions = [ 602 | "fullcontrol", 603 | "write", 604 | "write_dac", 605 | "generic_write", 606 | "key_write", 607 | "write_owner", 608 | "service_change_config", 609 | "changepermissions", 610 | "takeownership", 611 | "traverse", 612 | "key_all_access", 613 | "file_all_access", 614 | "all_access", 615 | "file_generic_write", 616 | "generic_all" 617 | ] 618 | 619 | for user in users: 620 | for permission in permissions: 621 | if user in line.lower() and permission in line.lower(): 622 | tmp = True 623 | break 624 | 625 | if tmp: 626 | break 627 | 628 | return tmp 629 | 630 | except Exception as e: 631 | self.__print_exception() 632 | 633 | 634 | # ==============================================# 635 | # Purpose: Given a hex permission bitmask, # 636 | # translate it into an ASCII two-byte denoting # 637 | # in order to analyze further. # 638 | # # 639 | # Return: String - Contains ACCESS strings # 640 | # ==============================================# 641 | def __access_from_hex(self, hex): 642 | 643 | hex = int(hex, 16) 644 | rights = "" 645 | 646 | for spec in self.ACCESS_HEX.items(): 647 | if hex & spec[0]: 648 | rights += spec[1] 649 | 650 | return rights 651 | 652 | 653 | 654 | # ===============================================# 655 | # Purpose: Write errors to File # 656 | # Return: None # 657 | # ===============================================# 658 | def __write_error(self, data): 659 | try: 660 | 661 | self.__error_out_file.write(data) 662 | 663 | except Exception as e: 664 | self.__print_exception() 665 | exit(1) 666 | 667 | 668 | # ==============================================# 669 | # Purpose: Clean Exception Printing # 670 | # Return: None # 671 | # ==============================================# 672 | def __print_exception(self): 673 | self.error_index += 1 674 | exc_type, exc_obj, tb = sys.exc_info() 675 | tmp_file = tb.tb_frame 676 | lineno = tb.tb_lineno 677 | filename = tmp_file.f_code.co_filename 678 | linecache.checkcache(filename) 679 | line = linecache.getline(filename, lineno, tmp_file.f_globals) 680 | sep = "-" * 100 681 | data = f"\n\nREGISTRY EXCEPTION IN: {filename}\n\t[i] LINE: {lineno}, {line.strip()}\n\t[i] OBJECT: {exc_obj}\n{sep}\n" 682 | self.__write_error(data) -------------------------------------------------------------------------------- /src/reporting.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import linecache 4 | import pandas as pd 5 | from colorama import Fore, init 6 | init() 7 | 8 | 9 | class report: 10 | 11 | def __init__(self, report_format, o_dir): 12 | self.name = "Report Generation" 13 | self.__report_format = report_format.lower() 14 | self.__output_dir = o_dir 15 | 16 | # ==========================================================# 17 | # Purpose: Takes several dictionaries from the low_fruit # 18 | # analysis and compiles a file report in one of # 19 | # two formats (Excel, JSON). It also builds a # 20 | # dataframe that is a very high-level view of the # 21 | # overall analysis # 22 | # Return: dataframe # 23 | # ==========================================================# 24 | def generate_fruit_report(self, services: dict, tasks: dict, registry: dict, message_events: dict, creds: dict): 25 | try: 26 | fruit_report = pd.DataFrame(columns=["Analysis", "Description", "Analysis Type", "Issues Found"]) 27 | 28 | # Analyze System Services 29 | services_name = services["name"] 30 | services_desc = services["description"] 31 | services_report = services["dataframe"] 32 | services_total = services["total_services"] 33 | services_vuln_perms = services["vuln_perms"] 34 | services_vuln_conf = services["vuln_conf"] 35 | services_vuln_unquote = services["vuln_unquote"] 36 | 37 | fruit_report = fruit_report.append({ 38 | "Analysis": services_name, 39 | "Description": services_desc, 40 | "Analysis Type": "Total Services", 41 | "Issues Found": services_total}, ignore_index=True) 42 | fruit_report = fruit_report.append({ 43 | "Analysis": services_name, 44 | "Description": services_desc, 45 | "Analysis Type": "Unquoted Service Paths", 46 | "Issues Found": services_vuln_unquote}, ignore_index=True) 47 | fruit_report = fruit_report.append({ 48 | "Analysis": services_name, 49 | "Description": services_desc, 50 | "Analysis Type": "Weak Permissions", 51 | "Issues Found": services_vuln_perms}, ignore_index=True) 52 | fruit_report = fruit_report.append({ 53 | "Analysis": services_name, 54 | "Description": services_desc, 55 | "Analysis Type": "Editable Configuration", 56 | "Issues Found": services_vuln_conf}, ignore_index=True) 57 | 58 | # Analyze Scheduled Tasks 59 | tasks_name = tasks["name"] 60 | tasks_desc = tasks["description"] 61 | tasks_report = tasks["dataframe"] 62 | tasks_total = tasks["total_tasks"] 63 | tasks_vuln_perms = tasks["vuln_perms"] 64 | 65 | fruit_report = fruit_report.append({ 66 | "Analysis": tasks_name, 67 | "Description": tasks_desc, 68 | "Analysis Type": "Total Scheduled Tasks", 69 | "Issues Found": tasks_total}, ignore_index=True) 70 | 71 | fruit_report = fruit_report.append({ 72 | "Analysis": tasks_name, 73 | "Description": tasks_desc, 74 | "Analysis Type": "Weak Permissions", 75 | "Issues Found": tasks_vuln_perms}, ignore_index=True) 76 | 77 | # Analyze Registry Keys: 78 | registry_name = registry["name"] 79 | registry_desc = registry["description"] 80 | registry_report = registry["dataframe"] 81 | 82 | fruit_report = fruit_report.append({ 83 | "Analysis": registry_name, 84 | "Description": registry_desc, 85 | "Analysis Type": "Common Key Analysis", 86 | "Issues Found": "Review Final Report"}, ignore_index=True) 87 | 88 | # Analyze Event Message Logging DLLs: 89 | message_name = message_events["name"] 90 | message_desc = message_events["description"] 91 | message_report = message_events["dataframe"] 92 | message_vuln_perms = message_events["vuln_perms"] 93 | 94 | fruit_report = fruit_report.append({ 95 | "Analysis": message_name, 96 | "Description": message_desc, 97 | "Analysis Type": "Weak Permissions", 98 | "Issues Found": message_vuln_perms}, ignore_index=True) 99 | 100 | # Analyze all files for credentials 101 | credential_name = creds["name"] 102 | credential_desc = creds["description"] 103 | credential_report = creds["dataframe"] 104 | credential_found = creds["total_cred_files"] 105 | 106 | fruit_report = fruit_report.append({ 107 | "Analysis": credential_name, 108 | "Description": credential_desc, 109 | "Analysis Type": "Possible Credentials Files", 110 | "Issues Found": credential_found}, ignore_index=True) 111 | 112 | # Write Final Report Excel: 113 | if (self.__report_format == "excel"): 114 | with pd.ExcelWriter(f"{self.__output_dir}/Low_Hanging_Fruit.xlsx") as writer: 115 | fruit_report.to_excel(writer, sheet_name="Totals") 116 | services_report.to_excel(writer, sheet_name=services_name) 117 | tasks_report.to_excel(writer, sheet_name=tasks_name) 118 | message_report.to_excel(writer, sheet_name=message_name) 119 | registry_report.to_excel(writer, sheet_name=registry_name) 120 | credential_report.to_excel(writer, sheet_name=credential_name) 121 | 122 | # Write Final Reports JSON: 123 | if (self.__report_format == "json"): 124 | services_report.to_json(f"{self.__output_dir}/{services_name.replace(' ', '_')}.json", orient="table") 125 | tasks_report.to_json(f"{self.__output_dir}/{tasks_name.replace(' ', '_')}.json", orient="table") 126 | message_report.to_json(f"{self.__output_dir}/{message_name.replace(' ', '_')}.json", orient="table") 127 | registry_report.to_json(f"{self.__output_dir}/{registry_name.replace(' ', '_')}.json", orient="table") 128 | credential_report.to_json(f"{self.__output_dir}/{credential_name.replace(' ', '_')}.json", orient="table") 129 | 130 | return fruit_report 131 | 132 | except Exception as e: 133 | self.__print_exception() 134 | 135 | 136 | # ===============================================# 137 | # Purpose: Clean Exception Printing # 138 | # Return: None # 139 | # ===============================================# 140 | def __print_exception(self): 141 | exc_type, exc_obj, tb = sys.exc_info() 142 | tmp_file = tb.tb_frame 143 | lineno = tb.tb_lineno 144 | filename = tmp_file.f_code.co_filename 145 | linecache.checkcache(filename) 146 | line = linecache.getline(filename, lineno, tmp_file.f_globals) 147 | print(f"{Fore.RED}EXCEPTION IN: {Fore.GREEN}{filename}\n\t[i] LINE: {lineno}, {line.strip()}\n\t[i] OBJECT: {exc_obj}{Fore.RESET}") 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/windows_objects.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ctypes 3 | import win32security 4 | import win32api 5 | from enum import Enum 6 | 7 | #-----------------------------------------------------# 8 | # Name: Windows Objects Class # 9 | # Purpose: Contains permission bitmasks / sets # 10 | # Author: @cribdragg3r # 11 | # Website: sevrosecurity.com # 12 | # API: http://timgolden.me.uk/pywin32-docs/win32.html # 13 | #-----------------------------------------------------# 14 | 15 | class windows_security_enums(Enum): 16 | FullControl = ctypes.c_uint32(0x1f01ff) 17 | modify = ctypes.c_uint32(0x0301bf) 18 | ReadExecAndSynchronize = ctypes.c_uint32(0x1200a9) 19 | Synchronize = ctypes.c_uint32(0x100000) 20 | ReadAndExecute = ctypes.c_uint32(0x0200a9) 21 | ReadAndWrite = ctypes.c_uint32(0x02019f) 22 | Read = ctypes.c_uint32(0x020089) 23 | Write = ctypes.c_uint32(0x000116) 24 | 25 | # Windows NT Permission Bitmasks 26 | class nt_security_enums(Enum): 27 | GenericRead = ctypes.c_uint32(0x80000000) 28 | GenericWrite = ctypes.c_uint32(0x40000000) 29 | GenericExecute = ctypes.c_uint32(0x20000000) 30 | GenericAll = ctypes.c_uint32(0x10000000) 31 | MaximumAllowed = ctypes.c_uint32(0x02000000) 32 | AccessSystemSecurity = ctypes.c_uint32(0x01000000) 33 | Synchronize = ctypes.c_uint32(0x00100000) 34 | WriteOwner = ctypes.c_uint32(0x00080000) 35 | WriteDAC = ctypes.c_uint32(0x00040000) 36 | ReadControl = ctypes.c_uint32(0x00020000) 37 | Delete = ctypes.c_uint32(0x00010000) 38 | WriteAttributes = ctypes.c_uint32(0x00000100) 39 | ReadAttributes = ctypes.c_uint32(0x00000080) 40 | DeleteChild = ctypes.c_uint32(0x00000040) 41 | ExecuteTraverse = ctypes.c_uint32(0x00000020) 42 | WriteExtendedAttributes = ctypes.c_uint32(0x00000010) 43 | ReadExtendedAttributes = ctypes.c_uint32(0x00000008) 44 | AppendDataAddSubdirectory = ctypes.c_uint32(0x00000004) 45 | WriteDataAddFile = ctypes.c_uint32(0x00000002) 46 | ReadDataListDirectory = ctypes.c_uint32(0x00000001) 47 | 48 | class windows_services_and_tasks(): 49 | 50 | #https://docs.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicetype?view=netframework-4.8 51 | SERVICE_TYPE = { 52 | 1 : "KERNEL DRIVER", 53 | 2 : "FILE SYSTEM DRIVER", 54 | 4 : "ADAPTER", 55 | 8 : "RECOGNIZER DRIVER", 56 | 16 : "WIN32_OWN_PROCESS", 57 | 32 : "WIN32_SHARE_PROCESS", 58 | 256 : "INTERACTIVE_PROCESS" 59 | } 60 | 61 | # https://docs.microsoft.com/en-us/dotnet/api/system.serviceprocess.servicestartmode?view=netframework-4.8 62 | START_TYPE = { 63 | 0 : "BOOT", # Started by the system loader (device drivers) 64 | 1 : "SYSTEM", # Device driver started by IOInitSystem() 65 | 2 : "AUTOMATIC", # Started by the OS at system startup 66 | 3 : "MANUAL", # Must be started manually by the user 67 | 4 : "DISABLED" # service is disabled. 68 | } 69 | 70 | TASK_STATE = { 71 | 0 : 'Unknown', 72 | 1 : 'Disabled', 73 | 2 : 'Queued', 74 | 3 : 'Ready', 75 | 4 : 'Running' 76 | } 77 | 78 | # When enumerating C:\Windows objects, this class mitigates BS redirects. 79 | # For more information about SysWow64 and System32 redirects see the link below: 80 | # --> https://docs.microsoft.com/en-us/windows/win32/winprog64/file-system-redirector?redirectedfrom=MSDN 81 | class disable_file_system_redirection: 82 | _disable = ctypes.windll.kernel32.Wow64DisableWow64FsRedirection 83 | _revert = ctypes.windll.kernel32.Wow64RevertWow64FsRedirection 84 | def __enter__(self): 85 | self.old_value = ctypes.c_long() 86 | self.success = self._disable(ctypes.byref(self.old_value)) 87 | def __exit__(self, type, value, traceback): 88 | if self.success: 89 | self._revert(self.old_value) 90 | 91 | 92 | class SDDL(): 93 | #https://itconnect.uw.edu/wares/msinf/other-help/understanding-sddl-syntax/ 94 | """ 95 | Access Mask: 32-bits 96 | ___________________________________ 97 | | Bit(s) | Meaning | 98 | ----------------------------------- 99 | | 0 - 15 | Object Access Rights | 100 | | 16 - 22 | Standard Access Rights | 101 | | 23 | Can access security ACL | 102 | | 24 - 27 | Reserved | 103 | | 28 - 31 | Generic Access Rights | 104 | ----------------------------------- 105 | """ 106 | 107 | SDDL_TYPE = { 108 | 'O': 'Owner', 109 | 'G': 'Group', 110 | 'D': 'DACL', 111 | 'S': 'SACL' 112 | } 113 | 114 | ACCESS = { 115 | # ACE Types 116 | 'A' : 'ACCESS_ALLOWED', 117 | 'D' : 'ACCESS_DENIED', 118 | 'OA': 'ACCESS_ALLOWED_OBJECT', 119 | 'OD': 'ACCESS_DENIED_OBJECT', 120 | 'AU': 'SYSTEM_AUDIT', 121 | 'AL': 'SYSTEM_ALARM', 122 | 'OU': 'SYSTEM_AUDIT_OBJECT', 123 | 'OL': 'SYSTEM_ALARM_OBJECT', 124 | 125 | # ACE Flags 126 | 'CI': 'CONTAINER_INHERIT', 127 | 'OI': 'OBJECT_INHERIT', 128 | 'NP': 'NO_PROPAGATE_INHERIT', 129 | 'IO': 'INHERIT_ONLY', 130 | 'ID': 'INHERITED', 131 | 'SA': 'SUCCESSFUL_ACCESS', 132 | 'FA': 'FAILED_ACCESS', 133 | 134 | # Generic Access Rights 135 | 'GA': 'GENERIC_ALL', 136 | 'GR': 'GENERIC_READ', 137 | 'GW': 'GENERIC_WRITE', 138 | 'GX': 'GENERIC_EXECUTE', 139 | 140 | # Standard Access Rights 141 | 'RC': 'READ_CONTROL', 142 | 'SD': 'DELETE', 143 | 'WD': 'WRITE_DAC', 144 | 'WO': 'WRITE_OWNER', 145 | 146 | # Directory Service Object Access Rights 147 | 'RP': 'DS_READ_PROP', 148 | 'WP': 'DS_WRITE_PROP', 149 | 'CC': 'DS_CREATE_CHILD', 150 | 'DC': 'DS_DELETE_CHILD', 151 | 'CR': 'SDDL_CONTROL_ACCESS', 152 | 'LC': 'DS_LIST', 153 | 'SW': 'DS_SELF', 154 | 'LO': 'DS_LIST_OBJECT', 155 | 'DT': 'DS_DELETE_TREE', 156 | 157 | # File Access Rights 158 | 'FA': 'FILE_ALL_ACCESS', 159 | 'FR': 'FILE_GENERIC_READ', 160 | 'FW': 'FILE_GENERIC_WRITE', 161 | 'FX': 'FILE_GENERIC_EXECUTE', 162 | 163 | # Registry Access Rights 164 | 'KA': 'KEY_ALL_ACCESS', 165 | 'KR': 'KEY_READ', 166 | 'KW': 'KEY_WRITE', 167 | 'KE': 'KEY_EXECUTE'} 168 | 169 | ACCESS_HEX = { 170 | # Generic Access Rights 171 | 0x10000000: 'GA', 172 | 0x20000000: 'GX', 173 | 0x40000000: 'GW', 174 | 0x80000000: 'GR', 175 | 176 | # Standard Access Rights 177 | 0x00010000: 'SD', 178 | 0x00020000: 'RC', 179 | 0x00040000: 'WD', 180 | 0x00080000: 'WO', 181 | 182 | # Object Access Rights 183 | 0x00000001: 'CC', 184 | 0x00000002: 'DC', 185 | 0x00000004: 'LC', 186 | 0x00000008: 'SW', 187 | 0x00000010: 'RP', 188 | 0x00000020: 'WP', 189 | 0x00000040: 'DT', 190 | 0x00000080: 'LO', 191 | 0x00000100: 'CR' 192 | } 193 | 194 | TRUSTEE = { 195 | 'AO': 'Account Operators', 196 | 'AC': 'All Application Packages', 197 | 'RU': 'Pre-Win2k Compatibility Access', 198 | 'AN': 'Anonymous', 199 | 'AU': 'Authenticated Users', 200 | 'BA': 'Administrators', 201 | 'BG': 'Guests', 202 | 'BO': 'Backup Operators', 203 | 'BU': 'Users', 204 | 'CA': 'Certificate Publishers', 205 | 'CD': 'Certificate Services DCOM Access', 206 | 'CG': 'Creator Group', 207 | 'CO': 'Creator Owner', 208 | 'DA': 'Domain Admins', 209 | 'DC': 'Domain Computers', 210 | 'DD': 'Domain Controllers', 211 | 'DG': 'Domain Guests', 212 | 'DU': 'Domain Users', 213 | 'EA': 'Enterprise Admins', 214 | 'ED': 'Enterprise Domain Controllers', 215 | 'RO': 'Enterprise Read-Only Domain Controllers', 216 | 'WD': 'Everyone', 217 | 'PA': 'Group Policy Admins', 218 | 'IU': 'Interactive Users', 219 | 'LA': 'Local Administrator', 220 | 'LG': 'Local Guest', 221 | 'LS': 'Local Service', 222 | 'SY': 'NT AUTHORITY/System', 223 | 'NU': 'Network', 224 | 'LW': 'Low Integrity', 225 | 'ME': 'Medium Integrity', 226 | 'OW': 'NT SERVICE/Dhcp', 227 | 'HI': 'High Integrity', 228 | 'SI': 'System Integrity', 229 | 'NO': 'Network Configuration Operators', 230 | 'NS': 'Network Service', 231 | 'PO': 'Printer Operators', 232 | 'PS': 'Self', 233 | 'PU': 'Power Users', 234 | 'RS': 'RAS Servers', 235 | 'RD': 'Remote Desktop Users', 236 | 'RE': 'Replicator', 237 | 'RC': 'Restricted Code', 238 | 'SA': 'Schema Administrators', 239 | 'SO': 'Server Operators', 240 | 'SU': 'Service' 241 | } 242 | 243 | --------------------------------------------------------------------------------