diff options
Diffstat (limited to '')
-rw-r--r-- | libcli/security/tests/windows/windows-sddl-tests.py | 181 |
1 files changed, 181 insertions, 0 deletions
diff --git a/libcli/security/tests/windows/windows-sddl-tests.py b/libcli/security/tests/windows/windows-sddl-tests.py new file mode 100644 index 0000000..38acb44 --- /dev/null +++ b/libcli/security/tests/windows/windows-sddl-tests.py @@ -0,0 +1,181 @@ +# Test SDDL strings on Windows +# +# +# Copyright (c) 2023 Catalyst IT +# +# GPLv3+. +# +# This uses the Python win32 module to access +# ConvertStringSecurityDescriptorToSecurityDescriptor and the like. To +# install this, you need to go +# +# pip install pywin32 +# +# or something like that. + +import argparse +from difflib import SequenceMatcher +from collections import defaultdict +import sys +import json + +try: + import win32security as w +except ImportError: + print("This test script is meant to be run on Windows using the pywin32 module.") + print("To install this module, try:\n") + print("pip install pywin32") + sys.exit(1) + + +# This is necessary for ANSI colour escapes to work in Powershell. +import os +os.system('') + +RED = "\033[1;31m" +GREEN = "\033[1;32m" +DARK_YELLOW = "\033[0;33m" +C_NORMAL = "\033[0m" + +def c_RED(s): + return f"{RED}{s}{C_NORMAL}" +def c_GREEN(s): + return f"{GREEN}{s}{C_NORMAL}" +def c_DY(s): + return f"{DARK_YELLOW}{s}{C_NORMAL}" + + +def read_strings(files): + """Try to read as JSON a JSON dictionary first, then secondly in the bespoke + sddl-in -> sddl-out + format used by other Samba SDDL test programs on Windows. + """ + pairs = [] + for filename in files: + with open(filename) as f: + try: + data = json.load(f) + print(f"loading {filename} as JSON") + for k, v in data.items(): + if not v or not isinstance(v, str): + v = k + pairs.append((k, v)) + continue + except json.JSONDecodeError: + pass + + print(f"loading {filename} as 'a -> b' style") + f.seek(0) + for line in f: + line = line.rstrip() + if line.startswith('#') or line == '': + continue + # note: if the line does not have ' -> ', we expect a + # perfect round trip. + o, _, c = line.partition(' -> ') + if c == '': + c = o + pairs.append((o, c)) + + return pairs + + +def colourdiff(a, b): + out = [] + a = a.replace(' ', '␠') + b = b.replace(' ', '␠') + + s = SequenceMatcher(None, a, b) + for op, al, ar, bl, br in s.get_opcodes(): + if op == 'equal': + out.append(a[al: ar]) + elif op == 'delete': + out.append(c_RED(a[al: ar])) + elif op == 'insert': + out.append(c_GREEN(b[bl: br])) + elif op == 'replace': + out.append(c_RED(a[al: ar])) + out.append(c_GREEN(b[bl: br])) + else: + print(f'unknown op {op}!') + + return ''.join(out) + + +def no_print(*args, **kwargs): + pass + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--export-bytes', const='sddl_bytes.json', nargs='?', + help='write JSON file containing SD bytes') + parser.add_argument('--quiet', action='store_true', + help='avoid printing to stdout') + parser.add_argument('files', nargs='+', help='read these files') + + args = parser.parse_args() + + if args.quiet: + global print + print = no_print + + cases = read_strings(args.files) + parseable_cases = [] + unparseable_cases = [] + unserializeable_cases = [] + round_trip_failures = [] + exceptions = defaultdict(list) + bytes_json = {} + + print(f"{len(set(cases))}/{len(cases)} unique pairs, " + f"{len(set(x[0] for x in cases))}/{len(cases)} unique strings") + + for a, b in sorted(set(cases)): + try: + sd = w.ConvertStringSecurityDescriptorToSecurityDescriptor(a, 1) + except Exception as e: + print(a) + exceptions[f"{e} parse"].append(a) + print(c_RED(e)) + unparseable_cases.append(a) + continue + + parseable_cases.append(a) + + try: + # maybe 0xffff is an incorrect guess -- it gives use v2 (NT), not v4 (AD) + c = w.ConvertSecurityDescriptorToStringSecurityDescriptor(sd, 1, 0xffff) + except Exception as e: + print(f"could not serialize '{sd}': {e}") + print(f" derived from '{a}'") + exceptions[f"{e} serialize"].append(a) + unserializeable_cases.append(a) + continue + + if args.export_bytes: + bytes_json[c] = list(bytes(sd)) + + if c != b: + round_trip_failures.append((a, b, c)) + exceptions["mismatch"].append(a) + #print(f"{c_GREEN(a)} -> {c_DY(c)}") + print(colourdiff(b, c)) + print(c_DY(f"{b} -> {c}")) + + for k, v in exceptions.items(): + print(f"{k}: {len(v)}") + + print(f"{len(unparseable_cases)} failed to parse") + print(f"{len(parseable_cases)} successfully parsed") + print(f"{len(unserializeable_cases)} of these failed to re-serialize") + print(f"{len(round_trip_failures)} of these failed to round trip") + #for p in parseable_cases: + # print(f"«{c_GREEN(p)}»") + + if args.export_bytes: + with open(args.export_bytes, 'w') as f: + json.dump(bytes_json, f) + print(f"wrote bytes to {args.export_bytes}") + +main() |