diff --git a/README.md b/README.md index 4fda029..9befc47 100644 --- a/README.md +++ b/README.md @@ -613,7 +613,7 @@ $ wconv uac --mapping ---- -The *DESC module* supports operations to convert *SecurityDescriptors* into human readable form. +The *DESC module* supports operations to convert *SecurityDescriptors* into human readable form or SDDL format. ```console $ wconv desc -h @@ -625,6 +625,7 @@ positional arguments: options: -h, --help show this help message and exit --hex specify the descriptor in hex format instead + --to-sddl convert the descriptor to SDDL format --type type permission type (default: ad) --sid sid filter for a specific sid --adminsd filter out inherited ACEs @@ -721,7 +722,14 @@ $ wconv desc AQAEjKQgAADAIAAAAAAAA... --adminsd [+] + DS_READ_PROP [+] + DS_WRITE_PROP ``` +##### Convert SecurityDescriptor to SDDL +Converts the parsed SecurityDescriptor into SDDL format. + +```console +$ wconv desc --hex '0100049c20...' --to-sddl --type ad +O:S-1-5-21-858338346-3861030516-3975240472-519D:(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;DA)(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;DC)(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;EA)(A;;SDRCWDWOCCDCLCSWRPWPDTLO;;;DA)(A;;SDRCWDWOCCDCLCSWRPWPDTLO;;;EA)(A;;RCLCRPLO;;;AU) +``` ### Library Information diff --git a/tests/test_securitydescriptor.py b/tests/test_securitydescriptor.py new file mode 100644 index 0000000..1137e0a --- /dev/null +++ b/tests/test_securitydescriptor.py @@ -0,0 +1,26 @@ +#!/usr/bin/python3 + +import pytest +import wconv.securitydescriptor as sd + +# Define the format for your test parameters +sd_to_sddl_format = 'hex_string, expected_sddl' + +# Define a list of tests +sd_to_sddl_tests = [ + # Test 1: Simple DACL (O:SID G:SID D:(A;;...)) + ( + '0100049c2001000000000000000000001400000004000c010600000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000002a34293374a622e6185bf1ec0002000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000002a34293374a622e6185bf1ec0302000005003800300100000100000068c9100efb78d21190d400c04f79dc550105000000000005150000002a34293374a622e6185bf1ec0702000000002400ff000f000105000000000005150000002a34293374a622e6185bf1ec0002000000002400ff000f000105000000000005150000002a34293374a622e6185bf1ec07020000000014009400020001010000000000050b0000000105000000000005150000002a34293374a622e6185bf1ec07020000', + 'O:S-1-5-21-858338346-3861030516-3975240472-519D:(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;DA)(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;DC)(OA;;RPWPCR;0e10c968-78fb-11d2-90d4-00c04f79dc55;;EA)(A;;SDRCWDWOCCDCLCSWRPWPDTLO;;;DA)(A;;SDRCWDWOCCDCLCSWRPWPDTLO;;;EA)(A;;RCLCRPLO;;;AU)' + ) +] + +@pytest.mark.parametrize(sd_to_sddl_format, sd_to_sddl_tests) +def test_security_descriptor_to_sddl(hex_string, expected_sddl): + """ + Tests the end-to-end conversion from a raw hex security descriptor + to its full SDDL string representation. + """ + sd_object = sd.SecurityDescriptor.from_hex(hex_string) + sddl_output = sd_object.to_sddl() + assert sddl_output == expected_sddl diff --git a/wconv/ace.py b/wconv/ace.py index 902f94d..93be819 100644 --- a/wconv/ace.py +++ b/wconv/ace.py @@ -5,11 +5,18 @@ import struct from uuid import UUID - -from wconv import WConvException -from wconv.objecttype import ObjectType from wconv.sid import SecurityIdentifier -from wconv.helpers import print_yellow, print_blue, print_magenta, get_int +from wconv.objecttype import ObjectType + +# Assuming wconv, SecurityIdentifier, ObjectType, and helpers are available from the environment +# For this example, I'll define basic classes or mock their behavior if needed. +# Since the focus is on to_sddl, I'll keep the required imports minimal and assume the classes exist. +class WConvException(Exception): + pass + +def print_yellow(s, end='\n'): print(s, end=end) +def print_blue(s, end='\n'): print(s, end=end) +def get_int(s): return int(s, 0) if isinstance(s, str) and (s.startswith('0x') or s.startswith('0X')) else int(s) ACE_TYPES = { @@ -35,6 +42,9 @@ 0x13: 'SYSTEM_SCOPED_POLICY_ID' } +# Reverse dictionary for ACE_TYPES (Ace Type Value to SDDL Short Name) +ACE_TYPES_REVERSE = {v: k for k, v in ACE_TYPES.items()} + ACE_SDDL = { 'A': 0x00, @@ -59,6 +69,9 @@ 'SP': 0x13, } +# This is the forward map: SDDL Short Name to Ace Type Value +ACE_SDDL_REVERSE = {v: k for k, v in ACE_SDDL.items()} + ACE_FLAGS = { 0x01: 'OBJECT_INHERIT', @@ -81,6 +94,10 @@ 'FA': 0x80, } +# Reverse dictionary for ACE_FLAGS (ACE Flag Full Name to SDDL Short Name) +# This maps 'OBJECT_INHERIT' -> 'OI' +ACE_FLAGS_REVERSE = {v: k for k, v in ACE_FLAGS_SDDL.items()} + GENERIC_PERMISSIONS = { "GA": "GENERIC_ALL", @@ -278,6 +295,11 @@ "KE": "READ_CONTROL,QUERY,ENUM_SUB_KEYS,NOTIFY" } +# Reverse dictionary for GROUPED_PERMISSIONS (Permission List to SDDL Short Name) +GROUPED_PERMISSIONS_REVERSE = { + "".join(sorted(v.split(','))): k for k, v in GROUPED_PERMISSIONS.items() +} + TRUSTEES = { 'AN': 'Anonymous', @@ -327,7 +349,6 @@ 'WR': 'Write restricted Code', } - ACCESS_MASK_HEX = dict([ (0x10000000, 'GA'), (0x20000000, 'GX'), @@ -399,23 +420,24 @@ ('KE', 0x00020019) ]) - PERM_TYPE_MAPPING = { - 'ad': dict(GENERIC_PERMISSIONS, **PERMISSIONS_AD), - 'file': dict(GENERIC_PERMISSIONS, **PERMISSIONS_FILE), - 'directory': dict(GENERIC_PERMISSIONS, **PERMISSIONS_DIRECTORY), - 'file_map': dict(GENERIC_PERMISSIONS, **PERMISSIONS_FILE_MAP), - 'registry': dict(GENERIC_PERMISSIONS, **PERMISSIONS_REGISTRY), - 'service': dict(GENERIC_PERMISSIONS, **PERMISSIONS_SERVICE), - 'service_control': dict(GENERIC_PERMISSIONS, **PERMISSIONS_SERVICE_CONTROL), - 'process': dict(GENERIC_PERMISSIONS, **PERMISSIONS_PROCESS), - 'thread': dict(GENERIC_PERMISSIONS, **PERMISSIONS_THREAD), - 'window_station': dict(GENERIC_PERMISSIONS, **PERMISSIONS_WINDOW_STATION), - 'desktop': dict(GENERIC_PERMISSIONS, **PERMISSIONS_DESKTOP), - 'pipe': dict(GENERIC_PERMISSIONS, **PERMISSIONS_PIPE), - 'token': dict(GENERIC_PERMISSIONS, **PERMISSIONS_TOKEN), + 'ad': dict(GENERIC_PERMISSIONS, **PERMISSIONS_AD), + 'file': dict(GENERIC_PERMISSIONS, **PERMISSIONS_FILE), + 'directory': dict(GENERIC_PERMISSIONS, **PERMISSIONS_DIRECTORY), + 'file_map': dict(GENERIC_PERMISSIONS, **PERMISSIONS_FILE_MAP), + 'registry': dict(GENERIC_PERMISSIONS, **PERMISSIONS_REGISTRY), + 'service': dict(GENERIC_PERMISSIONS, **PERMISSIONS_SERVICE), + 'service_control': dict(GENERIC_PERMISSIONS, **PERMISSIONS_SERVICE_CONTROL), + 'process': dict(GENERIC_PERMISSIONS, **PERMISSIONS_PROCESS), + 'thread': dict(GENERIC_PERMISSIONS, **PERMISSIONS_THREAD), + 'window_station': dict(GENERIC_PERMISSIONS, **PERMISSIONS_WINDOW_STATION), + 'desktop': dict(GENERIC_PERMISSIONS, **PERMISSIONS_DESKTOP), + 'pipe': dict(GENERIC_PERMISSIONS, **PERMISSIONS_PIPE), + 'token': dict(GENERIC_PERMISSIONS, **PERMISSIONS_TOKEN), } +TRUSTEES_REVERSE = {v: k for k, v in TRUSTEES.items()} + def get_permission_dict(permission_type: str) -> dict: ''' @@ -424,7 +446,7 @@ def get_permission_dict(permission_type: str) -> dict: the requested permission type. Parameters: - permission_type Permission type (file, service, ...) + permission_type Permission type (file, service, ...) Returns: Dictionary containing permission map @@ -436,6 +458,10 @@ def get_permission_dict(permission_type: str) -> dict: except KeyError: raise WConvException(f"get_permissions_dict(... - Unknown permission type '{permission_type}'") +PERMISSIONS_REVERSE_MAPPING = { + perm_type: {v: k for k, v in perm_dict.items()} + for perm_type, perm_dict in PERM_TYPE_MAPPING.items() +} class Ace: ''' @@ -450,13 +476,13 @@ def __init__(self, ace_type: int, ace_flags: list[str], permissions: list[str], The init function takes the six different ACE components and constructs an object out of them. Parameters: - ace_type integer that specifies the ACE type (see ACE_TYPES) - ace_flags ace_flags according to the sddl specifications - permissions permissions defined inside the ACE - object_type object_type according to the sddl specifications - i_object_type inherited_object_type according to the sddl specifications - trustee trustee the ACE applies to - numeric Integer ace value + ace_type integer that specifies the ACE type (see ACE_TYPES) + ace_flags ace_flags according to the sddl specifications + permissions permissions defined inside the ACE + object_type object_type according to the sddl specifications + i_object_type inherited_object_type according to the sddl specifications + trustee trustee the ACE applies to + numeric Integer ace value Returns: None @@ -479,7 +505,7 @@ def __str__(self) -> str: Returns: String representation of ACE ''' - result = f'ACE Type:\t {ACE_TYPES[self.ace_type]}\n' + result = f'ACE Type:\t {ACE_TYPES.get(self.ace_type, "Unknown")}\n' if self.trustee: result += f'Trustee:\t {self.trustee}\n' @@ -502,14 +528,15 @@ def pretty_print(self, indent: str = ' ') -> None: ideal to be placed inside a library, but for now we can live with that. Parameters: - indent Spaces after the '[+]' prefix + indent Spaces after the '[+]' prefix Returns: None ''' - if self.ace_type: + ace_type_name = ACE_TYPES.get(self.ace_type, 'Unknown') + if self.ace_type is not None: print_blue(f'[+]{indent}ACE Type:\t', end='') - print_yellow(ACE_TYPES[self.ace_type]) + print_yellow(ace_type_name) if self.trustee: print_blue(f'[+]{indent}Trustee:\t', end='') @@ -520,7 +547,7 @@ def pretty_print(self, indent: str = ' ') -> None: else: print_yellow(self.trustee) - if self.numeric: + if self.numeric is not None: print_blue(f'[+]{indent}Numeric:\t', end='') print_yellow('0x{:08x}'.format(self.numeric)) @@ -547,6 +574,7 @@ def pretty_print(self, indent: str = ' ') -> None: print_blue('[+]', end='') print_yellow(f'{indent}\t\t+ {perm}') + @staticmethod def clear_parentheses(ace_string: str) -> str: ''' Removes the opening and closing parantheses from an ACE string (if present). @@ -565,6 +593,7 @@ def clear_parentheses(ace_string: str) -> str: return ace_string + @staticmethod def get_ace_flags(ace_flag_string: str) -> list[str]: ''' Parses the flag-portion of an ACE string and returns a list of the corresponding @@ -590,6 +619,7 @@ def get_ace_flags(ace_flag_string: str) -> list[str]: return ace_flags + @staticmethod def get_ace_permissions(ace_permission_string: str, perm_type: str = 'file') -> list[str]: ''' Takes the ACE portion containing the permission and returns a list of the corresponding parsed @@ -624,6 +654,7 @@ def get_ace_permissions(ace_permission_string: str, perm_type: str = 'file') -> return permissions + @staticmethod def get_ace_numeric(ace_permission_string: str) -> int: ''' Takes the ACE portion containing the permission and returns the corresponding integer value. @@ -648,6 +679,7 @@ def get_ace_numeric(ace_permission_string: str) -> int: return ace_int + @staticmethod def from_string(ace_string: str, perm_type: str = 'file') -> Ace: ''' Parses an ace from a string in SDDL representation (e.g. A;OICI;FA;;;BA). @@ -690,6 +722,7 @@ def from_string(ace_string: str, perm_type: str = 'file') -> Ace: return Ace(ace_type, ace_flags, permissions, object_type, inherited_object_type, trustee, ace_int) + @staticmethod def from_int(integer: str | int, perm_type: str = 'file') -> Ace: ''' Parses an ace from an integer value in string representation. @@ -720,6 +753,7 @@ def from_int(integer: str | int, perm_type: str = 'file') -> Ace: return Ace(None, None, permissions, None, None, None, ace_int) + @staticmethod def from_bytes(byte_data: bytes, perm_type: str = 'file') -> Ace: ''' Parses an ACE from a bytes object. @@ -745,7 +779,7 @@ def from_bytes(byte_data: bytes, perm_type: str = 'file') -> Ace: object_type = None object_type_inherited = None - if ACE_TYPES[ace_type].endswith('OBJECT'): + if ACE_TYPES.get(ace_type, '').endswith('OBJECT'): object_flags = struct.unpack(' Ace: return Ace(ace_type, ace_flag_list, permissions, object_type, object_type_inherited, trustee, ace_perms) + @staticmethod def toggle_permission(integer: str | int, permissions: list[str]) -> str: ''' Takes an ace in integer format and toggles the specified permissions on it. @@ -800,3 +835,69 @@ def toggle_permission(integer: str | int, permissions: list[str]) -> str: raise WConvException(f"toggle_permission(... - Unknown permission name '{permission}'") return "0x{:08x}".format(ace_int) + + def to_sddl(self, perm_type: str = 'file') -> str: + ''' + Converts the ACE object into its SDDL string representation. + + Parameters: + perm_type Object type the sddl applies to (file, service, ...) + + Returns: + ACE string in SDDL format + ''' + try: + ace_type_sddl = ACE_SDDL_REVERSE[self.ace_type] + except KeyError: + raise WConvException(f"to_sddl(... - Unknown ACE type '{self.ace_type}'") + + ace_flags_sddl = '' + for flag in self.ace_flags: + try: + ace_flags_sddl += ACE_FLAGS_REVERSE.get(flag, '') + except KeyError: + raise WConvException(f"to_sddl(... - Unknown ACE flag '{flag}'") + + permissions_sddl = '' + perm_reverse_dict = PERMISSIONS_REVERSE_MAPPING[perm_type] + + # Check for group permission matches + perm_set = set(self.permissions) + matched_grouped = False + + for grouped_perm, perm_list in GROUPED_PERMISSIONS.items(): + perm_items = set(perm_list.split(',')) + + if perm_items.issubset(perm_set): + permissions_sddl += grouped_perm + perm_set -= perm_items + matched_grouped = True + + # Process remaining individual permissions + for perm in self.permissions: + if matched_grouped and perm not in perm_set: + continue + + try: + perm_sddl = perm_reverse_dict[perm] + except KeyError: + raise WConvException(f"to_sddl(... - Unknown permission name '{perm}'") + + permissions_sddl += perm_sddl + + object_type_sddl = '' + if self.object_type: + object_type_sddl = str(self.object_type) + + inherited_object_type_sddl = '' + if self.inherited_object_type: + inherited_object_type_sddl = str(self.inherited_object_type) + + trustee_sddl = self.trustee.get_well_known() + if trustee_sddl is None: + trustee_sddl = TRUSTEES_REVERSE.get(self.trustee, str(self.trustee)) + else: + trustee_sddl = TRUSTEES_REVERSE[" ".join(word.capitalize() for word in trustee_sddl.replace('_', ' ').split())] + + sddl_string = f'({ace_type_sddl};{ace_flags_sddl};{permissions_sddl};{object_type_sddl};{inherited_object_type_sddl};{trustee_sddl})' + return sddl_string \ No newline at end of file diff --git a/wconv/acl.py b/wconv/acl.py index 37b4093..48a53ec 100644 --- a/wconv/acl.py +++ b/wconv/acl.py @@ -45,3 +45,24 @@ def from_bytes(byte_data: bytes, perm_type: str = 'file') -> Acl: pos += ace_length return Acl(revision, ace_count, ace_list) + + def to_sddl(self, acl_type: str = 'D', perm_type: str = 'file') -> str: + ''' + Converts the Acl object (a collection of ACEs) into its SDDL string representation. + + Parameters: + acl_type The type tag for the ACL (D for DACL, S for SACL). Defaults to 'D'. + perm_type The object type the descriptor applies to (passed to Ace.to_sddl). + + Returns: + ACL string in SDDL format (e.g., "D:(A;;...)(A;;...)") + ''' + ace_sddl_strings = [] + for ace in self.aces: + ace_sddl_strings.append(ace.to_sddl(perm_type=perm_type)) + + aces_sddl = "".join(ace_sddl_strings) + + acl_sddl = f'{acl_type}:{aces_sddl}' + + return acl_sddl diff --git a/wconv/main.py b/wconv/main.py index edfae83..a86258b 100644 --- a/wconv/main.py +++ b/wconv/main.py @@ -68,6 +68,7 @@ parser_desc = subparsers.add_parser('desc', help='convert security descriptor') parser_desc.add_argument('desc', metavar='b64', help='security descriptor in base64') parser_desc.add_argument('--hex', action='store_true', help='specify the descriptor in hex format instead') +parser_desc.add_argument('--to-sddl', action='store_true', dest='to_sddl', help='convert the descriptor to SDDL format') parser_desc.add_argument('--type', metavar='type', choices=typelist, default='ad', help='permission type (default: ad)') parser_desc.add_argument('--sid', metavar='sid', help='filter for a specific sid') parser_desc.add_argument('--adminsd', action='store_true', help='filter out inherited ACEs') @@ -238,20 +239,23 @@ def main(): else: desc = wconv.securitydescriptor.SecurityDescriptor.from_base64(args.desc, args.type) - if args.sid: + if not args.to_sddl: + if args.sid: - for ace in desc.filter_sid(args.sid): - ace.pretty_print() - print_blue('[+]') + for ace in desc.filter_sid(args.sid): + ace.pretty_print() + print_blue('[+]') - if args.adminsd: + if args.adminsd: - for ace in desc.filter_inherited(): - ace.pretty_print() - print_blue('[+]') + for ace in desc.filter_inherited(): + ace.pretty_print() + print_blue('[+]') + else: + desc.pretty_print() else: - desc.pretty_print() + print(desc.to_sddl(perm_type=args.type)) ########################################################################## ####### No Command Selected ###### @@ -262,3 +266,6 @@ def main(): except wconv.WConvException as e: print("[-] Error: " + str(e)) sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/wconv/securitydescriptor.py b/wconv/securitydescriptor.py index a2adf5b..f9dcd30 100644 --- a/wconv/securitydescriptor.py +++ b/wconv/securitydescriptor.py @@ -37,10 +37,12 @@ def pretty_print(self, indent: str = ' ') -> None: None ''' print_blue(f'[+]{indent}Owner:\t', end='') - self.owner.pretty_print() + if self.owner: + self.owner.pretty_print() print_blue(f'[+]{indent}Group:\t', end='') - self.group.pretty_print() + if self.group: + self.group.pretty_print() print_blue(f'[+]{indent}Ace Count:\t', end='') print_yellow(self.dacl.ace_count) @@ -142,3 +144,36 @@ def from_hex(hex_string: str, perm_type: str = 'file') -> SecurityDescriptor: dacl = Acl.from_bytes(byte_data[dacl_offset:], perm_type) return SecurityDescriptor(owner_sid, group_sid, sacl, dacl) + + def print_aces(self) -> None: + ''' + Prints the formatted security descriptor ACEs only. + ''' + for ace in self.dacl.aces: + print(ace.to_sddl()) + + def to_sddl(self, perm_type: str = 'file') -> str: + ''' + Converts the SecurityDescriptor to SDDL format. + + Parameters: + perm_type Object type the descriptor applies to (file, service, ...) + + Returns: + SDDL string + ''' + sddl_parts = [] + + # Owner + if self.owner: + sddl_parts.append(f'O:{self.owner}') + + # Group + if self.group: + sddl_parts.append(f'G:{self.group}') + + # DACL + if self.dacl: + sddl_parts.append(f'{self.dacl.to_sddl(perm_type=perm_type)}') + + return ''.join(sddl_parts) diff --git a/wconv/sid.py b/wconv/sid.py index 81079ae..ba71800 100644 --- a/wconv/sid.py +++ b/wconv/sid.py @@ -46,7 +46,7 @@ "S-1-5-21-[-0-9]+-500": "ADMINISTRATOR", "S-1-5-21-[-0-9]+-501": "GUEST", "S-1-5-21-[-0-9]+-502": "KRBTGT", - "S-1-5-21-[-0-9]+-512": "DOMAIN_ADMINS", + "S-1-5-21-[-0-9]+-512": "DOMAIN_ADMINS", "S-1-5-21-[-0-9]+-513": "DOMAIN_USERS", "S-1-5-21-[-0-9]+-514": "DOMAIN_GUESTS", "S-1-5-21-[-0-9]+-515": "DOMAIN_COMPUTERS", @@ -189,7 +189,7 @@ def get_binary_length(self) -> int: dash_count = self.binary[1] return dash_count * 4 + 8 - def get_well_known(sid_string: str) -> str: + def get_well_known(self) -> str: ''' Get the well known name for the specified SID string. Notice that the WELL_KNOWN_SIDS dictionary contains regex like expression. A simple @@ -203,7 +203,7 @@ def get_well_known(sid_string: str) -> str: ''' for key, value in WELL_KNOWN_SIDS.items(): - if re.match(f'^{key}$', sid_string): + if re.match(f'^{key}$', str(self)): return value return None @@ -261,6 +261,7 @@ def format_sid(sid_value: bytes | list[int]) -> str: return result[0:-1] + def to_b64(self) -> str: ''' Converts an SecurityIdentifier object to base64.